150 lines
3.5 KiB
TypeScript
150 lines
3.5 KiB
TypeScript
import "server-only";
|
|
|
|
import { and, eq } from "drizzle-orm";
|
|
import { headers } from "next/headers";
|
|
import { db } from "@/lib/db";
|
|
import { user } from "@/lib/db/schema";
|
|
|
|
type ManagedUserType = "guest" | "regular" | "admin" | "agent_admin";
|
|
|
|
type ManagedSessionUser = {
|
|
id: string;
|
|
email: string;
|
|
name: string | null;
|
|
role: ManagedUserType;
|
|
isActive: boolean;
|
|
activeSessionKey: string | null;
|
|
};
|
|
|
|
function normalizeClientIp(value: string | null | undefined) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
const firstHop = value.split(",")[0]?.trim();
|
|
return firstHop || null;
|
|
}
|
|
|
|
function normalizeUserAgent(value: string | null | undefined) {
|
|
const normalized = String(value || "").trim();
|
|
if (!normalized) {
|
|
return null;
|
|
}
|
|
|
|
return normalized.slice(0, 512);
|
|
}
|
|
|
|
async function readRequestSessionMetadata() {
|
|
const requestHeaders = await headers();
|
|
const forwardedFor = requestHeaders.get("x-forwarded-for");
|
|
const realIp = requestHeaders.get("x-real-ip");
|
|
|
|
return {
|
|
ip: normalizeClientIp(forwardedFor) || normalizeClientIp(realIp),
|
|
userAgent: normalizeUserAgent(requestHeaders.get("user-agent")),
|
|
};
|
|
}
|
|
|
|
export function isManagedSessionUserType(
|
|
type: string | null | undefined
|
|
): type is ManagedUserType {
|
|
return (
|
|
type === "guest" ||
|
|
type === "regular" ||
|
|
type === "admin" ||
|
|
type === "agent_admin"
|
|
);
|
|
}
|
|
|
|
export function requiresSingleActiveSession(type: string | null | undefined) {
|
|
return type === "regular" || type === "admin" || type === "agent_admin";
|
|
}
|
|
|
|
export async function establishSingleActiveSession(params: { userId: string }) {
|
|
const { userId } = params;
|
|
const sessionKey = crypto.randomUUID();
|
|
const metadata = await readRequestSessionMetadata();
|
|
const now = new Date();
|
|
|
|
await db
|
|
.update(user)
|
|
.set({
|
|
activeSessionKey: sessionKey,
|
|
activeSessionCreatedAt: now,
|
|
activeSessionIp: metadata.ip,
|
|
activeSessionUserAgent: metadata.userAgent,
|
|
activeSessionLastSeenAt: now,
|
|
updatedAt: now,
|
|
})
|
|
.where(eq(user.id, userId));
|
|
|
|
return { sessionKey };
|
|
}
|
|
|
|
export async function getValidatedSessionUser(params: {
|
|
userId: string;
|
|
sessionKey: string;
|
|
}) {
|
|
const { userId, sessionKey } = params;
|
|
const rows = await db
|
|
.select({
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
role: user.role,
|
|
isActive: user.isActive,
|
|
activeSessionKey: user.activeSessionKey,
|
|
})
|
|
.from(user)
|
|
.where(eq(user.id, userId))
|
|
.limit(1);
|
|
|
|
const currentUser = rows[0] as ManagedSessionUser | undefined;
|
|
if (!currentUser) {
|
|
return null;
|
|
}
|
|
|
|
if (!currentUser.isActive) {
|
|
return null;
|
|
}
|
|
|
|
if (!requiresSingleActiveSession(currentUser.role)) {
|
|
return currentUser;
|
|
}
|
|
|
|
if (
|
|
!currentUser.activeSessionKey ||
|
|
currentUser.activeSessionKey !== sessionKey
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return currentUser;
|
|
}
|
|
|
|
export async function revokeSingleActiveSession(params: {
|
|
userId: string;
|
|
sessionKey?: string | null;
|
|
}) {
|
|
const { userId, sessionKey } = params;
|
|
const now = new Date();
|
|
const whereClause = sessionKey
|
|
? and(eq(user.id, userId), eq(user.activeSessionKey, sessionKey))
|
|
: eq(user.id, userId);
|
|
|
|
const revokedUsers = await db
|
|
.update(user)
|
|
.set({
|
|
activeSessionKey: null,
|
|
activeSessionCreatedAt: null,
|
|
activeSessionIp: null,
|
|
activeSessionUserAgent: null,
|
|
activeSessionLastSeenAt: null,
|
|
updatedAt: now,
|
|
})
|
|
.where(whereClause)
|
|
.returning({ id: user.id });
|
|
|
|
return revokedUsers.length > 0;
|
|
}
|