v0.1.3 ff61cc9

session.server.ts RawHistoryFolder

1import db from '../db/db.server';
2import { sessions, users } from '../db/schema';
3import { eq } from 'drizzle-orm';
4import type { Session, User } from '../types/chat';
5import { createLogger } from '../utils/logger.server';
6
7const log = createLogger('session-server');
8
9// Helper: Compute SHA-256 hash of a message and encode it as a hex string.
10export async function sha256(message: string): Promise<string> {
11  const encoder = new TextEncoder();
12  const data = encoder.encode(message);
13  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
14  const hashArray = Array.from(new Uint8Array(hashBuffer));
15  const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
16  return hashHex;
17}
18
19// Helper: Custom Base32 encoder using the standard alphabet.
20// We use this instead of @oslojs/encoding given Cloudflare Pages compatibility.
21const base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
22function base32Encode(data: Uint8Array): string {
23  let bits = 0;
24  let value = 0;
25  let output = "";
26  for (let i = 0; i < data.length; i++) {
27    value = (value << 8) | data[i];
28    bits += 8;
29    while (bits >= 5) {
30      const index = (value >>> (bits - 5)) & 0x1f;
31      output += base32Alphabet[index];
32      bits -= 5;
33    }
34  }
35  if (bits > 0) {
36    const index = (value << (5 - bits)) & 0x1f;
37    output += base32Alphabet[index];
38  }
39  return output;
40}
41
42// Generate a secure session token by generating 20 random bytes and Base32 encoding them.
43export function generateSessionToken(): string {
44  const randomBytesArray = new Uint8Array(20);
45  crypto.getRandomValues(randomBytesArray);
46  const token = base32Encode(randomBytesArray);
47  log.debug("Generated session token", { token });
48  return token;
49}
50
51// Create a session record once a valid token is available.
52// The session ID is the SHA-256 hash of the token.
53export async function createSession(token: string, userId: string): Promise<Session> {
54  log.debug("Creating session", { userId });
55  const sessionId = await sha256(token);
56  const createdAt = Date.now();
57  const expiresAt = createdAt + 24 * 60 * 60 * 1000; // 24 hours expiry
58  await db.insert(sessions).values({
59    id: sessionId,
60    userId,
61    createdAt,
62    expiresAt,
63  });
64  log.debug("Session created", { sessionId });
65  return { id: sessionId, userId, createdAt, expiresAt };
66}
67
68/**
69 * Type distributed through our API.
70 * Returns an object with both session and user if valid, or nulls if not.
71 * Note: This returns the full User type since it's used server-side in hooks.
72 */
73export type SessionValidationResult =
74  | { session: Session; user: User }
75  | { session: null; user: null };
76
77// Validate a session token by converting it to its SHA‑256 hash, checking expiration, and fetching the user.
78export async function validateSessionToken(token: string): Promise<SessionValidationResult> {
79  log.debug("Validating session token");
80  const sessionId = await sha256(token);
81  const session = await db.query.sessions.findFirst({
82    where: eq(sessions.id, sessionId)
83  });
84  if (!session) {
85    log.debug("Session not found", { sessionId });
86    return { session: null, user: null };
87  }
88  const now = Date.now();
89  if (session.expiresAt < now) {
90    log.debug("Session expired", { sessionId });
91    return { session: null, user: null };
92  }
93  const user = await db.query.users.findFirst({
94    where: eq(users.id, session.userId)
95  });
96  if (!user) {
97    log.debug("User not found for session", { sessionId });
98    return { session: null, user: null };
99  }
100  log.debug("Session validated", { sessionId, userId: user.id });
101  return { session, user };
102}
103
104// Invalidate a specific session by deleting it from the database.
105export async function invalidateSession(sessionId: string): Promise<void> {
106  log.debug("Invalidating session", { sessionId });
107  await db.delete(sessions).where(eq(sessions.id, sessionId));
108  log.debug("Session invalidated", { sessionId });
109}
110
111// Invalidate all sessions for a given user.
112export async function invalidateAllSessions(userId: string): Promise<void> {
113  log.debug("Invalidating all sessions for user", { userId });
114  await db.delete(sessions).where(eq(sessions.userId, userId));
115  log.debug("All sessions invalidated", { userId });
116} 

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close