.
This commit is contained in:
@@ -29,6 +29,7 @@ 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,
|
||||
mailboxInviteTtlMs: parseNumber(env.MAILBOX_INVITE_TTL_HOURS, 48) * 60 * 60_000,
|
||||
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]",
|
||||
|
||||
@@ -26,6 +26,15 @@ export type PasswordResetTokenRow = {
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
export type PendingMailboxInviteRow = {
|
||||
token_hash: string;
|
||||
user_email: string;
|
||||
created_by: string;
|
||||
expires_at: number;
|
||||
used_at: number | null;
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
mkdirSync(dirname(config.dbPath), { recursive: true });
|
||||
|
||||
const db = new Database(config.dbPath, { create: true });
|
||||
@@ -55,6 +64,15 @@ db.exec(`
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pending_mailbox_invites (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
user_email TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
used_at INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_app_sessions_expires_at
|
||||
ON app_sessions(expires_at);
|
||||
|
||||
@@ -63,6 +81,12 @@ db.exec(`
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_expires_at
|
||||
ON password_reset_tokens(expires_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pending_mailbox_invites_email
|
||||
ON pending_mailbox_invites(user_email);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pending_mailbox_invites_expires_at
|
||||
ON pending_mailbox_invites(expires_at);
|
||||
`);
|
||||
|
||||
const getAdminStatement = db.query("SELECT * FROM app_admins WHERE email = ?1");
|
||||
@@ -115,6 +139,46 @@ const deleteExpiredResetTokensStatement = db.query(`
|
||||
WHERE expires_at <= ?1
|
||||
OR (used_at IS NOT NULL AND used_at <= ?2)
|
||||
`);
|
||||
const insertPendingMailboxInviteStatement = db.query(`
|
||||
INSERT INTO pending_mailbox_invites (token_hash, user_email, created_by, expires_at, used_at, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4, NULL, ?5)
|
||||
`);
|
||||
const getActivePendingMailboxInviteStatement = db.query(`
|
||||
SELECT * FROM pending_mailbox_invites
|
||||
WHERE token_hash = ?1
|
||||
AND used_at IS NULL
|
||||
AND expires_at > ?2
|
||||
`);
|
||||
const getActivePendingMailboxInviteForUserStatement = db.query(`
|
||||
SELECT * FROM pending_mailbox_invites
|
||||
WHERE user_email = ?1
|
||||
AND used_at IS NULL
|
||||
AND expires_at > ?2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
const deleteActivePendingMailboxInvitesForUserStatement = db.query(`
|
||||
DELETE FROM pending_mailbox_invites
|
||||
WHERE user_email = ?1
|
||||
AND used_at IS NULL
|
||||
`);
|
||||
const claimPendingMailboxInviteStatement = db.query(`
|
||||
UPDATE pending_mailbox_invites
|
||||
SET used_at = ?2
|
||||
WHERE token_hash = ?1
|
||||
AND used_at IS NULL
|
||||
AND expires_at > ?3
|
||||
`);
|
||||
const releasePendingMailboxInviteStatement = db.query(`
|
||||
UPDATE pending_mailbox_invites
|
||||
SET used_at = NULL
|
||||
WHERE token_hash = ?1
|
||||
`);
|
||||
const deleteExpiredPendingMailboxInvitesStatement = db.query(`
|
||||
DELETE FROM pending_mailbox_invites
|
||||
WHERE expires_at <= ?1
|
||||
OR (used_at IS NOT NULL AND used_at <= ?2)
|
||||
`);
|
||||
|
||||
export const getAdmin = (email: string): AdminRow | null =>
|
||||
(getAdminStatement.get(email) as AdminRow | null) ?? null;
|
||||
@@ -171,8 +235,44 @@ export const releasePasswordResetToken = (tokenHash: string): void => {
|
||||
releaseResetTokenStatement.run(tokenHash);
|
||||
};
|
||||
|
||||
export const createPendingMailboxInvite = (input: {
|
||||
tokenHash: string;
|
||||
userEmail: string;
|
||||
createdBy: string;
|
||||
expiresAt: number;
|
||||
}): void => {
|
||||
insertPendingMailboxInviteStatement.run(
|
||||
input.tokenHash,
|
||||
input.userEmail,
|
||||
input.createdBy,
|
||||
input.expiresAt,
|
||||
Date.now()
|
||||
);
|
||||
};
|
||||
|
||||
export const getActivePendingMailboxInvite = (tokenHash: string): PendingMailboxInviteRow | null =>
|
||||
(getActivePendingMailboxInviteStatement.get(tokenHash, Date.now()) as PendingMailboxInviteRow | null) ?? null;
|
||||
|
||||
export const getActivePendingMailboxInviteForUser = (userEmail: string): PendingMailboxInviteRow | null =>
|
||||
(getActivePendingMailboxInviteForUserStatement.get(userEmail, Date.now()) as PendingMailboxInviteRow | null) ?? null;
|
||||
|
||||
export const deleteActivePendingMailboxInvitesForUser = (userEmail: string): void => {
|
||||
deleteActivePendingMailboxInvitesForUserStatement.run(userEmail);
|
||||
};
|
||||
|
||||
export const claimPendingMailboxInvite = (tokenHash: string): boolean => {
|
||||
const usedAt = Date.now();
|
||||
const result = claimPendingMailboxInviteStatement.run(tokenHash, usedAt, usedAt);
|
||||
return result.changes > 0;
|
||||
};
|
||||
|
||||
export const releasePendingMailboxInvite = (tokenHash: string): void => {
|
||||
releasePendingMailboxInviteStatement.run(tokenHash);
|
||||
};
|
||||
|
||||
export const cleanupDatabase = (): void => {
|
||||
const now = Date.now();
|
||||
deleteExpiredSessionsStatement.run(now);
|
||||
deleteExpiredResetTokensStatement.run(now, now - 86_400_000);
|
||||
deleteExpiredPendingMailboxInvitesStatement.run(now, now - 86_400_000);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user