189 lines
4.3 KiB
TypeScript
189 lines
4.3 KiB
TypeScript
// src/services/user.ts
|
|
// Copyright (C) 2026 DTP Technologies, LLC
|
|
// All Rights Reserved
|
|
|
|
import crypto from "crypto";
|
|
import { Types } from "mongoose";
|
|
|
|
import { DtpService } from "../lib/service.js";
|
|
import User, { IUser } from "../models/user.js";
|
|
import env from "../config/env.js";
|
|
|
|
export class UserService extends DtpService {
|
|
get name(): string {
|
|
return "UserService";
|
|
}
|
|
get slug(): string {
|
|
return "user";
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
this.log.info("service started");
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
this.log.info("service stopped");
|
|
}
|
|
|
|
/**
|
|
* Create a new user account
|
|
*/
|
|
async create(
|
|
username: string,
|
|
password: string,
|
|
displayName: string,
|
|
isAdmin: boolean = false,
|
|
): Promise<IUser> {
|
|
// Validate input
|
|
if (!username || !password || !displayName) {
|
|
throw new Error("Username, password, and display name are required");
|
|
}
|
|
|
|
if (username.length < 3 || username.length > 12) {
|
|
throw new Error("Username must be between 3 and 12 characters");
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
throw new Error("Password must be at least 8 characters");
|
|
}
|
|
|
|
if (displayName.length < 3 || displayName.length > 30) {
|
|
throw new Error("Display name must be between 3 and 30 characters");
|
|
}
|
|
|
|
// Check if user already exists
|
|
const existingUser = await User.findOne({
|
|
username_lc: username.toLowerCase(),
|
|
});
|
|
|
|
if (existingUser) {
|
|
throw new Error("Username already taken");
|
|
}
|
|
|
|
// Hash password with salt
|
|
const passwordSalt = env.user.passwordSalt;
|
|
const passwordHash = crypto
|
|
.pbkdf2Sync(password, passwordSalt, 100000, 64, "sha512")
|
|
.toString("hex");
|
|
|
|
// Create user
|
|
const user = new User();
|
|
user.username = username;
|
|
user.username_lc = username.toLowerCase();
|
|
user.password = passwordHash;
|
|
user.passwordSalt = passwordSalt;
|
|
user.displayName = displayName;
|
|
user.flags = {
|
|
isAdmin,
|
|
isTest: false,
|
|
isBanned: false,
|
|
};
|
|
user.connections = {
|
|
gab: {
|
|
social: { apiToken: "" },
|
|
ai: { apiToken: "" },
|
|
},
|
|
ai: {
|
|
providerIds: [],
|
|
agentProviderId: null,
|
|
agentModel: "",
|
|
vectorProviderId: null,
|
|
vectorModel: "",
|
|
utilityProviderId: null,
|
|
utilityModel: "",
|
|
},
|
|
};
|
|
|
|
await user.save();
|
|
this.log.info("User created", {
|
|
userId: user._id,
|
|
username: user.username,
|
|
});
|
|
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Get user by ID
|
|
*/
|
|
async getUserById(_id: Types.ObjectId): Promise<IUser | null> {
|
|
const user = await User.findById(_id);
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Get user by username (case-insensitive)
|
|
*/
|
|
async getUserByUsername(username: string): Promise<IUser | null> {
|
|
const user = await User.findOne({
|
|
username_lc: username.toLowerCase(),
|
|
});
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Get user by ID with sensitive fields (for auth purposes)
|
|
*/
|
|
async getUserByIdWithCredentials(_id: Types.ObjectId): Promise<IUser | null> {
|
|
const user = await User.findById(_id).select("+password +passwordSalt");
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Get user by username with sensitive fields (for auth purposes)
|
|
*/
|
|
async getUserByUsernameWithCredentials(
|
|
username: string,
|
|
): Promise<IUser | null> {
|
|
const user = await User.findOne({
|
|
username_lc: username.toLowerCase(),
|
|
}).select("+password +passwordSalt");
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Verify user password
|
|
*/
|
|
async verifyPassword(user: IUser, password: string): Promise<boolean> {
|
|
if (!user.password || !user.passwordSalt) {
|
|
return false;
|
|
}
|
|
|
|
const passwordHash = crypto
|
|
.pbkdf2Sync(password, user.passwordSalt, 100000, 64, "sha512")
|
|
.toString("hex");
|
|
|
|
return passwordHash === user.password;
|
|
}
|
|
|
|
/**
|
|
* Check if user is banned
|
|
*/
|
|
isUserBanned(user: IUser): boolean {
|
|
return user.flags.isBanned;
|
|
}
|
|
|
|
/**
|
|
* Get public user data (safe to expose to client)
|
|
*/
|
|
getPublicUserData(user: IUser): {
|
|
_id: string;
|
|
username: string;
|
|
displayName: string;
|
|
flags: IUser["flags"];
|
|
} {
|
|
return {
|
|
_id: user._id.toString(),
|
|
username: user.username,
|
|
displayName: user.displayName,
|
|
flags: user.flags,
|
|
};
|
|
}
|
|
}
|
|
|
|
export default new UserService();
|