import { AuthEnvironmentScopes, AuthSessionId, ServerAuthSessionMethod } from "@t3tools/contracts";import * as SqlClient from "effect/unstable/sql/SqlClient";import * as SqlSchema from "effect/unstable/sql/SqlSchema";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Schema from "effect/Schema"; import { toPersistenceDecodeError, toPersistenceSqlError, type AuthSessionRepositoryError,} from "../Errors.ts";import { AuthSessionRecord, AuthSessionRepository, type AuthSessionRepositoryShape, CreateAuthSessionInput, GetAuthSessionByIdInput, ListActiveAuthSessionsInput, RevokeAuthSessionInput, RevokeOtherAuthSessionsInput, SetAuthSessionLastConnectedAtInput,} from "../Services/AuthSessions.ts"; const AuthSessionDbRow = Schema.Struct({ sessionId: AuthSessionId, subject: Schema.String, scopes: Schema.fromJsonString(AuthEnvironmentScopes), method: ServerAuthSessionMethod, clientLabel: Schema.NullOr(Schema.String), clientIpAddress: Schema.NullOr(Schema.String), clientUserAgent: Schema.NullOr(Schema.String), clientDeviceType: Schema.Literals(["desktop", "mobile", "tablet", "bot", "unknown"]), clientOs: Schema.NullOr(Schema.String), clientBrowser: Schema.NullOr(Schema.String), issuedAt: Schema.DateTimeUtcFromString, expiresAt: Schema.DateTimeUtcFromString, lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString),}); function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionRecord { return { sessionId: row.sessionId, subject: row.subject, scopes: row.scopes, method: row.method, client: { label: row.clientLabel, ipAddress: row.clientIpAddress, userAgent: row.clientUserAgent, deviceType: row.clientDeviceType, os: row.clientOs, browser: row.clientBrowser, }, issuedAt: row.issuedAt, expiresAt: row.expiresAt, lastConnectedAt: row.lastConnectedAt, revokedAt: row.revokedAt, };} function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { return (cause: unknown): AuthSessionRepositoryError => Schema.isSchemaError(cause) ? toPersistenceDecodeError(decodeOperation)(cause) : toPersistenceSqlError(sqlOperation)(cause);} const makeAuthSessionRepository = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; const createSessionRow = SqlSchema.void({ Request: CreateAuthSessionInput, execute: (input) => sql` INSERT INTO auth_sessions ( session_id, subject, scopes, method, client_label, client_ip_address, client_user_agent, client_device_type, client_os, client_browser, issued_at, expires_at, revoked_at ) VALUES ( ${input.sessionId}, ${input.subject}, ${JSON.stringify(input.scopes)}, ${input.method}, ${input.client.label}, ${input.client.ipAddress}, ${input.client.userAgent}, ${input.client.deviceType}, ${input.client.os}, ${input.client.browser}, ${input.issuedAt}, ${input.expiresAt}, NULL ) `, }); const getSessionRowById = SqlSchema.findOneOption({ Request: GetAuthSessionByIdInput, Result: AuthSessionDbRow, execute: ({ sessionId }) => sql` SELECT session_id AS "sessionId", subject AS "subject", scopes AS "scopes", method AS "method", client_label AS "clientLabel", client_ip_address AS "clientIpAddress", client_user_agent AS "clientUserAgent", client_device_type AS "clientDeviceType", client_os AS "clientOs", client_browser AS "clientBrowser", issued_at AS "issuedAt", expires_at AS "expiresAt", last_connected_at AS "lastConnectedAt", revoked_at AS "revokedAt" FROM auth_sessions WHERE session_id = ${sessionId} `, }); const listActiveSessionRows = SqlSchema.findAll({ Request: ListActiveAuthSessionsInput, Result: AuthSessionDbRow, execute: ({ now }) => sql` SELECT session_id AS "sessionId", subject AS "subject", scopes AS "scopes", method AS "method", client_label AS "clientLabel", client_ip_address AS "clientIpAddress", client_user_agent AS "clientUserAgent", client_device_type AS "clientDeviceType", client_os AS "clientOs", client_browser AS "clientBrowser", issued_at AS "issuedAt", expires_at AS "expiresAt", last_connected_at AS "lastConnectedAt", revoked_at AS "revokedAt" FROM auth_sessions WHERE revoked_at IS NULL AND expires_at > ${now} ORDER BY issued_at DESC, session_id DESC `, }); const setLastConnectedAtRow = SqlSchema.void({ Request: SetAuthSessionLastConnectedAtInput, execute: ({ sessionId, lastConnectedAt }) => sql` UPDATE auth_sessions SET last_connected_at = ${lastConnectedAt} WHERE session_id = ${sessionId} AND revoked_at IS NULL `, }); const revokeSessionRows = SqlSchema.findAll({ Request: RevokeAuthSessionInput, Result: Schema.Struct({ sessionId: AuthSessionId }), execute: ({ sessionId, revokedAt }) => sql` UPDATE auth_sessions SET revoked_at = ${revokedAt} WHERE session_id = ${sessionId} AND revoked_at IS NULL RETURNING session_id AS "sessionId" `, }); const revokeOtherSessionRows = SqlSchema.findAll({ Request: RevokeOtherAuthSessionsInput, Result: Schema.Struct({ sessionId: AuthSessionId }), execute: ({ currentSessionId, revokedAt }) => sql` UPDATE auth_sessions SET revoked_at = ${revokedAt} WHERE session_id <> ${currentSessionId} AND revoked_at IS NULL RETURNING session_id AS "sessionId" `, }); const create: AuthSessionRepositoryShape["create"] = (input) => createSessionRow(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.create:query", "AuthSessionRepository.create:encodeRequest", ), ), ); const getById: AuthSessionRepositoryShape["getById"] = (input) => getSessionRowById(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.getById:query", "AuthSessionRepository.getById:decodeRow", ), ), Effect.flatMap((rowOption) => Option.match(rowOption, { onNone: () => Effect.succeed(Option.none()), onSome: (row) => Effect.succeed(Option.some(toAuthSessionRecord(row))), }), ), ); const listActive: AuthSessionRepositoryShape["listActive"] = (input) => listActiveSessionRows(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.listActive:query", "AuthSessionRepository.listActive:decodeRows", ), ), Effect.flatMap((rows) => Effect.succeed(rows.map((row) => toAuthSessionRecord(row)))), ); const revoke: AuthSessionRepositoryShape["revoke"] = (input) => revokeSessionRows(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.revoke:query", "AuthSessionRepository.revoke:decodeRows", ), ), Effect.map((rows) => rows.length > 0), ); const revokeAllExcept: AuthSessionRepositoryShape["revokeAllExcept"] = (input) => revokeOtherSessionRows(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.revokeAllExcept:query", "AuthSessionRepository.revokeAllExcept:decodeRows", ), ), Effect.map((rows) => rows.map((row) => row.sessionId)), ); const setLastConnectedAt: AuthSessionRepositoryShape["setLastConnectedAt"] = (input) => setLastConnectedAtRow(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthSessionRepository.setLastConnectedAt:query", "AuthSessionRepository.setLastConnectedAt:encodeRequest", ), ), ); return { create, getById, listActive, revoke, revokeAllExcept, setLastConnectedAt, } satisfies AuthSessionRepositoryShape;}); export const AuthSessionRepositoryLive = Layer.effect( AuthSessionRepository, makeAuthSessionRepository,);
import { AuthEnvironmentScopes, AuthSessionId, ServerAuthSessionMethod } from "@t3tools/contracts";
import * as SqlClient from "effect/unstable/sql/SqlClient";
import * as SqlSchema from "effect/unstable/sql/SqlSchema";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import {
toPersistenceDecodeError,
toPersistenceSqlError,
type AuthSessionRepositoryError,
} from "../Errors.ts";
import {
AuthSessionRecord,
AuthSessionRepository,
type AuthSessionRepositoryShape,
CreateAuthSessionInput,
GetAuthSessionByIdInput,
ListActiveAuthSessionsInput,
RevokeAuthSessionInput,
RevokeOtherAuthSessionsInput,
SetAuthSessionLastConnectedAtInput,
} from "../Services/AuthSessions.ts";
const AuthSessionDbRow = Schema.Struct({
sessionId: AuthSessionId,
subject: Schema.String,
scopes: Schema.fromJsonString(AuthEnvironmentScopes),
method: ServerAuthSessionMethod,
clientLabel: Schema.NullOr(Schema.String),
clientIpAddress: Schema.NullOr(Schema.String),
clientUserAgent: Schema.NullOr(Schema.String),
clientDeviceType: Schema.Literals(["desktop", "mobile", "tablet", "bot", "unknown"]),
clientOs: Schema.NullOr(Schema.String),
clientBrowser: Schema.NullOr(Schema.String),
issuedAt: Schema.DateTimeUtcFromString,
expiresAt: Schema.DateTimeUtcFromString,
lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString),
revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString),
});
function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionRecord {
return {
sessionId: row.sessionId,
subject: row.subject,
scopes: row.scopes,
method: row.method,
client: {
label: row.clientLabel,
ipAddress: row.clientIpAddress,
userAgent: row.clientUserAgent,
deviceType: row.clientDeviceType,
os: row.clientOs,
browser: row.clientBrowser,
},
issuedAt: row.issuedAt,
expiresAt: row.expiresAt,
lastConnectedAt: row.lastConnectedAt,
revokedAt: row.revokedAt,
};
}
function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
return (cause: unknown): AuthSessionRepositoryError =>
Schema.isSchemaError(cause)
? toPersistenceDecodeError(decodeOperation)(cause)
: toPersistenceSqlError(sqlOperation)(cause);
}
const makeAuthSessionRepository = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
const createSessionRow = SqlSchema.void({
Request: CreateAuthSessionInput,
execute: (input) =>
sql`
INSERT INTO auth_sessions (
session_id,
subject,
scopes,
method,
client_label,
client_ip_address,
client_user_agent,
client_device_type,
client_os,
client_browser,
issued_at,
expires_at,
revoked_at
)
VALUES (
${input.sessionId},
${input.subject},
${JSON.stringify(input.scopes)},
${input.method},
${input.client.label},
${input.client.ipAddress},
${input.client.userAgent},
${input.client.deviceType},
${input.client.os},
${input.client.browser},
${input.issuedAt},
${input.expiresAt},
NULL
)
`,
});
const getSessionRowById = SqlSchema.findOneOption({
Request: GetAuthSessionByIdInput,
Result: AuthSessionDbRow,
execute: ({ sessionId }) =>
sql`
SELECT
session_id AS "sessionId",
subject AS "subject",
scopes AS "scopes",
method AS "method",
client_label AS "clientLabel",
client_ip_address AS "clientIpAddress",
client_user_agent AS "clientUserAgent",
client_device_type AS "clientDeviceType",
client_os AS "clientOs",
client_browser AS "clientBrowser",
issued_at AS "issuedAt",
expires_at AS "expiresAt",
last_connected_at AS "lastConnectedAt",
revoked_at AS "revokedAt"
FROM auth_sessions
WHERE session_id = ${sessionId}
`,
});
const listActiveSessionRows = SqlSchema.findAll({
Request: ListActiveAuthSessionsInput,
Result: AuthSessionDbRow,
execute: ({ now }) =>
sql`
SELECT
session_id AS "sessionId",
subject AS "subject",
scopes AS "scopes",
method AS "method",
client_label AS "clientLabel",
client_ip_address AS "clientIpAddress",
client_user_agent AS "clientUserAgent",
client_device_type AS "clientDeviceType",
client_os AS "clientOs",
client_browser AS "clientBrowser",
issued_at AS "issuedAt",
expires_at AS "expiresAt",
last_connected_at AS "lastConnectedAt",
revoked_at AS "revokedAt"
FROM auth_sessions
WHERE revoked_at IS NULL
AND expires_at > ${now}
ORDER BY issued_at DESC, session_id DESC
`,
});
const setLastConnectedAtRow = SqlSchema.void({
Request: SetAuthSessionLastConnectedAtInput,
execute: ({ sessionId, lastConnectedAt }) =>
sql`
UPDATE auth_sessions
SET last_connected_at = ${lastConnectedAt}
WHERE session_id = ${sessionId}
AND revoked_at IS NULL
`,
});
const revokeSessionRows = SqlSchema.findAll({
Request: RevokeAuthSessionInput,
Result: Schema.Struct({ sessionId: AuthSessionId }),
execute: ({ sessionId, revokedAt }) =>
sql`
UPDATE auth_sessions
SET revoked_at = ${revokedAt}
WHERE session_id = ${sessionId}
AND revoked_at IS NULL
RETURNING session_id AS "sessionId"
`,
});
const revokeOtherSessionRows = SqlSchema.findAll({
Request: RevokeOtherAuthSessionsInput,
Result: Schema.Struct({ sessionId: AuthSessionId }),
execute: ({ currentSessionId, revokedAt }) =>
sql`
UPDATE auth_sessions
SET revoked_at = ${revokedAt}
WHERE session_id <> ${currentSessionId}
AND revoked_at IS NULL
RETURNING session_id AS "sessionId"
`,
});
const create: AuthSessionRepositoryShape["create"] = (input) =>
createSessionRow(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.create:query",
"AuthSessionRepository.create:encodeRequest",
),
),
);
const getById: AuthSessionRepositoryShape["getById"] = (input) =>
getSessionRowById(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.getById:query",
"AuthSessionRepository.getById:decodeRow",
),
),
Effect.flatMap((rowOption) =>
Option.match(rowOption, {
onNone: () => Effect.succeed(Option.none()),
onSome: (row) => Effect.succeed(Option.some(toAuthSessionRecord(row))),
}),
),
);
const listActive: AuthSessionRepositoryShape["listActive"] = (input) =>
listActiveSessionRows(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.listActive:query",
"AuthSessionRepository.listActive:decodeRows",
),
),
Effect.flatMap((rows) => Effect.succeed(rows.map((row) => toAuthSessionRecord(row)))),
);
const revoke: AuthSessionRepositoryShape["revoke"] = (input) =>
revokeSessionRows(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.revoke:query",
"AuthSessionRepository.revoke:decodeRows",
),
),
Effect.map((rows) => rows.length > 0),
);
const revokeAllExcept: AuthSessionRepositoryShape["revokeAllExcept"] = (input) =>
revokeOtherSessionRows(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.revokeAllExcept:query",
"AuthSessionRepository.revokeAllExcept:decodeRows",
),
),
Effect.map((rows) => rows.map((row) => row.sessionId)),
);
const setLastConnectedAt: AuthSessionRepositoryShape["setLastConnectedAt"] = (input) =>
setLastConnectedAtRow(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"AuthSessionRepository.setLastConnectedAt:query",
"AuthSessionRepository.setLastConnectedAt:encodeRequest",
),
),
);
return {
create,
getById,
listActive,
revoke,
revokeAllExcept,
setLastConnectedAt,
} satisfies AuthSessionRepositoryShape;
});
export const AuthSessionRepositoryLive = Layer.effect(
AuthSessionRepository,
makeAuthSessionRepository,
);