refresh.js (3110B)
1 // src/lib/auth/refresh.js - refresh tokens the built-in provider issued 2 3 import { hashToken } from '../ids.js'; 4 import { paginateOffset } from '../page.js'; 5 6 // user_agent and ip are shown to tell sessions apart. Never validated. 7 const COLUMNS = 'id, user_id, issued_at, expires_at, last_used_at, revoked_at, user_agent, ip'; 8 9 export function createRefreshTokens({ db }) { 10 return { 11 async issue({ id, userId, token, expiresAt, userAgent = null, ip = null, now = Date.now() }) { 12 await db.run( 13 `INSERT INTO refresh_tokens (id, user_id, token_hash, issued_at, expires_at, user_agent, ip) 14 VALUES ({id}, {user}, {hash}, {now}, {expires}, {agent}, {ip})`, 15 { id, user: userId, hash: hashToken(token), now, expires: expiresAt, agent: userAgent, ip } 16 ); 17 return { id, user_id: userId, issued_at: now, expires_at: expiresAt, user_agent: userAgent, ip }; 18 }, 19 20 async claim(token, { now = Date.now() } = {}) { 21 if (typeof token !== 'string' || token.length === 0) return null; 22 const row = await db.get( 23 `SELECT ${COLUMNS} FROM refresh_tokens WHERE token_hash = {hash}`, 24 { hash: hashToken(token) } 25 ); 26 if (!row || row.revoked_at !== null || row.expires_at <= now) return null; 27 return row; 28 }, 29 30 // Records the use. Leaving expiresAt out keeps the original deadline, 31 // which is what a non-sliding session wants. 32 async touch({ id, expiresAt = null, userAgent = null, ip = null, now = Date.now() }) { 33 const moved = expiresAt === null ? '' : ', expires_at = {expires}'; 34 const res = await db.run( 35 `UPDATE refresh_tokens SET last_used_at = {now}, user_agent = {agent}, ip = {ip}${moved} 36 WHERE id = {id} AND revoked_at IS NULL`, 37 { id, now, agent: userAgent, ip, expires: expiresAt } 38 ); 39 return res.changes > 0; 40 }, 41 42 async revoke(id, { now = Date.now() } = {}) { 43 const res = await db.run( 44 'UPDATE refresh_tokens SET revoked_at = {now} WHERE id = {id} AND revoked_at IS NULL', 45 { id, now } 46 ); 47 return res.changes > 0; 48 }, 49 50 async revokeAllFor(userId, { now = Date.now() } = {}) { 51 const res = await db.run( 52 'UPDATE refresh_tokens SET revoked_at = {now} WHERE user_id = {user} AND revoked_at IS NULL', 53 { user: userId, now } 54 ); 55 return res.changes; 56 }, 57 58 async get(id) { 59 return db.get(`SELECT ${COLUMNS} FROM refresh_tokens WHERE id = {id}`, { id }); 60 }, 61 62 // Live sessions only. 63 async page({ userId = null, offset, limit, now = Date.now() }) { 64 const scoped = userId ? 'user_id = {user} AND ' : ''; 65 return paginateOffset(db, { 66 select: COLUMNS, 67 from: 'refresh_tokens', 68 where: `${scoped}revoked_at IS NULL AND expires_at > {now}`, 69 order: 'issued_at DESC', 70 params: userId ? { user: userId, now } : { now }, 71 offset, 72 limit, 73 }); 74 }, 75 76 async sweep(cutoff) { 77 const res = await db.run('DELETE FROM refresh_tokens WHERE expires_at < {cutoff}', { cutoff }); 78 return res.changes; 79 }, 80 }; 81 }