714 lines
18 KiB
TypeScript
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),
|
|
);
|
|
}
|