TutorLoom
The learning platform for companies, colleges and training institutes. Free and open source, with an Enterprise edition.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Test
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗An AI-assisted career assessment for students and graduates. Someone answers a 46-question assessment, gets a scored breakdown of their strongest career clusters, and a written report explaining why they fit them. A paid follow-up questionnaire produces a more detailed roadmap.
Next.js 16 (App Router) · Supabase (auth + Postgres) · Stripe · OpenAI · Upstash Redis · Sentry · deployed on Vercel.
npm install
cp .env.example .env.local # then fill it in
npm run dev
.env.example lists every variable with a note on what it is for. The app
starts without the optional ones — email and Stripe branding degrade quietly
rather than crashing.
npm run build # production build
npm run test # 161 tests
npx tsc --noEmit # type check
npx eslint . # lint
These are the decisions that look wrong until you know why. Each one has a longer explanation in the file itself.
The brand lives in lib/site.ts. BRAND holds the name and the short
descriptor shown beside it; COMPANY.tradingName in lib/legal.ts is set from
it, and every page, email, prompt and the Stripe checkout header read from one
or the other. The images (icon, favicon, link previews, Stripe logo) are drawn
by scripts/generate-brand-assets.mjs; run it again after changing either.
The session is httpOnly and OAuth stays on one origin. Browser JavaScript
never sees the access token; the client learns who it is from
GET /api/auth/me. Three different places used to write the auth cookies with
different flags and whichever ran last won, which is what made Google sign-in
work only on the second attempt. lib/auth-cookies.ts is now the single place
that decides. Do not "simplify" it back.
Everything server-side uses the Supabase service role, which bypasses RLS.
So route-level authorisation is the real protection, not the database policies:
every route takes the user id from the verified session and never from the
request body. RLS is the second layer beneath that, and the Data API is
disabled at table level above it. scripts/rls-proof.mjs checks all of it.
Money is idempotent by database constraint. The Stripe webhook and the
success page's verify call race each other by design; the UNIQUE constraint on
payments.stripe_session_id decides the winner. Grants are compare-and-swap
with a retry, because two payments settling at once used to overwrite each
other and credit the customer once for two purchases.
A report attempt is a reservation, not a debit. It is consumed before the OpenAI call so two tabs cannot both generate, and handed back if generation fails. Losing a paid attempt to a timeout is the most refund-worthy thing this product can do.
Pricing and attempt rules live only in lib/plans.ts. The checkout route,
the webhook and the pricing UI all read from it. Changing what a product grants
means changing it there and nowhere else.
The model has guardrails, and they are not decoration. lib/crisis.ts
screens free-text answers for distress and puts support information above the
report. lib/guardrails.ts adds rules to every prompt that keep
recommendations lawful and redirect rather than lecture. Both have tests
pinning the behaviour, including the false positives they must not produce.
app/
api/ route handlers — auth, checkout, reports, account, admin
assess/ the main questionnaire
followup/ the paid follow-up questionnaire
history/ past attempts and reports
account/ data export and account deletion
privacy|terms|refunds/ legal pages
lib/
auth*.ts session handling, CSRF, the one-origin rule
plans.ts pricing and what each product grants
fulfillment.ts turning a paid session into account credit
scoring.ts the cluster scoring model
followup-questions.ts the follow-up questionnaire
crisis.ts distress screening and support resources
guardrails.ts lawful-guidance rules for the model
kill-switch.ts the emergency brake
scripts/
rls-proof.mjs prove row level security actually holds
kill-switch.mjs turn report generation or checkout off and on
supabase/
*.sql audit queries and the migrations already applied
Turn something off in a hurry. Reads a Redis key, so it takes effect in about ten seconds with no deploy:
node scripts/kill-switch.mjs status
node scripts/kill-switch.mjs off reports
node scripts/kill-switch.mjs on all
Check the database is still locked down. Run after any policy change or new table:
node scripts/rls-proof.mjs
node scripts/rls-proof.mjs --email [email protected] --password '…'
Get into the admin area. Nobody is an admin until a profiles.role is set
to admin or superadmin — run supabase/admin-setup.sql block 1 once. The
area is guarded three times over (proxy, server component, and the API route),
all reading the same role from profiles. Note profiles.is_admin is a legacy
column that nothing reads; setting it grants nothing.
Fix a customer who paid but cannot reach what they bought. Usually someone who signed up with Google once and with email the next time, so the purchase sits on a different account. Signed in as an admin:
GET /api/admin/[email protected] # find the payment
POST /api/admin/regrant { paymentId, targetUserId, reason }
Lookup and grant are separate on purpose, so you see what you are about to do.
Every regrant is written to audit_logs.
See where people give up. supabase/analytics-setup.sql block 2 is the
funnel; block 3 shows which question number loses people.
lib/legal.ts still has placeholder company details, and every legal page
shows a banner saying so until they are filled in. The banner disappears on its
own. The remaining launch tasks live outside this repo: a domain, Stripe live
keys, a transactional email provider, and a spend cap on the OpenAI account.