1
This commit is contained in:
214
server-node/src/repositories/userSessionRepository.ts
Normal file
214
server-node/src/repositories/userSessionRepository.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import type { QueryResultRow } from 'pg';
|
||||
|
||||
import type { AppDatabase } from '../db.js';
|
||||
|
||||
export type UserSessionRecord = {
|
||||
id: string;
|
||||
userId: string;
|
||||
refreshTokenHash: string;
|
||||
clientType: string;
|
||||
userAgent: string | null;
|
||||
ip: string | null;
|
||||
expiresAt: string;
|
||||
revokedAt: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastSeenAt: string;
|
||||
};
|
||||
|
||||
type UserSessionRow = QueryResultRow & {
|
||||
id: string;
|
||||
user_id: string;
|
||||
refresh_token_hash: string;
|
||||
client_type: string;
|
||||
user_agent: string | null;
|
||||
ip: string | null;
|
||||
expires_at: string;
|
||||
revoked_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_seen_at: string;
|
||||
};
|
||||
|
||||
function toUserSessionRecord(
|
||||
row: UserSessionRow | undefined,
|
||||
): UserSessionRecord | null {
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
refreshTokenHash: row.refresh_token_hash,
|
||||
clientType: row.client_type,
|
||||
userAgent: row.user_agent,
|
||||
ip: row.ip,
|
||||
expiresAt: row.expires_at,
|
||||
revokedAt: row.revoked_at,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
lastSeenAt: row.last_seen_at,
|
||||
};
|
||||
}
|
||||
|
||||
export type CreateUserSessionInput = {
|
||||
userId: string;
|
||||
refreshTokenHash: string;
|
||||
clientType: string;
|
||||
userAgent: string | null;
|
||||
ip: string | null;
|
||||
expiresAt: string;
|
||||
};
|
||||
|
||||
export class UserSessionRepository {
|
||||
constructor(private readonly db: AppDatabase) {}
|
||||
|
||||
async create(input: CreateUserSessionInput) {
|
||||
const now = new Date().toISOString();
|
||||
const sessionId = `usess_${crypto.randomBytes(16).toString('hex')}`;
|
||||
|
||||
const result = await this.db.query<UserSessionRow>(
|
||||
`INSERT INTO user_sessions (
|
||||
id,
|
||||
user_id,
|
||||
refresh_token_hash,
|
||||
client_type,
|
||||
user_agent,
|
||||
ip,
|
||||
expires_at,
|
||||
revoked_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
last_seen_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8, $9, $10)
|
||||
RETURNING id, user_id, refresh_token_hash, client_type, user_agent, ip, expires_at, revoked_at, created_at, updated_at, last_seen_at`,
|
||||
[
|
||||
sessionId,
|
||||
input.userId,
|
||||
input.refreshTokenHash,
|
||||
input.clientType,
|
||||
input.userAgent,
|
||||
input.ip,
|
||||
input.expiresAt,
|
||||
now,
|
||||
now,
|
||||
now,
|
||||
],
|
||||
);
|
||||
|
||||
return toUserSessionRecord(result.rows[0]);
|
||||
}
|
||||
|
||||
async findActiveByRefreshTokenHash(refreshTokenHash: string) {
|
||||
const result = await this.db.query<UserSessionRow>(
|
||||
`SELECT id, user_id, refresh_token_hash, client_type, user_agent, ip, expires_at, revoked_at, created_at, updated_at, last_seen_at
|
||||
FROM user_sessions
|
||||
WHERE refresh_token_hash = $1
|
||||
LIMIT 1`,
|
||||
[refreshTokenHash],
|
||||
);
|
||||
|
||||
return toUserSessionRecord(result.rows[0]);
|
||||
}
|
||||
|
||||
async rotate(
|
||||
sessionId: string,
|
||||
input: {
|
||||
refreshTokenHash: string;
|
||||
expiresAt: string;
|
||||
lastSeenAt: string;
|
||||
},
|
||||
) {
|
||||
const result = await this.db.query<UserSessionRow>(
|
||||
`UPDATE user_sessions
|
||||
SET refresh_token_hash = $1,
|
||||
expires_at = $2,
|
||||
last_seen_at = $3,
|
||||
updated_at = $4
|
||||
WHERE id = $5
|
||||
RETURNING id, user_id, refresh_token_hash, client_type, user_agent, ip, expires_at, revoked_at, created_at, updated_at, last_seen_at`,
|
||||
[
|
||||
input.refreshTokenHash,
|
||||
input.expiresAt,
|
||||
input.lastSeenAt,
|
||||
new Date().toISOString(),
|
||||
sessionId,
|
||||
],
|
||||
);
|
||||
|
||||
return toUserSessionRecord(result.rows[0]);
|
||||
}
|
||||
|
||||
async revoke(sessionId: string) {
|
||||
const now = new Date().toISOString();
|
||||
const result = await this.db.query<UserSessionRow>(
|
||||
`UPDATE user_sessions
|
||||
SET revoked_at = $1,
|
||||
updated_at = $2
|
||||
WHERE id = $3
|
||||
RETURNING id, user_id, refresh_token_hash, client_type, user_agent, ip, expires_at, revoked_at, created_at, updated_at, last_seen_at`,
|
||||
[now, now, sessionId],
|
||||
);
|
||||
|
||||
return toUserSessionRecord(result.rows[0]);
|
||||
}
|
||||
|
||||
async findById(sessionId: string) {
|
||||
const result = await this.db.query<UserSessionRow>(
|
||||
`SELECT id, user_id, refresh_token_hash, client_type, user_agent, ip, expires_at, revoked_at, created_at, updated_at, last_seen_at
|
||||
FROM user_sessions
|
||||
WHERE id = $1
|
||||
LIMIT 1`,
|
||||
[sessionId],
|
||||
);
|
||||
|
||||
return toUserSessionRecord(result.rows[0]);
|
||||
}
|
||||
|
||||
async listActiveByUserId(userId: string) {
|
||||
const result = await this.db.query<UserSessionRow>(
|
||||
`SELECT id, user_id, refresh_token_hash, client_type, user_agent, ip, expires_at, revoked_at, created_at, updated_at, last_seen_at
|
||||
FROM user_sessions
|
||||
WHERE user_id = $1
|
||||
AND revoked_at IS NULL
|
||||
ORDER BY last_seen_at DESC, created_at DESC`,
|
||||
[userId],
|
||||
);
|
||||
|
||||
return result.rows
|
||||
.map((row) => toUserSessionRecord(row))
|
||||
.filter((row): row is UserSessionRecord => Boolean(row));
|
||||
}
|
||||
|
||||
async revokeAllByUserId(userId: string) {
|
||||
const now = new Date().toISOString();
|
||||
await this.db.query(
|
||||
`UPDATE user_sessions
|
||||
SET revoked_at = $1,
|
||||
updated_at = $2
|
||||
WHERE user_id = $3
|
||||
AND revoked_at IS NULL`,
|
||||
[now, now, userId],
|
||||
);
|
||||
}
|
||||
|
||||
async revokeByUserIdAndSessionId(userId: string, sessionId: string) {
|
||||
const now = new Date().toISOString();
|
||||
const result = await this.db.query<UserSessionRow>(
|
||||
`UPDATE user_sessions
|
||||
SET revoked_at = $1,
|
||||
updated_at = $2
|
||||
WHERE user_id = $3
|
||||
AND id = $4
|
||||
AND revoked_at IS NULL
|
||||
RETURNING id, user_id, refresh_token_hash, client_type, user_agent, ip, expires_at, revoked_at, created_at, updated_at, last_seen_at`,
|
||||
[now, now, userId, sessionId],
|
||||
);
|
||||
|
||||
return toUserSessionRecord(result.rows[0]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user