integratedhub/db/sqlUserRepository.ts
2026-02-14 02:20:19 +00:00

714 lines
18 KiB
TypeScript

// db/sqlUserRepository.ts
// PostgreSQL database operations for users, roles, state keys, tokens, and financial balances
import { and, eq, gte, lte, sql } from "drizzle-orm";
import {
financial_balances,
roles,
session_configs,
stateKeys,
user_pins,
userRoles,
users,
userTokens,
webTokens,
} from "./schema.ts";
import { db } from "./db.ts";
import { User } from "./interfaces.ts";
// Re-export types from userService for backward compatibility
export interface FinancialBalance {
id: string;
user_id: string;
balance: number;
balance_date: number;
encrypted_data: string | null;
created_on: number;
updated_on: number;
}
export interface Role {
id: string;
description: string;
}
export interface UserWithRole {
id: string;
name: string;
email: string;
role: string;
roleDescription: string;
}
// ============================================================================
// SQL USER OPERATIONS
// ============================================================================
export async function sqlGetAllUsers() {
return await db.select().from(users);
}
export async function sqlDeleteUser(id: string) {
return await db.delete(users).where(eq(users.id, id)).returning();
}
export async function sqlCreateUserRole(user_id: string, role_id: string) {
const now = Date.now();
return await db.insert(userRoles)
.values({
id: crypto.randomUUID(),
userId: user_id,
roleId: role_id,
created_on: now,
updated_on: now,
})
.returning();
}
// Upsert user by OAuth2 ID
export async function sqlUpsertUser(id: string, name: string, email: string) {
const existingUser = await db.select().from(users).where(eq(users.id, id));
if (existingUser.length > 0) {
// Update existing user profile
return await db.update(users)
.set({ name, email })
.where(eq(users.id, id))
.returning();
} else {
// Insert new user profile
return await db.insert(users)
.values({
id,
name,
email,
phone: "",
address_1: "",
address_2: "",
city: "",
state: "",
zip: "",
country: "",
started_on: Date.now(),
updated_on: Date.now(),
})
.returning();
}
}
// Upsert full user record (all profile fields) by user_id
export async function sqlUpsertUserFull(
user: { id: string } & Partial<User>,
) {
const now = Date.now();
const insertValues = {
id: user.id,
name: user.name ?? "",
email: user.email ?? "",
phone: user.phone ?? "",
address_1: user.address_1 ?? "",
address_2: user.address_2 ?? "",
city: user.city ?? "",
state: user.state ?? "",
zip: user.zip ?? "",
country: user.country ?? "",
user_type: user.user_type ?? null,
onboarded_landlord: user.onboarded_landlord ?? false,
onboarded_tenant: user.onboarded_tenant ?? false,
email_verified: user.email_verified ?? null,
banking_setup_complete: user.banking_setup_complete ?? null,
onboarding_step: user.onboarding_step ?? null,
notification_preferences_json: user.notification_preferences_json ?? null,
payment_preferences_json: user.payment_preferences_json ?? null,
encrypted_data: user.encrypted_data ?? null,
started_on: user.started_on ?? now,
updated_on: user.updated_on ?? now,
};
const updateValues = {
name: insertValues.name,
email: insertValues.email,
phone: insertValues.phone,
address_1: insertValues.address_1,
address_2: insertValues.address_2,
city: insertValues.city,
state: insertValues.state,
zip: insertValues.zip,
country: insertValues.country,
user_type: insertValues.user_type,
onboarded_landlord: insertValues.onboarded_landlord,
onboarded_tenant: insertValues.onboarded_tenant,
email_verified: insertValues.email_verified,
banking_setup_complete: insertValues.banking_setup_complete,
onboarding_step: insertValues.onboarding_step,
notification_preferences_json: insertValues.notification_preferences_json,
payment_preferences_json: insertValues.payment_preferences_json,
encrypted_data: insertValues.encrypted_data,
// Keep original started_on, only bump updated_on
updated_on: now,
};
const result = await db.insert(users)
.values(insertValues)
.onConflictDoUpdate({
target: users.id,
set: updateValues,
})
.returning();
return result;
}
// Update all user fields by user_id
export async function sqlUpdateUser(id: string, fields: Partial<User>) {
return await db.update(users)
.set(fields)
.where(eq(users.id, id))
.returning();
}
// Get user roles by user_id
export async function sqlGetUserRoles(id: string) {
return await db.select({ roleId: userRoles.roleId }).from(userRoles).where(
eq(userRoles.userId, id),
);
}
export async function sqlGetAllUserRoles() {
return await db.select().from(userRoles);
}
// Delete user role by user_id
export async function sqlDeleteUserRole(id: string) {
return await db.delete(userRoles)
.where(eq(userRoles.userId, id))
.returning();
}
// Update user role by user_id
export async function sqlUpdateUserRole(id: string, role_id: string) {
return await db.update(userRoles)
.set({ roleId: role_id })
.where(eq(userRoles.userId, id))
.returning();
}
// Get all users with their roles for admin center
export async function sqlGetAllUsersWithRoles() {
const results = await db
.select({
id: users.id,
name: users.name || "",
email: users.email || "",
role: roles.id || "",
roleDescription: roles.description || "",
})
.from(users)
.leftJoin(userRoles, eq(users.id, userRoles.userId))
.leftJoin(roles, eq(userRoles.roleId, roles.id));
// Group by user
const userMap: Record<string, UserWithRole> = {};
for (const row of results) {
if (!userMap[row.id]) {
userMap[row.id] = {
id: row.id,
name: row.name || "",
email: row.email || "",
role: row.role || "",
roleDescription: row.roleDescription || "",
};
}
}
return Object.values(userMap);
}
// Get all available roles
export async function sqlGetAllRoles() {
return await db.select().from(roles);
}
export async function sqlGetUserWithRoles(userId: string) {
const user = await sqlGetUser(userId);
if (!user) {
return null;
}
const userRoles = await sqlGetUserRoles(userId);
return {
id: user?.id,
name: user?.name,
email: user?.email,
phone: user?.phone,
address_1: user?.address_1,
address_2: user?.address_2,
city: user?.city,
state: user?.state,
zip: user?.zip,
country: user?.country,
roles: userRoles,
};
}
// ============================================================================
// SQL STATE KEY OPERATIONS
// ============================================================================
export async function sqlGetAllStateKeys() {
return await db.select().from(stateKeys);
}
export async function sqlGetStateKeys(stateId: string) {
return await db.select().from(stateKeys).where(
eq(stateKeys.state_id, stateId),
);
}
export async function sqlSetStateKey(
sealKey: string,
stateId: string,
cookieHash: string,
) {
const now = Date.now();
return await db.insert(stateKeys)
.values({
seal_key: sealKey,
state_id: stateId,
cookie_hash: cookieHash,
created_on: now,
})
.onConflictDoUpdate({
target: stateKeys.seal_key,
set: {
state_id: stateId,
cookie_hash: cookieHash,
created_on: now,
},
})
.returning();
}
export async function sqlDeleteStateKey(sealKey: string) {
return await db.delete(stateKeys).where(eq(stateKeys.seal_key, sealKey));
}
// ============================================================================
// SQL USER TOKENS OPERATIONS
// ============================================================================
export async function sqlGetUserTokens(userId: string) {
// Returns all tokens for a user (supports multiple sessions)
const result = await db.select().from(userTokens).where(
eq(userTokens.user_id, userId),
);
return result; // Returns array of tokens
}
export async function sqlGetUserTokenByStateId(
userId: string,
stateId: string,
) {
// Get a specific token by user_id and state_id
const result = await db.select().from(userTokens).where(
and(
eq(userTokens.user_id, userId),
eq(userTokens.state_id, stateId),
),
);
return result[0] || null;
}
export async function sqlSetUserTokens(
userId: string,
stateId: string,
accessToken: string,
refreshToken: string,
) {
const now = Date.now();
// Ensure FK parent exists in public.users before writing public.user_tokens.
// This prevents auth callback failures if cache/database state drifts.
await db.execute(sql`
INSERT INTO public.users (
id,
name,
email,
started_on,
updated_on
)
VALUES (
${userId},
${""},
${""},
${now},
${now}
)
ON CONFLICT (id) DO NOTHING
`);
const result = await db.execute(sql`
INSERT INTO public.user_tokens (
user_id,
state_id,
access_token,
refresh_token,
created_on,
updated_on
)
VALUES (
${userId},
${stateId},
${accessToken},
${refreshToken},
${now},
${now}
)
ON CONFLICT (user_id, state_id)
DO UPDATE SET
state_id = EXCLUDED.state_id,
access_token = EXCLUDED.access_token,
refresh_token = EXCLUDED.refresh_token,
updated_on = EXCLUDED.updated_on
RETURNING
user_id,
state_id,
access_token,
refresh_token,
created_on,
updated_on
`);
const rows = (result as { rows?: Record<string, unknown>[] }).rows ?? [];
return rows as typeof userTokens.$inferSelect[];
}
export async function sqlGetAllUserTokens() {
return await db.select().from(userTokens);
}
export async function sqlDeleteUserTokens(userId: string) {
return await db.delete(userTokens).where(eq(userTokens.user_id, userId));
}
export async function sqlDeleteUserTokenByStateId(
userId: string,
stateId: string,
) {
// Delete a specific session's token
return await db.delete(userTokens).where(
and(
eq(userTokens.user_id, userId),
eq(userTokens.state_id, stateId),
),
);
}
// ============================================================================
// SQL WEB TOKENS OPERATIONS
// ============================================================================
export async function sqlCreateWebToken(
tokenId: string,
stateId: string,
userId: string,
tokenType: string,
tokenValue: string,
expiresOn: number,
metadata?: unknown | null,
) {
const now = Date.now();
return await db.insert(webTokens)
.values({
token_id: tokenId,
state_id: stateId,
user_id: userId,
token_type: tokenType,
token_value: tokenValue,
expires_on: expiresOn,
created_on: now,
updated_on: now,
last_used_on: null,
is_active: true,
metadata: metadata ?? null,
})
.returning();
}
export async function sqlGetWebToken(tokenId: string) {
const result = await db.select().from(webTokens).where(
eq(webTokens.token_id, tokenId),
);
return result[0] || null;
}
export async function sqlGetWebTokenByValue(tokenValue: string) {
const result = await db.select().from(webTokens).where(
eq(webTokens.token_value, tokenValue),
);
return result[0] || null;
}
export async function sqlGetWebTokensByStateId(stateId: string) {
return await db.select().from(webTokens).where(
eq(webTokens.state_id, stateId),
);
}
export async function sqlGetWebTokensByUserId(userId: string) {
return await db.select().from(webTokens).where(eq(webTokens.user_id, userId));
}
export async function sqlUpdateWebToken(
tokenId: string,
fields: Partial<typeof webTokens.$inferInsert>,
) {
return await db.update(webTokens)
.set({
...fields,
updated_on: Date.now(),
})
.where(eq(webTokens.token_id, tokenId))
.returning();
}
export async function sqlDeleteWebToken(tokenId: string) {
return await db.delete(webTokens).where(eq(webTokens.token_id, tokenId));
}
export async function sqlDeleteExpiredWebTokens(now = Date.now()) {
return await db.delete(webTokens).where(
lte(webTokens.expires_on, now),
);
}
export async function sqlGetActiveWebTokens(now = Date.now()) {
return await db.select().from(webTokens).where(
and(
eq(webTokens.is_active, true),
gte(webTokens.expires_on, now),
),
);
}
export async function sqlGetUser(userId: string) {
const result = await db.select().from(users).where(eq(users.id, userId));
return result[0] || null;
}
// Get user by ID (alias for compatibility)
export async function sqlGetUserById(userId: string) {
const result = await sqlGetUser(userId);
return result || null;
}
export async function sqlCreateUser(
id: string,
name: string,
email: string,
phone: string,
address_1: string,
address_2: string,
city: string,
state: string,
zip: string,
country: string,
) {
const now = Date.now();
return await db.insert(users)
.values({
id,
name,
email,
phone,
address_1,
address_2,
city,
state,
zip,
country,
started_on: now,
updated_on: now,
})
.returning();
}
// ============================================================================
// SQL FINANCIAL BALANCE OPERATIONS
// ============================================================================
// Upsert financial balance for a user
export async function sqlUpsertFinancialBalance(
user_id: string,
balance: number,
balance_date: number,
) {
const now = Date.now();
// Check if record exists
const existing = await db.select().from(financial_balances).where(
eq(financial_balances.user_id, user_id),
);
if (existing.length > 0) {
// Update existing record
const newBalance: Partial<FinancialBalance> = {
balance,
balance_date,
updated_on: now,
};
return await db.update(financial_balances)
.set(newBalance)
.where(eq(financial_balances.id, existing[0].id))
.returning();
} else {
// Insert new record
return await db.insert(financial_balances)
.values({
id: crypto.randomUUID(),
user_id,
balance,
balance_date,
created_on: now,
updated_on: now,
})
.returning();
}
}
export async function sqlDeleteFinancialBalance(id: string) {
return await db.delete(financial_balances).where(
eq(financial_balances.id, id),
);
}
export async function sqlGetAllFinancialBalances() {
return await db.select().from(financial_balances);
}
export async function sqlGetFinancialBalancesByUser(user_id: string) {
return await db.select().from(financial_balances).where(
eq(financial_balances.user_id, user_id),
);
}
// ============================================================================
// SQL USER PINS OPERATIONS
// ============================================================================
export async function sqlGetUserPin(userId: string) {
const result = await db.select().from(user_pins).where(
eq(user_pins.user_id, userId),
);
return result[0] || null;
}
export async function sqlSetUserPin(
userId: string,
pin: string,
) {
const now = Date.now();
return await db.insert(user_pins)
.values({
user_id: userId,
pin,
created_on: now,
modified_on: now,
})
.onConflictDoUpdate({
target: user_pins.user_id,
set: {
pin,
modified_on: now,
},
})
.returning();
}
export async function sqlUpdateUserPin(
userId: string,
pin: string,
) {
const now = Date.now();
return await db.update(user_pins)
.set({
pin,
modified_on: now,
})
.where(eq(user_pins.user_id, userId))
.returning();
}
export async function sqlGetAllUserPins() {
return await db.select().from(user_pins);
}
export async function sqlDeleteUserPin(userId: string) {
return await db.delete(user_pins).where(eq(user_pins.user_id, userId));
}
// ============================================================================
// SQL SESSION CONFIG OPERATIONS
// ============================================================================
export async function sqlGetSessionConfig(deviceId: string) {
const result = await db.select().from(session_configs).where(
eq(session_configs.device_id, deviceId),
);
return result[0] || null;
}
export async function sqlSetSessionConfig(
deviceId: string,
userId: string,
otherData: string,
activeRoute: string,
pageData: string,
) {
const now = Date.now();
return await db.insert(session_configs)
.values({
device_id: deviceId,
user_id: userId,
other_data: otherData,
active_date: now,
active_route: activeRoute,
page_data: pageData,
})
.onConflictDoUpdate({
target: session_configs.device_id,
set: {
user_id: userId,
other_data: otherData,
active_date: now,
active_route: activeRoute,
page_data: pageData,
},
})
.returning();
}
export async function sqlUpdateSessionConfig(
deviceId: string,
fields: Partial<typeof session_configs.$inferInsert>,
) {
return await db.update(session_configs)
.set(fields)
.where(eq(session_configs.device_id, deviceId))
.returning();
}
export async function sqlDeleteSessionConfig(deviceId: string) {
return await db.delete(session_configs).where(
eq(session_configs.device_id, deviceId),
);
}
export async function sqlGetAllSessionConfigs() {
return await db.select().from(session_configs);
}
export async function sqlGetSessionConfigsByUser(userId: string) {
return await db.select().from(session_configs).where(
eq(session_configs.user_id, userId),
);
}