refactored Authentication
This commit is contained in:
@@ -17,6 +17,7 @@ import { resendVerificationHandler } from './resend-verification';
|
||||
import { changePasswordHandler } from './change-password';
|
||||
import { forgotPasswordHandler } from './forgot-password';
|
||||
import { resetPasswordHandler } from './reset-password';
|
||||
import { bootstrapHandler } from './bootstrap';
|
||||
import { usersMe } from './users-me';
|
||||
import { passkeyRouter } from './passkey-router';
|
||||
|
||||
@@ -32,6 +33,7 @@ authRouter.get('/me', userMiddleware, usersMe);
|
||||
authRouter.post('/signin', signinRateLimiter, signinHandler);
|
||||
authRouter.post('/signout', userMiddleware, signoutHandler);
|
||||
authRouter.post('/signup', signupRateLimiter, signupHandler);
|
||||
authRouter.post('/bootstrap', signupRateLimiter, bootstrapHandler);
|
||||
authRouter.post('/verify', verifyHandler);
|
||||
authRouter.post('/verify-token', verifyTokenHandler);
|
||||
authRouter.post('/resend-verification', resendVerificationHandler);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Handler } from 'hono';
|
||||
import { sendMail } from 'emailer';
|
||||
import { officerdb, count, Users } from 'officerdb';
|
||||
import { sign, verify } from '@@/jwt';
|
||||
import argon2 from 'argon2';
|
||||
import * as errors from '@@/custom-errors';
|
||||
import { validatePassword } from './validate-password';
|
||||
|
||||
export const bootstrapHandler: Handler = async function (ctx) {
|
||||
const body = ctx.get('body');
|
||||
const origin = ctx.get('origin');
|
||||
const token = body.token as string;
|
||||
const email = body.email as string;
|
||||
|
||||
const result = await officerdb.select({ count: count() }).from(Users);
|
||||
const userCount = result[0]?.count ?? 0;
|
||||
if (userCount > 0) throw errors.FORBIDDEN('Registration is closed');
|
||||
|
||||
if (!token) {
|
||||
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
throw errors.BAD_REQUEST('Invalid email address');
|
||||
}
|
||||
|
||||
const verificationCode = await sign({ email }, '24h');
|
||||
const url = `${origin}/auth/verify?verificationCode=${verificationCode}`;
|
||||
|
||||
await sendMail({
|
||||
template: 'VerifyAdmin',
|
||||
subject: 'Verify your Officer account',
|
||||
to: email,
|
||||
data: { name: email, url },
|
||||
});
|
||||
|
||||
return ctx.json({ ok: true });
|
||||
}
|
||||
|
||||
const payload = await verify(token).catch(() => null) as { email: string } | null;
|
||||
if (!payload?.email) throw errors.BAD_REQUEST('Invalid or expired token');
|
||||
|
||||
const name = body.name as string;
|
||||
const username = body.username as string;
|
||||
const password = body.password as string;
|
||||
const confirmPassword = body.confirmPassword as string;
|
||||
|
||||
if (!name || !name.trim()) throw errors.BAD_REQUEST('Name is required');
|
||||
if (!username || !username.trim()) throw errors.BAD_REQUEST('Username is required');
|
||||
validatePassword(password);
|
||||
if (password !== confirmPassword) throw errors.BAD_REQUEST('Passwords do not match');
|
||||
|
||||
const passwordHash = await argon2.hash(password);
|
||||
|
||||
const insertedUsers = await officerdb.insert(Users).values({
|
||||
email: payload.email,
|
||||
password: passwordHash,
|
||||
name: name.trim(),
|
||||
username: username.trim(),
|
||||
role: 'Super Admin',
|
||||
status: 'Active',
|
||||
}).returning();
|
||||
|
||||
if (!insertedUsers || insertedUsers.length === 0) throw errors.INTERNAL_SERVER_ERROR('Failed to create user');
|
||||
return ctx.json({ ok: true });
|
||||
};
|
||||
@@ -13,7 +13,7 @@ export const forgotPasswordHandler: Handler = async function (ctx) {
|
||||
});
|
||||
|
||||
if (!dbUser) return ctx.json({ ok: true });
|
||||
const verificationCode = await sign({ id: dbUser.id, email }, '6h');
|
||||
const verificationCode = await sign({ id: dbUser.id, email, purpose: 'reset-password' }, '6h');
|
||||
const url = `${PUBLIC_URL}/auth/reset-password?verificationCode=${verificationCode}`;
|
||||
|
||||
await sendMail({
|
||||
|
||||
@@ -15,13 +15,21 @@ export const verifyTokenHandler: Handler = async function (ctx) {
|
||||
throw errors.BAD_REQUEST('Token is invalid or expired');
|
||||
}
|
||||
|
||||
// Bootstrap token: has email but no id (user not yet created)
|
||||
if (userInfo?.email && !userInfo?.id) {
|
||||
return ctx.json({ ok: true, email: userInfo.email });
|
||||
}
|
||||
|
||||
if (!userInfo?.id) throw errors.BAD_REQUEST('Token is invalid or expired');
|
||||
|
||||
const user = await officerdb.query.Users.findFirst({
|
||||
where: eq(Users.id, userInfo.id),
|
||||
});
|
||||
if (!user) throw errors.NOT_FOUND('User not found');
|
||||
if (user.status !== 'Unverified') throw errors.BAD_REQUEST('Account is already verified');
|
||||
|
||||
// Reset-password tokens skip the verification status check
|
||||
const isResetToken = (userInfo as Record<string, unknown>).purpose === 'reset-password';
|
||||
if (!isResetToken && user.status !== 'Unverified') throw errors.BAD_REQUEST('Account is already verified');
|
||||
|
||||
return ctx.json({ ok: true, email: user.email });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { officerdb, count, Users } from 'officerdb';
|
||||
|
||||
export const landingPageDataRouter = createRouter();
|
||||
|
||||
landingPageDataRouter.get('/', async (ctx) => {
|
||||
const result = await officerdb.select({ count: count() }).from(Users);
|
||||
const userCount = result[0]?.count ?? 0;
|
||||
return ctx.json({ registrationOpen: userCount === 0 });
|
||||
});
|
||||
@@ -23,12 +23,6 @@ serverSettingsRouter.route('/claude-code', claudeCodeRouter);
|
||||
serverSettingsRouter.route('/opencode', opencodeRouter);
|
||||
serverSettingsRouter.route('/applications', applicationsRouter);
|
||||
|
||||
serverSettingsRouter.get('/', async (ctx) => {
|
||||
const result = await officerdb.select({ count: count() }).from(Users);
|
||||
const userCount = result[0]?.count ?? 0;
|
||||
return ctx.json({ registrationOpen: userCount === 0 });
|
||||
});
|
||||
|
||||
serverSettingsRouter.get('/settings', async (ctx) => {
|
||||
const settings = await Bun.file(settingsPath).json();
|
||||
return ctx.json(settings);
|
||||
|
||||
Reference in New Issue
Block a user