mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
176 lines
4.2 KiB
TypeScript
176 lines
4.2 KiB
TypeScript
import { sha256 } from '@oslojs/crypto/sha2';
|
|
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
|
|
import { type Session, type User, UserSecurityAuditLogType } from '@prisma/client';
|
|
|
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
import { prisma } from '@documenso/prisma';
|
|
|
|
import { AUTH_SESSION_LIFETIME } from '../../config';
|
|
|
|
/**
|
|
* The user object to pass around the app.
|
|
*
|
|
* Do not put anything sensitive in here since it will be public.
|
|
*/
|
|
export type SessionUser = Pick<
|
|
User,
|
|
| 'id'
|
|
| 'name'
|
|
| 'email'
|
|
| 'emailVerified'
|
|
| 'avatarImageId'
|
|
| 'twoFactorEnabled'
|
|
| 'roles'
|
|
| 'signature'
|
|
>;
|
|
|
|
export type SessionValidationResult =
|
|
| {
|
|
session: Session;
|
|
user: SessionUser;
|
|
isAuthenticated: true;
|
|
}
|
|
| { session: null; user: null; isAuthenticated: false };
|
|
|
|
export const generateSessionToken = (): string => {
|
|
const bytes = new Uint8Array(20);
|
|
|
|
crypto.getRandomValues(bytes);
|
|
|
|
const token = encodeBase32LowerCaseNoPadding(bytes);
|
|
|
|
return token;
|
|
};
|
|
|
|
export const createSession = async (
|
|
token: string,
|
|
userId: number,
|
|
metadata: RequestMetadata,
|
|
): Promise<Session> => {
|
|
const hashedSessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
|
|
|
const session: Session = {
|
|
id: hashedSessionId,
|
|
sessionToken: hashedSessionId,
|
|
userId,
|
|
updatedAt: new Date(),
|
|
createdAt: new Date(),
|
|
expiresAt: new Date(Date.now() + AUTH_SESSION_LIFETIME),
|
|
ipAddress: metadata.ipAddress ?? null,
|
|
userAgent: metadata.userAgent ?? null,
|
|
};
|
|
|
|
await prisma.session.create({
|
|
data: session,
|
|
});
|
|
|
|
await prisma.userSecurityAuditLog.create({
|
|
data: {
|
|
userId,
|
|
ipAddress: metadata.ipAddress,
|
|
userAgent: metadata.userAgent,
|
|
type: UserSecurityAuditLogType.SIGN_IN,
|
|
},
|
|
});
|
|
|
|
return session;
|
|
};
|
|
|
|
export const validateSessionToken = async (token: string): Promise<SessionValidationResult> => {
|
|
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
|
|
|
const result = await prisma.session.findUnique({
|
|
where: {
|
|
id: sessionId,
|
|
},
|
|
include: {
|
|
user: {
|
|
/**
|
|
* Do not expose anything sensitive here.
|
|
*/
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
email: true,
|
|
emailVerified: true,
|
|
avatarImageId: true,
|
|
twoFactorEnabled: true,
|
|
roles: true,
|
|
signature: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!result?.user) {
|
|
return { session: null, user: null, isAuthenticated: false };
|
|
}
|
|
|
|
const { user, ...session } = result;
|
|
|
|
if (Date.now() >= session.expiresAt.getTime()) {
|
|
await prisma.session.delete({ where: { id: sessionId } });
|
|
return { session: null, user: null, isAuthenticated: false };
|
|
}
|
|
|
|
if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
|
|
session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
|
|
|
|
await prisma.session.update({
|
|
where: {
|
|
id: session.id,
|
|
},
|
|
data: {
|
|
expiresAt: session.expiresAt,
|
|
},
|
|
});
|
|
}
|
|
|
|
return { session, user, isAuthenticated: true };
|
|
};
|
|
|
|
type InvalidateSessionsOptions = {
|
|
userId: number;
|
|
sessionIds: string[];
|
|
metadata: RequestMetadata;
|
|
isRevoke?: boolean;
|
|
};
|
|
|
|
export const invalidateSessions = async ({
|
|
userId,
|
|
sessionIds,
|
|
metadata,
|
|
isRevoke,
|
|
}: InvalidateSessionsOptions): Promise<void> => {
|
|
if (sessionIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
const { count } = await tx.session.deleteMany({
|
|
where: {
|
|
userId,
|
|
id: { in: sessionIds },
|
|
},
|
|
});
|
|
|
|
if (count !== sessionIds.length) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'One or more sessions are not valid.',
|
|
});
|
|
}
|
|
|
|
await tx.userSecurityAuditLog.createMany({
|
|
data: sessionIds.map(() => ({
|
|
userId,
|
|
ipAddress: metadata.ipAddress,
|
|
userAgent: metadata.userAgent,
|
|
type: isRevoke
|
|
? UserSecurityAuditLogType.SESSION_REVOKED
|
|
: UserSecurityAuditLogType.SIGN_OUT,
|
|
})),
|
|
});
|
|
});
|
|
};
|