dumbtv
dumbtv가 ADB로 Fire TV에서 광고·추적·업데이트 알림을 제거, 루트·앱 변경 없이, 무료 오픈소스
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗An internal tool for submitting, reviewing, assigning, and controlling access to supplier account credentials ("IDs"). There are three kinds of user, each with their own sign-in and their own screens:
| Role | How the login is created | What they can do |
|---|---|---|
| Supplier | Signs up himself at /signup | Submit IDs; see his own dashboard — how many IDs he gave, and which were accepted or rejected (with the reason), day by day, and which IDs he has been paid for (with the date — never the amount). Only accepted IDs are paid. Never sees a stored password again, and never sees another supplier's IDs. |
| Media buyer | Added by an admin on Suppliers & buyers | See only the IDs assigned to him, with their login details (passwords, Outlook mail, temp mail, 2FA key) shown in plain text so he can check them, change a password / 2FA key if it changes, and mark each one active or rejected (a note saying why is required). The New today count and filter show which IDs he was given today. |
| Admin | Promoted by hand (see below) | See everything, credentials shown in plain text; change any ID's passwords / 2FA key; accept or reject IDs; assign IDs to a media buyer by name, one at a time or in bulk; add media buyers; see on which date each supplier gave how many IDs, pay for that day's accepted IDs (Mark paid with the amount), and see each month's IDs bought / pending / accepted / rejected / paid for and the total spent (dashboard and Supplier payments); export. |
Life of an ID: a supplier submits it (pending) → an admin accepts it, or assigns it to a media buyer, which also accepts it (accepted) → the media buyer checks it and marks it active or rejected. An admin can also reject, archive or reassign at any point. Credentials are shown in plain text on an ID's page, to an admin or the media buyer it is assigned to only; every time that page is opened it is freshly authorized and audit-logged.
Stack: Next.js 16 (App Router) + TypeScript + Tailwind CSS v4, Supabase (Postgres + Auth). Credentials are stored as plain text — no encryption key is needed (see Credential storage).
With no configuration at all, the app runs in demo mode: an in-memory, fictional data set with a demo admin, two demo media buyers and two demo suppliers. Nothing is persisted — it resets every time the server restarts — and this is loudly banner-labeled in the UI. The view/audit flow behaves like production, just without durability.
npm install
npm run dev
On Windows you can instead double-click start-app.bat, which runs those two commands
and opens the browser.
Open http://localhost:3000 — you'll land on the sign-in page. In demo mode, pick any of
the listed demo users to see that role's screens (a simulated sign-in, clearly marked as
such — see src/app/actions/auth.ts), or use Create an account to try the supplier
sign-up.
supabase/migrations/ in filename order:
0001_schema.sql — tables, triggers, the profiles role sync trigger.0002_rls.sql — Row Level Security policies. This is the real authorization
boundary; the app's server checks are a second layer, not a substitute.0003_secret_presence.sql — a narrow, presence-only RPC so the UI can show
"a value is stored" without ever exposing ciphertext through it.0004_two_factor_secret.sql — allows storing an encrypted 2FA key.0005_roles_assignment.sql — supplier / media buyer / admin roles, which supplier
an ID belongs to, which media buyer it is assigned to, the accepted / rejected
statuses and the rejection note. If your database already exists, this is the one
you still need to run. It is safe to run more than once; it renames the old team
role to supplier and moves any needs_review IDs back to pending.0006_buyer_source.sql — lets a media buyer add IDs of his own.0007_supplier_payments.sql — the Supplier payments page: which supplier was
paid for which day. Admin-only (RLS). Safe to run more than once.0008_paid_per_id.sql — only accepted IDs are paid; each paid ID gets its own
paid_at. Run it before deploying the version that uses it. Safe to run more
than once.supabase/seed.sql for fictional sample suppliers/accounts (no secrets —
attach fictional secret values afterward through the app UI)..env.example to .env.local and fill in:
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY — Project Settings → API.SUPABASE_SERVICE_ROLE_KEY — same page. Server-only. Never expose this to the
browser, never prefix with NEXT_PUBLIC_, never commit it.ENCRYPTION_KEYS — optional, only to read credentials saved before they were
stored as plain text; see the next section.npm run dev. The app detects the Supabase env vars and leaves demo mode automatically.New sign-ups are always the supplier role (enforced by the handle_new_user trigger and
by RLS — a user can never change their own role). Suppliers sign up at /signup; the
login is created by the server with the email already confirmed, so sign-up does not
depend on the Supabase project's email settings, and there is no email verification —
anyone who knows the address of the site can create a supplier account. Media buyers are
added by an admin inside the app. For the first admin, see
Creating the first admin.
Passwords, email passwords and 2FA keys are stored in account_secrets as plain text
(the value is in the ciphertext column, key_version = -1). No encryption key is needed
to save or read them. Access still goes only through the server: RLS denies all direct
client access to account_secrets, and every view or export is authorized and
audit-logged. But anyone who can read that table — a database backup, the Supabase
dashboard, or whoever holds SUPABASE_SERVICE_ROLE_KEY — can read every credential, so
protect those accordingly.
Older, encrypted rows. Credentials saved before this change are AES-256-GCM encrypted
(key_version >= 1). They can still be read as long as their key stays in
ENCRYPTION_KEYS (e.g. ENCRYPTION_KEYS={"1":"<key>"}); without it they show as
"could not decrypt". Changing such a value in the app re-saves it as plain text.
npm run check-key tells you whether a key opens them.
Converting them all at once. Deploy this version first (the older one cannot read
plain-text rows). Then, with NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY and
ENCRYPTION_KEYS in .env.local (or the environment):
npm run decrypt-secrets
This is a dry run: it checks that every encrypted row can be read and lists any that can't, without changing anything. Then convert them:
npm run decrypt-secrets -- --apply
It never prints a credential value, skips rows that are already plain text, leaves any
row it can't read as it is, and writes one secret.bulk_convert_plaintext audit entry.
Converted rows get a new updated_at. It is safe to run again. When it reports
Still encrypted: 0, remove ENCRYPTION_KEYS and ENCRYPTION_KEY_VERSION from every
environment and redeploy. Keep a private copy of the key for as long as you might restore
a database backup taken before the conversion.
There's no bootstrap UI for this on purpose (an admin-creation button reachable by anyone
would defeat the point). After a user has signed in at least once (so a profiles row
exists via the handle_new_user trigger), promote them from the Supabase SQL editor:
update public.profiles set role = 'admin' where email = '[email protected]';
profiles.role is the single source of truth for authorization, checked fresh on every
server action, route handler, and RLS policy — never cached from a JWT claim.profiles_update_self_limited
RLS policy enforces this at the database level, independent of the app code)./signup.supplier with the
same kind of SQL update (they immediately lose the media-buyer screens and can no longer
see any credential), and ban the user in Supabase → Authentication if they should not
be able to sign in at all. IDs that were assigned to them show "Former media buyer" in
the admin list until you reassign them..env.example's variables in your host's environment configuration (e.g.
Vercel → Project → Settings → Environment Variables). Never commit them.SUPABASE_SERVICE_ROLE_KEY and ENCRYPTION_KEYS are marked server-only /
"sensitive" if your host distinguishes that; they must never end up in a client bundle.npm run build && npm run start, or deploy via your platform's normal Next.js flow.supabase/migrations/0002_rls.sql.account_secrets denies all direct client access. The only way to reach it is the
server's service-role client inside src/lib/data/secrets.ts, which re-checks the
caller's authentication and role from scratch on every single call — view or write.
An admin can see any ID's credentials; a media buyer only those of an ID assigned to
him at that moment (unassigning cuts him off immediately); a supplier never.accounts at all. Their screens go
through server code (src/lib/data/accounts.ts) that checks the role and then filters
by supplier_id / assigned_to itself, using the id from the session — never one from
the URL or a form. Directly against the database, with their own login, they can read
nothing but their own profile row.getSecretsForViewer in src/lib/data/secrets.ts and
src/components/CredentialsList.tsx. Anyone looking at that screen can read them.secret.view) — actor, record, which fields were shown,
outcome — with the secret values themselves never included.changeSecret
(src/lib/data/secrets.ts). The new value is stored as plain text like any other, and every
attempt is audit-logged (secret.update) without the value.src/app/api/export/accounts/route.ts)
decrypts every password and 2FA key into the file, one sheet per day. Every export is
audit-logged and the filename contains — treat the downloaded file as
being as sensitive as the whole database.This template does not implement automatic retention/deletion — decide policy for your organization and enforce it operationally (e.g. a scheduled job) or ask for scoped follow-up work:
account_secrets rows.Works out of the box (demo mode, no setup):
Works once you configure Supabase (see setup above):
Needs your decision / follow-up work, not included by default:
src/
app/
actions/ Server Actions (auth, accounts, secrets, users) — the only write paths
api/export/ Admin-only Excel export (includes decrypted credentials)
admin/ Admin pages (dashboard, all IDs, suppliers & buyers)
buyer/ Media buyer pages (my IDs, check one ID)
supplier/ Supplier pages (my dashboard, submit an ID)
signup/ Supplier sign-up
login/ Sign-in for every role (real or simulated demo sign-in)
team/ Old links only — redirects to the supplier area
components/ Shared UI (CredentialsList, AccountForm, AccountTable, AssignSelect, RoleShell, ...)
lib/
crypto.ts credential storage format (plain text; reads old AES rows) — server-only
auth.ts getCurrentUser/requireUser/requireRole/requireAdmin — fresh check every call
data/ Data-access layer (accounts, profiles, secrets, audit)
demo/ In-memory demo-mode store (never used when Supabase is configured)
supabase/ RLS-bound client (server.ts) and service-role client (admin.ts)
supabase/
migrations/ Schema + RLS policies, run in order
seed.sql Fictional sample data (no secrets)
scripts/generate-key.mjs Prints a new base64 AES-256 key
scripts/decrypt-secrets.mjs One-time: converts old encrypted credentials to plain text
CREDENTIALSrequireAdmin()) and by the accounts_admin_update /
accounts_select_admin_only RLS policies.accounts
has no INSERT policy for anon/authenticated at all — the same "deny by default,
only the service-role server function can reach it" pattern this app already uses for
account_secrets and audit_log. See createAccount in src/lib/data/accounts.ts.