feat: welcome desk auth and operator password reset
CI / Test (push) Canceled after 0s
CI / Build and push (push) Canceled after 0s

First visit lands on /welcome instead of burying signup in Settings.
Add a host CLI to reset passwords without a session, plus Settings
change-password. Refresh as-built design docs and the Unraid operator guide.
This commit is contained in:
Investor Flow Build
2026-08-19 21:19:57 -04:00
parent 90e1829d39
commit 7649eaf399
23 changed files with 1692 additions and 658 deletions
@@ -71,6 +71,23 @@ test('me returns null unauthenticated; prefs when session resolves', async () =>
assert.equal(me?.complexity, 'beginner');
});
test('changePassword updates hash; rejects wrong current password', async () => {
const { db, freshCtx } = setup();
const signupCtx = freshCtx();
const { userId } = await appRouter.createCaller(signupCtx).auth.signup({ email: 'a@b.co', password: 'password123' });
db.prepare('UPDATE users SET status=? WHERE id=?').run('active', userId);
const { createSession } = await import('../../trpc/context.ts');
const { cookie } = createSession(db, userId);
const authed = () => appRouter.createCaller(freshCtx(reqWithCookie(cookie.split(';')[0])));
await assert.rejects(
() => authed().auth.changePassword({ current: 'wrong', next: 'newpass123' }),
(e: { code: string }) => e.code === 'UNAUTHORIZED',
);
await authed().auth.changePassword({ current: 'password123', next: 'newpass123' });
const login = await appRouter.createCaller(freshCtx()).auth.login({ email: 'a@b.co', password: 'newpass123' });
assert.ok(login.userId);
});
test('logout clears the session cookie', async () => {
const { freshCtx } = setup();
const ctx = freshCtx();
+10
View File
@@ -129,6 +129,16 @@ const authRouter = router({
ctx.resHeaders.append('Set-Cookie', clearCookie());
return { ok: true };
}),
changePassword: protectedProcedure
.input(z.object({ current: z.string(), next: z.string().min(8) }))
.mutation(({ ctx, input }) => {
const row = ctx.db.prepare('SELECT pw_hash FROM users WHERE id=?').get(ctx.userId) as { pw_hash: string } | undefined;
if (!row || row.pw_hash === 'oauth' || !verifyPassword(input.current, row.pw_hash)) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Current password is wrong.' });
}
ctx.db.prepare('UPDATE users SET pw_hash=? WHERE id=?').run(hashPassword(input.next), ctx.userId);
return { ok: true };
}),
me: publicProcedure.query(({ ctx }) => {
if (!ctx.userId) return null;
const u = ctx.db.prepare(