Refactor code structure for improved readability and maintainability

This commit is contained in:
2025-11-27 23:47:33 +00:00
parent ab88ac896f
commit 9f5e7e2558
23 changed files with 13024 additions and 109 deletions

View File

@@ -4,13 +4,10 @@
import * as plugins from '../plugins.ts';
import type { IApiToken, ITokenScope, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
export class ApiToken
extends plugins.smartdata.SmartDataDbDoc<ApiToken, ApiToken>
implements IApiToken
{
@plugins.smartdata.Collection(() => db)
export class ApiToken extends plugins.smartdata.SmartDataDbDoc<ApiToken, ApiToken> implements IApiToken {
@plugins.smartdata.unI()
public id: string = '';

View File

@@ -4,13 +4,10 @@
import * as plugins from '../plugins.ts';
import type { IAuditLog, TAuditAction, TAuditResourceType } from '../interfaces/audit.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
export class AuditLog
extends plugins.smartdata.SmartDataDbDoc<AuditLog, AuditLog>
implements IAuditLog
{
@plugins.smartdata.Collection(() => db)
export class AuditLog extends plugins.smartdata.SmartDataDbDoc<AuditLog, AuditLog> implements IAuditLog {
@plugins.smartdata.unI()
public id: string = '';

View File

@@ -1,50 +1,65 @@
/**
* Database connection singleton
*
* SmartData models need a db reference at class definition time via lazy getter.
* The actual .init() is called later when the server starts.
*/
import * as plugins from '../plugins.ts';
import { User } from './user.ts';
let dbInstance: plugins.smartdata.SmartdataDb | null = null;
// Database instance - created lazily in initDb()
// The @Collection(() => db) decorator uses a lazy getter, so db can be undefined
// until initDb() is called. Default admin is seeded after db.init() completes.
export let db: plugins.smartdata.SmartdataDb;
let isInitialized = false;
/**
* Initialize database connection
*/
export async function initDb(config: {
mongoDbUrl: string;
mongoDbName?: string;
}): Promise<plugins.smartdata.SmartdataDb> {
if (dbInstance) {
return dbInstance;
export async function initDb(
mongoDbUrl: string,
mongoDbName?: string
): Promise<plugins.smartdata.SmartdataDb> {
if (isInitialized && db) {
return db;
}
dbInstance = new plugins.smartdata.SmartdataDb({
mongoDbUrl: config.mongoDbUrl,
mongoDbName: config.mongoDbName || 'stackregistry',
// Create the database instance with actual configuration
db = new plugins.smartdata.SmartdataDb({
mongoDbUrl: mongoDbUrl,
mongoDbName: mongoDbName || 'stackregistry',
});
await dbInstance.init();
await db.init();
isInitialized = true;
console.log('Database connected successfully');
return dbInstance;
// Seed default admin user if none exists
try {
await User.seedDefaultAdmin();
} catch (err) {
console.warn('[Database] Failed to seed default admin:', err);
}
return db;
}
/**
* Get database instance (must call initDb first)
* Get database instance (for backward compatibility)
*/
export function getDb(): plugins.smartdata.SmartdataDb {
if (!dbInstance) {
throw new Error('Database not initialized. Call initDb() first.');
}
return dbInstance;
return db;
}
/**
* Close database connection
*/
export async function closeDb(): Promise<void> {
if (dbInstance) {
await dbInstance.close();
dbInstance = null;
if (db && isInitialized) {
await db.close();
isInitialized = false;
console.log('Database connection closed');
}
}
@@ -53,5 +68,5 @@ export async function closeDb(): Promise<void> {
* Check if database is connected
*/
export function isDbConnected(): boolean {
return dbInstance !== null;
return isInitialized;
}

View File

@@ -4,13 +4,10 @@
import * as plugins from '../plugins.ts';
import type { IOrganizationMember, TOrganizationRole } from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
export class OrganizationMember
extends plugins.smartdata.SmartDataDbDoc<OrganizationMember, OrganizationMember>
implements IOrganizationMember
{
@plugins.smartdata.Collection(() => db)
export class OrganizationMember extends plugins.smartdata.SmartDataDbDoc<OrganizationMember, OrganizationMember> implements IOrganizationMember {
@plugins.smartdata.unI()
public id: string = '';

View File

@@ -8,7 +8,7 @@ import type {
IOrganizationSettings,
TOrganizationPlan,
} from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
const DEFAULT_SETTINGS: IOrganizationSettings = {
requireMfa: false,
@@ -17,11 +17,8 @@ const DEFAULT_SETTINGS: IOrganizationSettings = {
allowedProtocols: ['oci', 'npm', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'],
};
@plugins.smartdata.Collection(() => getDb())
export class Organization
extends plugins.smartdata.SmartDataDbDoc<Organization, Organization>
implements IOrganization
{
@plugins.smartdata.Collection(() => db)
export class Organization extends plugins.smartdata.SmartDataDbDoc<Organization, Organization> implements IOrganization {
@plugins.smartdata.unI()
public id: string = '';

View File

@@ -9,9 +9,9 @@ import type {
IProtocolMetadata,
} from '../interfaces/package.interfaces.ts';
import type { TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
@plugins.smartdata.Collection(() => db)
export class Package extends plugins.smartdata.SmartDataDbDoc<Package, Package> implements IPackage {
@plugins.smartdata.unI()
public id: string = ''; // {protocol}:{org}:{name}

View File

@@ -4,13 +4,10 @@
import * as plugins from '../plugins.ts';
import type { IRepositoryPermission, TRepositoryRole } from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
export class RepositoryPermission
extends plugins.smartdata.SmartDataDbDoc<RepositoryPermission, RepositoryPermission>
implements IRepositoryPermission
{
@plugins.smartdata.Collection(() => db)
export class RepositoryPermission extends plugins.smartdata.SmartDataDbDoc<RepositoryPermission, RepositoryPermission> implements IRepositoryPermission {
@plugins.smartdata.unI()
public id: string = '';

View File

@@ -4,13 +4,10 @@
import * as plugins from '../plugins.ts';
import type { IRepository, TRepositoryVisibility, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
export class Repository
extends plugins.smartdata.SmartDataDbDoc<Repository, Repository>
implements IRepository
{
@plugins.smartdata.Collection(() => db)
export class Repository extends plugins.smartdata.SmartDataDbDoc<Repository, Repository> implements IRepository {
@plugins.smartdata.unI()
public id: string = '';

View File

@@ -4,13 +4,10 @@
import * as plugins from '../plugins.ts';
import type { ISession } from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
export class Session
extends plugins.smartdata.SmartDataDbDoc<Session, Session>
implements ISession
{
@plugins.smartdata.Collection(() => db)
export class Session extends plugins.smartdata.SmartDataDbDoc<Session, Session> implements ISession {
@plugins.smartdata.unI()
public id: string = '';

View File

@@ -4,13 +4,10 @@
import * as plugins from '../plugins.ts';
import type { ITeamMember, TTeamRole } from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
export class TeamMember
extends plugins.smartdata.SmartDataDbDoc<TeamMember, TeamMember>
implements ITeamMember
{
@plugins.smartdata.Collection(() => db)
export class TeamMember extends plugins.smartdata.SmartDataDbDoc<TeamMember, TeamMember> implements ITeamMember {
@plugins.smartdata.unI()
public id: string = '';

View File

@@ -4,9 +4,9 @@
import * as plugins from '../plugins.ts';
import type { ITeam } from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
@plugins.smartdata.Collection(() => db)
export class Team extends plugins.smartdata.SmartDataDbDoc<Team, Team> implements ITeam {
@plugins.smartdata.unI()
public id: string = '';

View File

@@ -4,9 +4,9 @@
import * as plugins from '../plugins.ts';
import type { IUser, TUserStatus } from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
@plugins.smartdata.Collection(() => db)
export class User extends plugins.smartdata.SmartDataDbDoc<User, User> implements IUser {
@plugins.smartdata.unI()
public id: string = '';
@@ -112,4 +112,119 @@ export class User extends plugins.smartdata.SmartDataDbDoc<User, User> implement
this.id = await User.getNewId();
}
}
/**
* Check if user is active (status === 'active')
*/
public get isActive(): boolean {
return this.status === 'active';
}
/**
* Alias for isPlatformAdmin for backward compatibility
*/
public get isSystemAdmin(): boolean {
return this.isPlatformAdmin;
}
/**
* Find user by ID
*/
public static async findById(id: string): Promise<User | null> {
return await User.getInstance({ id });
}
/**
* Verify password against stored hash
*/
public async verifyPassword(password: string): Promise<boolean> {
if (!this.passwordHash) return false;
const [saltHex, expectedHash] = this.passwordHash.split(':');
if (!saltHex || !expectedHash) return false;
const encoder = new TextEncoder();
const data = encoder.encode(saltHex + password);
let hash = data;
for (let i = 0; i < 10000; i++) {
hash = new Uint8Array(await crypto.subtle.digest('SHA-256', hash));
}
const hashHex = Array.from(hash)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return hashHex === expectedHash;
}
/**
* Hash a password for storage
*/
public static async hashPassword(password: string): Promise<string> {
const salt = crypto.getRandomValues(new Uint8Array(16));
const saltHex = Array.from(salt)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
const encoder = new TextEncoder();
const data = encoder.encode(saltHex + password);
let hash = data;
for (let i = 0; i < 10000; i++) {
hash = new Uint8Array(await crypto.subtle.digest('SHA-256', hash));
}
const hashHex = Array.from(hash)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return `${saltHex}:${hashHex}`;
}
/**
* Create the default admin user if no admin exists
*/
public static async seedDefaultAdmin(): Promise<User | null> {
const adminEmail = Deno.env.get('ADMIN_EMAIL') || 'admin@stack.gallery';
const adminPassword = Deno.env.get('ADMIN_PASSWORD') || 'admin';
// Check if any platform admin exists
const existingAdmin = await User.getInstance({ isPlatformAdmin: true });
if (existingAdmin) {
console.log('[User] Platform admin already exists, skipping seed');
return null;
}
// Check if admin email already exists
const existingUser = await User.findByEmail(adminEmail);
if (existingUser) {
console.log('[User] User with admin email already exists, promoting to admin');
existingUser.isPlatformAdmin = true;
existingUser.status = 'active';
await existingUser.save();
return existingUser;
}
// Create new admin user
console.log('[User] Creating default admin user:', adminEmail);
const passwordHash = await User.hashPassword(adminPassword);
const admin = new User();
admin.id = await User.getNewId();
admin.email = adminEmail.toLowerCase();
admin.username = 'admin';
admin.passwordHash = passwordHash;
admin.displayName = 'System Administrator';
admin.status = 'active';
admin.emailVerified = true;
admin.isPlatformAdmin = true;
admin.createdAt = new Date();
admin.updatedAt = new Date();
await admin.save();
console.log('[User] Default admin user created successfully');
return admin;
}
}