.
This commit is contained in:
@@ -18,16 +18,16 @@ const sessionView = (adminEmail: string | null): AdminSession => ({
|
||||
});
|
||||
|
||||
export const seedAdminAccount = async (): Promise<void> => {
|
||||
if (!config.appAdminEmail || !config.appAdminPassword) {
|
||||
if (!config.appAdminEmail || !config.appAdminAccessKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = getAdmin(config.appAdminEmail);
|
||||
if (existing && (await Bun.password.verify(config.appAdminPassword, existing.password_hash))) {
|
||||
if (existing && (await Bun.password.verify(config.appAdminAccessKey, existing.password_hash))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordHash = await Bun.password.hash(config.appAdminPassword, {
|
||||
const passwordHash = await Bun.password.hash(config.appAdminAccessKey, {
|
||||
algorithm: "argon2id",
|
||||
memoryCost: 19_456,
|
||||
timeCost: 2
|
||||
@@ -36,18 +36,22 @@ export const seedAdminAccount = async (): Promise<void> => {
|
||||
upsertAdmin(config.appAdminEmail, passwordHash);
|
||||
};
|
||||
|
||||
export const loginAdmin = async (email: string, password: string): Promise<{
|
||||
export const loginAdmin = async (accessKey: string): Promise<{
|
||||
session: AdminSession;
|
||||
cookie: string;
|
||||
}> => {
|
||||
const admin = getAdmin(email);
|
||||
if (!admin) {
|
||||
throw new HttpError(401, "Invalid email or password.");
|
||||
if (!config.appAdminAccessKey) {
|
||||
throw new HttpError(503, "Admin access key is not configured on the server.");
|
||||
}
|
||||
|
||||
const validPassword = await Bun.password.verify(password, admin.password_hash);
|
||||
if (!validPassword) {
|
||||
throw new HttpError(401, "Invalid email or password.");
|
||||
const admin = getAdmin(config.appAdminEmail);
|
||||
if (!admin) {
|
||||
throw new HttpError(401, "Invalid access key.");
|
||||
}
|
||||
|
||||
const validAccessKey = await Bun.password.verify(accessKey, admin.password_hash);
|
||||
if (!validAccessKey) {
|
||||
throw new HttpError(401, "Invalid access key.");
|
||||
}
|
||||
|
||||
const rawToken = createOpaqueToken();
|
||||
@@ -97,4 +101,3 @@ export const getSessionFromRequest = (request: Request): AdminSession => {
|
||||
return sessionView(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+3
-16
@@ -9,8 +9,7 @@ import {
|
||||
loginSchema,
|
||||
parseRouteEmail,
|
||||
resetPasswordSchema,
|
||||
resetTokenSchema,
|
||||
updatePasswordSchema
|
||||
resetTokenSchema
|
||||
} from "./lib/validation";
|
||||
import {
|
||||
consumePasswordReset,
|
||||
@@ -18,8 +17,7 @@ import {
|
||||
deleteUser,
|
||||
inspectPasswordReset,
|
||||
listUsers,
|
||||
triggerPasswordReset,
|
||||
updateUserPassword
|
||||
triggerPasswordReset
|
||||
} from "./users/service";
|
||||
|
||||
const app = new Elysia();
|
||||
@@ -68,7 +66,7 @@ app.post(
|
||||
"/api/auth/login",
|
||||
route(async ({ request }) => {
|
||||
const body = await parseJson(request, loginSchema);
|
||||
const result = await loginAdmin(body.email, body.password);
|
||||
const result = await loginAdmin(body.accessKey);
|
||||
|
||||
return jsonResponse(
|
||||
request,
|
||||
@@ -109,17 +107,6 @@ app.post(
|
||||
})
|
||||
);
|
||||
|
||||
app.patch(
|
||||
"/api/users/:email/password",
|
||||
route(async ({ request, params }) => {
|
||||
requireAdminSession(request);
|
||||
const email = parseRouteEmail(params.email);
|
||||
const body = await parseJson(request, updatePasswordSchema);
|
||||
await updateUserPassword(email, body.password);
|
||||
return emptyResponse(request, 204);
|
||||
})
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/api/users/:email",
|
||||
route(async ({ request, params }) => {
|
||||
|
||||
@@ -29,8 +29,8 @@ export const config = {
|
||||
sessionCookieName: env.SESSION_COOKIE_NAME ?? "mail_admin_session",
|
||||
sessionTtlMs: parseNumber(env.SESSION_TTL_MINUTES, 480) * 60_000,
|
||||
resetTokenTtlMs: parseNumber(env.RESET_TOKEN_TTL_MINUTES, 60) * 60_000,
|
||||
appAdminEmail: env.APP_ADMIN_EMAIL?.trim().toLowerCase() ?? "",
|
||||
appAdminPassword: env.APP_ADMIN_PASSWORD ?? "",
|
||||
appAdminEmail: env.APP_ADMIN_EMAIL?.trim().toLowerCase() ?? "panel-admin@local",
|
||||
appAdminAccessKey: env.APP_ADMIN_ACCESS_KEY ?? "",
|
||||
appMailFrom: env.APP_MAIL_FROM?.trim().toLowerCase() ?? "[email protected]",
|
||||
webDistPath: resolve(process.cwd(), env.WEB_DIST_PATH ?? "../web/dist"),
|
||||
isProduction: (env.NODE_ENV ?? "development") === "production"
|
||||
|
||||
@@ -25,8 +25,11 @@ export const passwordSchema = z
|
||||
.max(128, "Password is too long.");
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: baseEmailSchema,
|
||||
password: z.string().min(1, "Password is required.")
|
||||
accessKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(12, "Access key is required.")
|
||||
.max(256, "Access key is too long.")
|
||||
});
|
||||
|
||||
export const createUserSchema = z.object({
|
||||
@@ -52,4 +55,3 @@ export const routeEmailSchema = z.object({
|
||||
|
||||
export const parseRouteEmail = (value: string): string =>
|
||||
routeEmailSchema.parse({ email: decodeURIComponent(value) }).email;
|
||||
|
||||
|
||||
@@ -32,6 +32,32 @@ const maskEmail = (email: string): string => {
|
||||
const buildResetLink = (token: string): string =>
|
||||
`${config.externalBaseUrl}/reset-password?token=${encodeURIComponent(token)}`;
|
||||
|
||||
const createPasswordResetLink = async (
|
||||
email: string,
|
||||
createdBy: string
|
||||
): Promise<PasswordResetTriggerResponse> => {
|
||||
await ensureUserExists(email);
|
||||
deleteActivePasswordResetTokensForUser(email);
|
||||
|
||||
const rawToken = createOpaqueToken();
|
||||
const tokenHash = hashToken(rawToken);
|
||||
const expiresAtMs = Date.now() + config.resetTokenTtlMs;
|
||||
const expiresAt = new Date(expiresAtMs).toISOString();
|
||||
const resetLink = buildResetLink(rawToken);
|
||||
|
||||
createPasswordResetToken({
|
||||
tokenHash,
|
||||
userEmail: email,
|
||||
createdBy,
|
||||
expiresAt: expiresAtMs
|
||||
});
|
||||
|
||||
return {
|
||||
resetLink,
|
||||
expiresAt
|
||||
};
|
||||
};
|
||||
|
||||
const ensureUserExists = async (email: string): Promise<MailUser> => {
|
||||
const users = await listMailUsers();
|
||||
const existing = users.find((item) => item.email === email);
|
||||
@@ -52,7 +78,8 @@ export const createUser = async (email: string, createdBy: string): Promise<Crea
|
||||
const user = await ensureUserExists(email);
|
||||
|
||||
try {
|
||||
const reset = await triggerPasswordReset(email, createdBy);
|
||||
const reset = await createPasswordResetLink(email, createdBy);
|
||||
await sendPasswordResetEmail(email, reset.resetLink, reset.expiresAt);
|
||||
|
||||
return {
|
||||
user,
|
||||
@@ -70,11 +97,6 @@ export const createUser = async (email: string, createdBy: string): Promise<Crea
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserPassword = async (email: string, password: string): Promise<void> => {
|
||||
await ensureUserExists(email);
|
||||
await updateMailUserPassword(email, password);
|
||||
};
|
||||
|
||||
export const deleteUser = async (email: string): Promise<void> => {
|
||||
await ensureUserExists(email);
|
||||
await deleteMailUser(email);
|
||||
@@ -84,29 +106,7 @@ export const triggerPasswordReset = async (
|
||||
email: string,
|
||||
createdBy: string
|
||||
): Promise<PasswordResetTriggerResponse> => {
|
||||
await ensureUserExists(email);
|
||||
deleteActivePasswordResetTokensForUser(email);
|
||||
|
||||
const rawToken = createOpaqueToken();
|
||||
const tokenHash = hashToken(rawToken);
|
||||
const expiresAtMs = Date.now() + config.resetTokenTtlMs;
|
||||
const expiresAt = new Date(expiresAtMs).toISOString();
|
||||
|
||||
createPasswordResetToken({
|
||||
tokenHash,
|
||||
userEmail: email,
|
||||
createdBy,
|
||||
expiresAt: expiresAtMs
|
||||
});
|
||||
|
||||
try {
|
||||
await sendPasswordResetEmail(email, buildResetLink(rawToken), expiresAt);
|
||||
} catch (error) {
|
||||
deletePasswordResetToken(tokenHash);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { expiresAt };
|
||||
return createPasswordResetLink(email, createdBy);
|
||||
};
|
||||
|
||||
export const inspectPasswordReset = async (token: string): Promise<PasswordResetInspectResponse> => {
|
||||
|
||||
Reference in New Issue
Block a user