feat: improve account security

Support TOTP 2fa, allow for changing password, add session management
and account removal.
This commit is contained in:
Zoe
2026-10-08 12:43:22 -05:00
parent ff33e9c573
commit 4bba529ba8
27 changed files with 4874 additions and 468 deletions
@@ -0,0 +1,40 @@
import { and, eq } from 'drizzle-orm';
import { sessions } from '~~/drizzle/schema';
import { auth } from '~~/lib/auth';
import { db } from '~~/server/lib/db';
export default defineEventHandler(async (event) => {
const sessionData = await auth.api.getSession({ headers: event.headers });
if (sessionData === null) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized',
});
}
const id = getRouterParam(event, 'id')!;
// signing out the current device goes through the normal sign out, which
// also clears its cookies
if (id === sessionData.session.id) {
throw createError({
statusCode: 400,
statusMessage: 'Cannot revoke the current session',
});
}
const deleted = await db.delete(sessions)
.where(and(eq(sessions.id, id), eq(sessions.userId, sessionData.user.id)))
.returning({ id: sessions.id });
if (deleted.length === 0) {
throw createError({
statusCode: 404,
statusMessage: 'Not Found',
message: 'Session not found',
});
}
return { ok: true };
});
+38
View File
@@ -0,0 +1,38 @@
import { auth } from '~~/lib/auth';
import { db } from '~~/server/lib/db';
/**
* Lists the signed in devices for the current user. Session tokens are left
* out on purpose: the client only ever needs the id to sign a device out.
*/
export default defineEventHandler(async (event) => {
const sessionData = await auth.api.getSession({ headers: event.headers });
if (sessionData === null) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized',
});
}
const result = await db.query.sessions.findMany({
where: {
userId: sessionData.user.id,
expiresAt: {
gt: new Date(),
},
},
orderBy: {
updatedAt: 'desc',
},
});
return result.map(session => ({
id: session.id,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
ipAddress: session.ipAddress,
userAgent: session.userAgent,
current: session.id === sessionData.session.id,
}));
});