/** * Organization model for Stack.Gallery Registry */ import * as plugins from '../plugins.ts'; import type { IOrganization, IOrganizationSettings, TOrganizationPlan, } from '../interfaces/auth.interfaces.ts'; import { getDb } from './db.ts'; const DEFAULT_SETTINGS: IOrganizationSettings = { requireMfa: false, allowPublicRepositories: true, defaultRepositoryVisibility: 'private', allowedProtocols: ['oci', 'npm', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'], }; @plugins.smartdata.Collection(() => getDb()) export class Organization extends plugins.smartdata.SmartDataDbDoc implements IOrganization { @plugins.smartdata.unI() public id: string = ''; @plugins.smartdata.svDb() @plugins.smartdata.searchable() @plugins.smartdata.index({ unique: true }) public name: string = ''; // URL-safe slug @plugins.smartdata.svDb() @plugins.smartdata.searchable() public displayName: string = ''; @plugins.smartdata.svDb() public description?: string; @plugins.smartdata.svDb() public avatarUrl?: string; @plugins.smartdata.svDb() @plugins.smartdata.index() public plan: TOrganizationPlan = 'free'; @plugins.smartdata.svDb() public settings: IOrganizationSettings = DEFAULT_SETTINGS; @plugins.smartdata.svDb() public billingEmail?: string; @plugins.smartdata.svDb() public isVerified: boolean = false; @plugins.smartdata.svDb() public verifiedDomains: string[] = []; @plugins.smartdata.svDb() public storageQuotaBytes: number = 5 * 1024 * 1024 * 1024; // 5GB default @plugins.smartdata.svDb() public usedStorageBytes: number = 0; @plugins.smartdata.svDb() @plugins.smartdata.index() public createdAt: Date = new Date(); @plugins.smartdata.svDb() public updatedAt: Date = new Date(); @plugins.smartdata.svDb() @plugins.smartdata.index() public createdById: string = ''; /** * Create a new organization */ public static async createOrganization(data: { name: string; displayName: string; description?: string; createdById: string; }): Promise { // Validate name (URL-safe) const nameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/; if (!nameRegex.test(data.name)) { throw new Error( 'Organization name must be lowercase alphanumeric with optional hyphens' ); } const org = new Organization(); org.id = await Organization.getNewId(); org.name = data.name.toLowerCase(); org.displayName = data.displayName; org.description = data.description; org.createdById = data.createdById; org.settings = { ...DEFAULT_SETTINGS }; org.createdAt = new Date(); org.updatedAt = new Date(); await org.save(); return org; } /** * Find organization by name (slug) */ public static async findByName(name: string): Promise { return await Organization.getInstance({ name: name.toLowerCase() }); } /** * Check if storage quota is exceeded */ public hasStorageAvailable(additionalBytes: number): boolean { if (this.storageQuotaBytes < 0) return true; // Unlimited return this.usedStorageBytes + additionalBytes <= this.storageQuotaBytes; } /** * Update storage usage */ public async updateStorageUsage(deltaBytes: number): Promise { this.usedStorageBytes = Math.max(0, this.usedStorageBytes + deltaBytes); await this.save(); } /** * Lifecycle hook: Update timestamps before save */ public async beforeSave(): Promise { this.updatedAt = new Date(); if (!this.id) { this.id = await Organization.getNewId(); } } }