import "server-only";
import type { ExternalUserId } from "@/lib/auth/types";
export type UserServiceProfile = {
id: ExternalUserId;
login: string;
fullName: string;
position: string;
email: string;
lastName: string;
firstName: string;
middleName: string;
roles: string[];
createdAt: string;
updatedAt: string;
};
export type UserServiceAuthErrorReason =
| "invalid_credentials"
| "unavailable";
export class UserServiceAuthError extends Error {
constructor(readonly reason: UserServiceAuthErrorReason) {
super(reason === "invalid_credentials" ? "Invalid credentials" : "User service unavailable");
this.name = "UserServiceAuthError";
}
}
export async function authenticateWithUserService(
identifier: string,
password: string,
signal?: AbortSignal,
): Promise<UserServiceProfile> {
try {
const response = await fetch(`${userServiceUrl()}/api/user/auth`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ identifier, password }),
signal: AbortSignal.any([
signal ?? new AbortController().signal,
AbortSignal.timeout(5_000),
]),
cache: "no-store",
});
if (response.status === 401) {
throw new UserServiceAuthError("invalid_credentials");
}
if (!response.ok) {
throw new UserServiceAuthError("unavailable");
}
return parseProfile(await response.json());
} catch (error) {
if (error instanceof UserServiceAuthError) {
throw error;
}
throw new UserServiceAuthError("unavailable");
}
}
function userServiceUrl(): string {
const value = process.env.USER_SERVICE_URL?.trim();
if (!value) {
throw new Error("Missing required environment variable: USER_SERVICE_URL");
}
return value.replace(/\/+$/, "");
}
function parseProfile(value: unknown): UserServiceProfile {
if (!isRecord(value) || !isPositiveSafeInteger(value.id) || value.is_active !== true) {
throw new Error("Invalid user-service profile");
}
const fields = [
["login", "login"],
["full_name", "fullName"],
["position", "position"],
["email", "email"],
["last_name", "lastName"],
["first_name", "firstName"],
["middle_name", "middleName"],
["created_at", "createdAt"],
["updated_at", "updatedAt"],
] as const;
if (!Array.isArray(value.roles) || !value.roles.every(isString)) {
throw new Error("Invalid user-service profile");
}
const profile = { id: String(value.id), roles: value.roles } as Partial<UserServiceProfile>;
for (const [source, target] of fields) {
if (!isString(value[source])) {
throw new Error("Invalid user-service profile");
}
profile[target] = value[source];
}
return profile as UserServiceProfile;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isPositiveSafeInteger(value: unknown): value is number {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
}
function isString(value: unknown): value is string {
return typeof value === "string";
}