@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
636 lines (614 loc) • 20.6 kB
text/typescript
/**
* Auth Adapter
*
* Unifies AuthHttpClient and AuthService behind a common interface.
* Eliminates mode checks by delegating to the appropriate implementation.
*/
import { AuthService, ApiKeyAuthRequest } from "../../auth-service";
import { AuthHttpClient } from "../../http-clients/auth-http-client";
import { AdminUser, ProjectUser } from "../../types";
import { handleAdapterError, createAdapterInitError } from "./error-handler";
type Mode = "client" | "server";
export class AuthAdapter {
private mode: Mode;
private httpClient: AuthHttpClient | undefined;
private service: AuthService | undefined;
constructor(mode: Mode, httpClient?: AuthHttpClient, service?: AuthService) {
this.mode = mode;
this.httpClient = httpClient;
this.service = service;
}
async createSession(apiKey: string): Promise<{
session_token: string;
expires_at: string;
user_type: "admin" | "project";
scopes: string[];
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "createSession");
}
try {
const response = await this.httpClient.createSession(apiKey);
const data = response?.data;
if (!data) {
throw createAdapterInitError("Response data", this.mode, "No response data from create session");
}
return {
session_token: data.session_token,
expires_at: data.expires_at,
user_type: data.user_type,
scopes: data.scopes,
};
} catch (error) {
throw handleAdapterError(error, this.mode, "createSession", { apiKey });
}
} else {
if (!this.service) {
throw createAdapterInitError("Auth service", this.mode, "createSession");
}
try {
const result = await this.service.createSession({
user_id: apiKey, // API key is used as user_id for API key sessions
user_type: "admin",
scopes: [],
});
return {
session_token: result.token,
expires_at: result.expires_at,
user_type: result.user_type,
scopes: result.scopes,
};
} catch (error) {
throw handleAdapterError(error, this.mode, "createSession", { apiKey });
}
}
}
async login(
username: string,
password: string,
remember_me?: boolean
): Promise<{
session_token: string;
expires_at: string;
user: AdminUser | ProjectUser;
scopes: string[];
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "adminLogin");
}
const loginRequest: {
username: string;
password: string;
remember_me?: boolean;
} = {
username,
password,
};
if (remember_me !== undefined) loginRequest.remember_me = remember_me;
const response = await this.httpClient.adminLogin(loginRequest);
const data = response?.data;
if (!data) {
throw createAdapterInitError("Response data", this.mode, "No response data from admin login");
}
return {
session_token: data.token,
expires_at: data.expires_at,
user: data.user as unknown as AdminUser | ProjectUser,
scopes: data.scopes,
};
} else {
if (!this.service) {
throw createAdapterInitError("Auth service", this.mode, "adminLogin");
}
const result = await this.service.authenticateAdmin({
username,
password,
});
if (!result) {
throw createAdapterInitError("Authentication result", this.mode, "No result from authenticate admin");
}
return {
session_token: result.token,
expires_at: result.expires_at,
user: result.user as unknown as AdminUser | ProjectUser,
scopes: result.scopes,
};
}
}
async setSessionToken(token: string): Promise<void> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "setSessionToken");
}
this.httpClient.setSessionToken(token);
} else {
// Server mode doesn't need session token management
// Sessions are managed via database
}
}
setApiKey(apiKey: string): void {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "setApiKey");
}
this.httpClient.setApiKey(apiKey);
} else {
// Server mode doesn't use API keys for HTTP requests
}
}
async logout(): Promise<{ success: boolean }> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "logout");
}
const response = await this.httpClient.logout();
return response.data || { success: false };
} else {
// Server mode logout is handled differently
return { success: true };
}
}
async getCurrentUser(): Promise<{
success: boolean;
data?: AdminUser | ProjectUser;
error?: string;
}> {
if (this.mode === "client") {
if (!this.httpClient) {
return {
success: false,
error: "HTTP client not initialized",
};
}
try {
const response = await this.httpClient.getCurrentSession();
if (response && typeof response === "object") {
if ("success" in response) {
const apiResponse = response as { success: boolean; data?: unknown; error?: string };
if (apiResponse.success === false) {
return {
success: false,
error: apiResponse.error || "Failed to get current user",
};
}
if (apiResponse.data) {
const session = apiResponse.data as { user?: unknown };
if (session.user) {
return {
success: true,
data: session.user as AdminUser | ProjectUser,
};
}
}
}
if ("user" in response) {
return {
success: true,
data: (response as { user: unknown }).user as AdminUser | ProjectUser,
};
}
if (("id" in response || "username" in response || "email" in response) &&
!("success" in response)) {
return {
success: true,
data: response as unknown as AdminUser | ProjectUser,
};
}
}
return {
success: false,
error: "Invalid response format",
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Failed to get current user",
};
}
} else {
if (!this.service) {
return {
success: false,
error: "Auth service not initialized",
};
}
// Server mode getCurrentUser requires passing the session token
// Use validateSession(token) to get user info in server mode
return {
success: false,
error: "getCurrentUser requires session context. In server mode, use validateSession(token) to get user information.",
};
}
}
async refreshSession(): Promise<{
session_token: string;
expires_at: string;
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "refreshSession");
}
const response = await this.httpClient.refreshSession();
const data = response?.data;
if (!data) {
throw createAdapterInitError("Response data", this.mode, "No response data from refresh session");
}
return {
session_token: data.session_token,
expires_at: data.expires_at,
};
} else {
if (!this.service) {
throw createAdapterInitError("Auth service", this.mode, "refreshSession");
}
// Server mode refresh requires the current session token
// The caller should pass the token and handle the refresh through the auth service directly
throw createAdapterInitError("Session context", this.mode, "refreshSession requires session context. In server mode, use authService.refreshSession(token) directly.");
}
}
async validateSession(token: string): Promise<{
valid: boolean;
session?: AdminUser | ProjectUser;
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "validateSession");
}
const response = await this.httpClient.validateSession(token);
const data = response?.data;
if (!data) {
return { valid: false };
}
// HTTP client returns session directly, not wrapped in user object
let user: AdminUser | ProjectUser | undefined;
if (data.session && typeof data.session === "object") {
const sessionObj = data.session as unknown as Record<string, unknown>;
// Check if it's AdminUser or ProjectUser based on properties
if ("role" in sessionObj || "access_level" in sessionObj) {
user = sessionObj as unknown as AdminUser;
} else if ("project_id" in sessionObj || "metadata" in sessionObj) {
user = sessionObj as unknown as ProjectUser;
}
}
const result: { valid: boolean; session?: AdminUser | ProjectUser } = {
valid: data.valid || false,
};
if (user) {
result.session = user;
}
return result;
} else {
if (!this.service) {
throw createAdapterInitError("Auth service", this.mode, "validateSession");
}
const result = await this.service.validateSession(token);
if (!result) {
const response: {
valid: boolean;
session?: AdminUser | ProjectUser;
} = {
valid: false,
};
return response;
}
// Service returns Session | null, but we need to return user object
// This is a limitation - the service validateSession doesn't return the user object
// We'll return valid: true but session will be undefined until service is updated
const response: {
valid: boolean;
session?: AdminUser | ProjectUser;
} = {
valid: true,
};
return response;
}
}
async changePassword(
_oldPassword: string,
_newPassword: string
): Promise<{ success: boolean }> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "changePassword");
}
// Client mode changePassword would need user context
throw createAdapterInitError("User context", this.mode, "changePassword requires user context in client mode");
} else {
if (!this.service) {
throw createAdapterInitError("Auth service", this.mode, "changePassword");
}
// Server mode changePassword would need user context
throw createAdapterInitError("User context", this.mode, "changePassword requires user context in server mode");
}
}
async regenerateApiKey(
req: unknown
): Promise<{ success: boolean; data?: { apiKey: string }; error?: string }> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "regenerateApiKey");
}
const response = await this.httpClient.regenerateApiKey(req);
return response.data || { success: false, error: "Failed to regenerate API key" };
} else {
if (!this.service) {
throw createAdapterInitError("Auth service", this.mode, "regenerateApiKey");
}
return this.service.regenerateApiKey(req);
}
}
// Additional methods for adminLogin wrapper
async adminLogin(credentials: {
username: string;
password: string;
remember_me?: boolean;
}): Promise<{
success: boolean;
data?: {
session_token: string;
expires_at: string;
user: AdminUser | ProjectUser;
scopes: string[];
};
error?: string;
}> {
try {
const result = await this.login(
credentials.username,
credentials.password,
credentials.remember_me
);
return {
success: true,
data: {
session_token: result.session_token,
expires_at: result.expires_at,
user: result.user,
scopes: result.scopes,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Login failed",
};
}
}
async adminApiLogin(
apiKey: string | { api_key?: string }
): Promise<{
success: boolean;
data?: {
user: AdminUser & { scopes: string[] };
session_token: string;
expires_at: string;
};
error?: string;
}> {
if (this.mode === "client") {
if (!this.httpClient) {
return {
success: false,
error: "HTTP client not initialized",
};
}
try {
const apiKeyValue = typeof apiKey === "string" ? apiKey : apiKey.api_key;
if (!apiKeyValue) {
return {
success: false,
error: "API key is required",
};
}
const request: ApiKeyAuthRequest = { api_key: apiKeyValue };
const response = await this.httpClient.adminApiLogin(request);
// Response interceptor returns response.data, so response is already unwrapped
// Handle both formats: { user, token, ... } or { data: { user, token, ... } }
let responseData: {
user?: unknown;
scopes?: string[];
token?: string;
expires_at?: string;
} | undefined;
if (response && typeof response === "object") {
// If response has 'data' field, extract it
if ("data" in response && response.data && typeof response.data === "object") {
responseData = response.data as {
user?: unknown;
scopes?: string[];
token?: string;
expires_at?: string;
};
} else {
// Response is the data directly
responseData = response as {
user?: unknown;
scopes?: string[];
token?: string;
expires_at?: string;
};
}
}
if (responseData && responseData.user) {
const userData = responseData.user;
const scopes = responseData.scopes || [];
return {
success: true,
data: {
user: {
...(userData as Record<string, unknown>),
scopes,
} as AdminUser & { scopes: string[] },
session_token: responseData.token || "",
expires_at: responseData.expires_at || "",
},
};
}
return {
success: false,
error: "No data in response",
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "API key login failed",
};
}
} else {
if (!this.service) {
return {
success: false,
error: "Auth service not initialized",
};
}
try {
const apiKeyValue = typeof apiKey === "string" ? apiKey : (apiKey as { api_key?: string }).api_key;
if (!apiKeyValue) {
return {
success: false,
error: "API key is required",
};
}
const result = await this.service.authenticateAdminWithApiKey({
api_key: apiKeyValue,
});
const userData = result.user;
const scopes = result.scopes || [];
return {
success: true,
data: {
user: {
...userData,
scopes,
} as AdminUser & { scopes: string[] },
session_token: result.token,
expires_at: result.expires_at,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "API key login failed",
};
}
}
}
async register(registerData: {
username: string;
email: string;
password: string;
role?: string;
access_level?: string;
permissions?: string[];
}): Promise<{ success: boolean; user: Record<string, unknown> }> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "register");
}
const response = await this.httpClient.register(registerData);
// Response interceptor returns response.data, so response is already unwrapped
// Handle both formats: { success: true, user: {...} } or { success: true, data: { user: {...} } }
if (response && typeof response === "object") {
// If response has 'data' field, extract it
if ("data" in response && response.data && typeof response.data === "object") {
const data = response.data as { success?: boolean; user?: Record<string, unknown> };
if (data.user) {
return {
success: data.success ?? true,
user: data.user,
};
}
}
// If response has 'user' field directly
if ("user" in response) {
const user = (response as { user?: Record<string, unknown> }).user;
return {
success: (response as { success?: boolean }).success ?? true,
user: user || ({} as Record<string, unknown>),
};
}
}
// Fallback to empty user if response format is unexpected
return {
success: false,
user: {} as Record<string, unknown>,
};
} else {
if (!this.service) {
throw createAdapterInitError("Auth service", this.mode, "register");
}
const result = await this.service.register(registerData);
return {
success: result.success,
user: result.user as unknown as Record<string, unknown>,
};
}
}
async validateApiKey(apiKey: string): Promise<{
valid: boolean;
key_info?: {
id: string;
name: string;
type: string;
scopes: string[];
project_id?: string;
};
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode, "validateApiKey");
}
const response = await this.httpClient.validateApiKey(apiKey);
// Normalize response format
if (response && typeof response === "object") {
const data = "data" in response ? (response as { data?: unknown }).data : response;
if (data && typeof data === "object") {
const responseData = data as {
valid?: boolean;
key_info?: {
id?: string;
name?: string;
type?: string;
scopes?: string[];
project_id?: string;
};
};
const result: {
valid: boolean;
key_info?: {
id: string;
name: string;
type: string;
scopes: string[];
project_id?: string;
};
} = {
valid: responseData.valid || false,
};
if (responseData.key_info) {
result.key_info = {
id: responseData.key_info.id || "",
name: responseData.key_info.name || "",
type: responseData.key_info.type || "",
scopes: responseData.key_info.scopes || [],
};
if (responseData.key_info.project_id !== undefined) {
result.key_info.project_id = responseData.key_info.project_id;
}
}
return result;
}
}
return { valid: false };
} else {
if (!this.service) {
throw createAdapterInitError("Auth service", this.mode, "validateApiKey");
}
// Server mode - use auth service validateApiKey if available
// For now, return not implemented since auth service may not have this method
throw createAdapterInitError("Auth service", this.mode, "validateApiKey not available in server mode");
}
}
}