index.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import {
  2. encodeBase32LowerCaseNoPadding,
  3. encodeHexLowerCase,
  4. } from "@oslojs/encoding";
  5. import { sha256 } from "@oslojs/crypto/sha2";
  6. import { Session, sessions, User, users } from "@server/db/schema";
  7. import db from "@server/db";
  8. import { eq } from "drizzle-orm";
  9. import config from "@server/lib/config";
  10. import type { RandomReader } from "@oslojs/crypto/random";
  11. import { generateRandomString } from "@oslojs/crypto/random";
  12. export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name;
  13. export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
  14. export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
  15. export const COOKIE_DOMAIN = "." + config.getBaseDomain();
  16. export function generateSessionToken(): string {
  17. const bytes = new Uint8Array(20);
  18. crypto.getRandomValues(bytes);
  19. const token = encodeBase32LowerCaseNoPadding(bytes);
  20. return token;
  21. }
  22. export async function createSession(
  23. token: string,
  24. userId: string,
  25. ): Promise<Session> {
  26. const sessionId = encodeHexLowerCase(
  27. sha256(new TextEncoder().encode(token)),
  28. );
  29. const session: Session = {
  30. sessionId: sessionId,
  31. userId,
  32. expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
  33. };
  34. await db.insert(sessions).values(session);
  35. return session;
  36. }
  37. export async function validateSessionToken(
  38. token: string,
  39. ): Promise<SessionValidationResult> {
  40. const sessionId = encodeHexLowerCase(
  41. sha256(new TextEncoder().encode(token)),
  42. );
  43. const result = await db
  44. .select({ user: users, session: sessions })
  45. .from(sessions)
  46. .innerJoin(users, eq(sessions.userId, users.userId))
  47. .where(eq(sessions.sessionId, sessionId));
  48. if (result.length < 1) {
  49. return { session: null, user: null };
  50. }
  51. const { user, session } = result[0];
  52. if (Date.now() >= session.expiresAt) {
  53. await db
  54. .delete(sessions)
  55. .where(eq(sessions.sessionId, session.sessionId));
  56. return { session: null, user: null };
  57. }
  58. if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
  59. session.expiresAt = new Date(
  60. Date.now() + SESSION_COOKIE_EXPIRES,
  61. ).getTime();
  62. await db
  63. .update(sessions)
  64. .set({
  65. expiresAt: session.expiresAt,
  66. })
  67. .where(eq(sessions.sessionId, session.sessionId));
  68. }
  69. return { session, user };
  70. }
  71. export async function invalidateSession(sessionId: string): Promise<void> {
  72. await db.delete(sessions).where(eq(sessions.sessionId, sessionId));
  73. }
  74. export async function invalidateAllSessions(userId: string): Promise<void> {
  75. await db.delete(sessions).where(eq(sessions.userId, userId));
  76. }
  77. export function serializeSessionCookie(token: string): string {
  78. if (SECURE_COOKIES) {
  79. return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
  80. } else {
  81. return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
  82. }
  83. }
  84. export function createBlankSessionTokenCookie(): string {
  85. if (SECURE_COOKIES) {
  86. return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
  87. } else {
  88. return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
  89. }
  90. }
  91. const random: RandomReader = {
  92. read(bytes: Uint8Array): void {
  93. crypto.getRandomValues(bytes);
  94. },
  95. };
  96. export function generateId(length: number): string {
  97. const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
  98. return generateRandomString(random, alphabet, length);
  99. }
  100. export function generateIdFromEntropySize(size: number): string {
  101. const buffer = crypto.getRandomValues(new Uint8Array(size));
  102. return encodeBase32LowerCaseNoPadding(buffer);
  103. }
  104. export type SessionValidationResult =
  105. | { session: Session; user: User }
  106. | { session: null; user: null };