294 lines
8.5 KiB
TypeScript
294 lines
8.5 KiB
TypeScript
import pool from '../config/database';
|
|
import logger from '../utils/logger';
|
|
import { User, CreateUserData, UpdateUserData } from '../models/user.model';
|
|
|
|
class UserService {
|
|
/**
|
|
* Find user by Authentik sub (subject identifier)
|
|
*/
|
|
async findByAuthentikSub(sub: string): Promise<User | null> {
|
|
try {
|
|
const query = `
|
|
SELECT id, email, authentik_sub, name, preferred_username, given_name,
|
|
family_name, profile_picture_url, refresh_token, refresh_token_expires_at,
|
|
is_active, last_login_at, created_at, updated_at, preferences, authentik_groups
|
|
FROM users
|
|
WHERE authentik_sub = $1
|
|
`;
|
|
|
|
const result = await pool.query(query, [sub]);
|
|
|
|
if (result.rows.length === 0) {
|
|
logger.debug('User not found by Authentik sub', { sub });
|
|
return null;
|
|
}
|
|
|
|
logger.debug('User found by Authentik sub', { sub, userId: result.rows[0].id });
|
|
return result.rows[0] as User;
|
|
} catch (error) {
|
|
logger.error('Error finding user by Authentik sub', { error, sub });
|
|
throw new Error('Database query failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find user by email
|
|
*/
|
|
async findByEmail(email: string): Promise<User | null> {
|
|
try {
|
|
const query = `
|
|
SELECT id, email, authentik_sub, name, preferred_username, given_name,
|
|
family_name, profile_picture_url, refresh_token, refresh_token_expires_at,
|
|
is_active, last_login_at, created_at, updated_at, preferences, authentik_groups
|
|
FROM users
|
|
WHERE email = $1
|
|
`;
|
|
|
|
const result = await pool.query(query, [email]);
|
|
|
|
if (result.rows.length === 0) {
|
|
logger.debug('User not found by email', { email });
|
|
return null;
|
|
}
|
|
|
|
logger.debug('User found by email', { email, userId: result.rows[0].id });
|
|
return result.rows[0] as User;
|
|
} catch (error) {
|
|
logger.error('Error finding user by email', { error, email });
|
|
throw new Error('Database query failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find user by ID
|
|
*/
|
|
async findById(id: string): Promise<User | null> {
|
|
try {
|
|
const query = `
|
|
SELECT id, email, authentik_sub, name, preferred_username, given_name,
|
|
family_name, profile_picture_url, refresh_token, refresh_token_expires_at,
|
|
is_active, last_login_at, created_at, updated_at, preferences, authentik_groups
|
|
FROM users
|
|
WHERE id = $1
|
|
`;
|
|
|
|
const result = await pool.query(query, [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
logger.debug('User not found by ID', { id });
|
|
return null;
|
|
}
|
|
|
|
logger.debug('User found by ID', { id });
|
|
return result.rows[0] as User;
|
|
} catch (error) {
|
|
logger.error('Error finding user by ID', { error, id });
|
|
throw new Error('Database query failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new user
|
|
*/
|
|
async createUser(userData: CreateUserData): Promise<User> {
|
|
try {
|
|
const query = `
|
|
INSERT INTO users (
|
|
email,
|
|
authentik_sub,
|
|
name,
|
|
preferred_username,
|
|
given_name,
|
|
family_name,
|
|
profile_picture_url,
|
|
is_active,
|
|
authentik_groups
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, true, $8)
|
|
RETURNING id, email, authentik_sub, name, preferred_username, given_name,
|
|
family_name, profile_picture_url, refresh_token, refresh_token_expires_at,
|
|
is_active, last_login_at, created_at, updated_at, preferences, authentik_groups
|
|
`;
|
|
|
|
const values = [
|
|
userData.email,
|
|
userData.authentik_sub,
|
|
userData.name || null,
|
|
userData.preferred_username || null,
|
|
userData.given_name || null,
|
|
userData.family_name || null,
|
|
userData.profile_picture_url || null,
|
|
userData.authentik_groups ?? [],
|
|
];
|
|
|
|
const result = await pool.query(query, values);
|
|
|
|
const user = result.rows[0] as User;
|
|
logger.info('User created successfully', {
|
|
userId: user.id,
|
|
email: user.email,
|
|
});
|
|
|
|
return user;
|
|
} catch (error) {
|
|
logger.error('Error creating user', { error, email: userData.email });
|
|
throw new Error('Failed to create user');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update user information
|
|
*/
|
|
async updateUser(id: string, data: UpdateUserData): Promise<User> {
|
|
try {
|
|
const updateFields: string[] = [];
|
|
const values: any[] = [];
|
|
let paramCount = 1;
|
|
|
|
if (data.name !== undefined) {
|
|
updateFields.push(`name = $${paramCount++}`);
|
|
values.push(data.name);
|
|
}
|
|
if (data.preferred_username !== undefined) {
|
|
updateFields.push(`preferred_username = $${paramCount++}`);
|
|
values.push(data.preferred_username);
|
|
}
|
|
if (data.given_name !== undefined) {
|
|
updateFields.push(`given_name = $${paramCount++}`);
|
|
values.push(data.given_name);
|
|
}
|
|
if (data.family_name !== undefined) {
|
|
updateFields.push(`family_name = $${paramCount++}`);
|
|
values.push(data.family_name);
|
|
}
|
|
if (data.profile_picture_url !== undefined) {
|
|
updateFields.push(`profile_picture_url = $${paramCount++}`);
|
|
values.push(data.profile_picture_url);
|
|
}
|
|
if (data.is_active !== undefined) {
|
|
updateFields.push(`is_active = $${paramCount++}`);
|
|
values.push(data.is_active);
|
|
}
|
|
if (data.preferences !== undefined) {
|
|
updateFields.push(`preferences = $${paramCount++}`);
|
|
values.push(JSON.stringify(data.preferences));
|
|
}
|
|
|
|
if (updateFields.length === 0) {
|
|
throw new Error('No fields to update');
|
|
}
|
|
|
|
updateFields.push(`updated_at = CURRENT_TIMESTAMP`);
|
|
values.push(id);
|
|
|
|
const query = `
|
|
UPDATE users
|
|
SET ${updateFields.join(', ')}
|
|
WHERE id = $${paramCount}
|
|
RETURNING id, email, authentik_sub, name, preferred_username, given_name,
|
|
family_name, profile_picture_url, refresh_token, refresh_token_expires_at,
|
|
is_active, last_login_at, created_at, updated_at, preferences, authentik_groups
|
|
`;
|
|
|
|
const result = await pool.query(query, values);
|
|
|
|
if (result.rows.length === 0) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
const user = result.rows[0] as User;
|
|
logger.info('User updated successfully', { userId: user.id });
|
|
|
|
return user;
|
|
} catch (error) {
|
|
logger.error('Error updating user', { error, userId: id });
|
|
throw new Error('Failed to update user');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update last login timestamp
|
|
*/
|
|
async updateLastLogin(id: string): Promise<void> {
|
|
try {
|
|
const query = `
|
|
UPDATE users
|
|
SET last_login_at = CURRENT_TIMESTAMP
|
|
WHERE id = $1
|
|
`;
|
|
|
|
await pool.query(query, [id]);
|
|
logger.debug('Updated last login timestamp', { userId: id });
|
|
} catch (error) {
|
|
logger.error('Error updating last login', { error, userId: id });
|
|
// Don't throw - this is not critical
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update refresh token
|
|
*/
|
|
async updateRefreshToken(
|
|
id: string,
|
|
refreshToken: string | null,
|
|
expiresAt: Date | null
|
|
): Promise<void> {
|
|
try {
|
|
const query = `
|
|
UPDATE users
|
|
SET refresh_token = $1,
|
|
refresh_token_expires_at = $2
|
|
WHERE id = $3
|
|
`;
|
|
|
|
await pool.query(query, [refreshToken, expiresAt, id]);
|
|
logger.debug('Updated refresh token', { userId: id });
|
|
} catch (error) {
|
|
logger.error('Error updating refresh token', { error, userId: id });
|
|
throw new Error('Failed to update refresh token');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user is active
|
|
*/
|
|
async isUserActive(id: string): Promise<boolean> {
|
|
try {
|
|
const query = `
|
|
SELECT is_active
|
|
FROM users
|
|
WHERE id = $1
|
|
`;
|
|
|
|
const result = await pool.query(query, [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return result.rows[0].is_active;
|
|
} catch (error) {
|
|
logger.error('Error checking user active status', { error, userId: id });
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync Authentik groups for a user
|
|
*/
|
|
async updateGroups(id: string, groups: string[]): Promise<void> {
|
|
try {
|
|
await pool.query(
|
|
`UPDATE users SET authentik_groups = $1 WHERE id = $2`,
|
|
[groups, id]
|
|
);
|
|
logger.debug('Updated authentik_groups', { userId: id });
|
|
} catch (error) {
|
|
logger.error('Error updating authentik_groups', { error, userId: id });
|
|
throw new Error('Failed to update user groups');
|
|
}
|
|
}
|
|
}
|
|
|
|
export default new UserService();
|