feat: implement account settings and API tokens management
- Added SettingsComponent for user profile management, including display name and password change functionality. - Introduced TokensComponent for managing API tokens, including creation and revocation. - Created LayoutComponent for consistent application layout with navigation and user information. - Established main application structure in index.html and main.ts. - Integrated Tailwind CSS for styling and responsive design. - Configured TypeScript settings for strict type checking and module resolution.
This commit is contained in:
167
ts/models/apitoken.ts
Normal file
167
ts/models/apitoken.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* ApiToken model for Stack.Gallery Registry
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type { IApiToken, ITokenScope, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
|
||||
import { getDb } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class ApiToken
|
||||
extends plugins.smartdata.SmartDataDbDoc<ApiToken, ApiToken>
|
||||
implements IApiToken
|
||||
{
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public userId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public name: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index({ unique: true })
|
||||
public tokenHash: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public tokenPrefix: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public protocols: TRegistryProtocol[] = [];
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public scopes: ITokenScope[] = [];
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public expiresAt?: Date;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastUsedAt?: Date;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastUsedIp?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public usageCount: number = 0;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public isRevoked: boolean = false;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public revokedAt?: Date;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public revokedReason?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public createdIp?: string;
|
||||
|
||||
/**
|
||||
* Find token by hash
|
||||
*/
|
||||
public static async findByHash(tokenHash: string): Promise<ApiToken | null> {
|
||||
return await ApiToken.getInstance({
|
||||
tokenHash,
|
||||
isRevoked: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find token by prefix (for listing)
|
||||
*/
|
||||
public static async findByPrefix(tokenPrefix: string): Promise<ApiToken | null> {
|
||||
return await ApiToken.getInstance({
|
||||
tokenPrefix,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tokens for a user
|
||||
*/
|
||||
public static async getUserTokens(userId: string): Promise<ApiToken[]> {
|
||||
return await ApiToken.getInstances({
|
||||
userId,
|
||||
isRevoked: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is valid (not expired, not revoked)
|
||||
*/
|
||||
public isValid(): boolean {
|
||||
if (this.isRevoked) return false;
|
||||
if (this.expiresAt && this.expiresAt < new Date()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record token usage
|
||||
*/
|
||||
public async recordUsage(ip?: string): Promise<void> {
|
||||
this.lastUsedAt = new Date();
|
||||
this.lastUsedIp = ip;
|
||||
this.usageCount += 1;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke token
|
||||
*/
|
||||
public async revoke(reason?: string): Promise<void> {
|
||||
this.isRevoked = true;
|
||||
this.revokedAt = new Date();
|
||||
this.revokedReason = reason;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token has permission for protocol
|
||||
*/
|
||||
public hasProtocol(protocol: TRegistryProtocol): boolean {
|
||||
return this.protocols.includes(protocol) || this.protocols.includes('*' as TRegistryProtocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token has permission for action on resource
|
||||
*/
|
||||
public hasScope(
|
||||
protocol: TRegistryProtocol,
|
||||
organizationId?: string,
|
||||
repositoryId?: string,
|
||||
action?: string
|
||||
): boolean {
|
||||
for (const scope of this.scopes) {
|
||||
// Check protocol
|
||||
if (scope.protocol !== '*' && scope.protocol !== protocol) continue;
|
||||
|
||||
// Check organization
|
||||
if (scope.organizationId && scope.organizationId !== organizationId) continue;
|
||||
|
||||
// Check repository
|
||||
if (scope.repositoryId && scope.repositoryId !== repositoryId) continue;
|
||||
|
||||
// Check action
|
||||
if (action && !scope.actions.includes('*') && !scope.actions.includes(action as never)) continue;
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
if (!this.id) {
|
||||
this.id = await ApiToken.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
171
ts/models/auditlog.ts
Normal file
171
ts/models/auditlog.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* AuditLog model for Stack.Gallery Registry
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type { IAuditLog, TAuditAction, TAuditResourceType } from '../interfaces/audit.interfaces.ts';
|
||||
import { getDb } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class AuditLog
|
||||
extends plugins.smartdata.SmartDataDbDoc<AuditLog, AuditLog>
|
||||
implements IAuditLog
|
||||
{
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public actorId?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public actorType: 'user' | 'api_token' | 'system' | 'anonymous' = 'anonymous';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public actorTokenId?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public actorIp?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public actorUserAgent?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public action: TAuditAction = 'USER_CREATED';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public resourceType: TAuditResourceType = 'user';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public resourceId?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public resourceName?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public organizationId?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public repositoryId?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public metadata: Record<string, unknown> = {};
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public success: boolean = true;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public errorCode?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public errorMessage?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public durationMs?: number;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public timestamp: Date = new Date();
|
||||
|
||||
/**
|
||||
* Create an audit log entry
|
||||
*/
|
||||
public static async log(data: {
|
||||
actorId?: string;
|
||||
actorType?: 'user' | 'api_token' | 'system' | 'anonymous';
|
||||
actorTokenId?: string;
|
||||
actorIp?: string;
|
||||
actorUserAgent?: string;
|
||||
action: TAuditAction;
|
||||
resourceType: TAuditResourceType;
|
||||
resourceId?: string;
|
||||
resourceName?: string;
|
||||
organizationId?: string;
|
||||
repositoryId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
success?: boolean;
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
durationMs?: number;
|
||||
}): Promise<AuditLog> {
|
||||
const log = new AuditLog();
|
||||
log.id = await AuditLog.getNewId();
|
||||
log.actorId = data.actorId;
|
||||
log.actorType = data.actorType || (data.actorId ? 'user' : 'anonymous');
|
||||
log.actorTokenId = data.actorTokenId;
|
||||
log.actorIp = data.actorIp;
|
||||
log.actorUserAgent = data.actorUserAgent;
|
||||
log.action = data.action;
|
||||
log.resourceType = data.resourceType;
|
||||
log.resourceId = data.resourceId;
|
||||
log.resourceName = data.resourceName;
|
||||
log.organizationId = data.organizationId;
|
||||
log.repositoryId = data.repositoryId;
|
||||
log.metadata = data.metadata || {};
|
||||
log.success = data.success ?? true;
|
||||
log.errorCode = data.errorCode;
|
||||
log.errorMessage = data.errorMessage;
|
||||
log.durationMs = data.durationMs;
|
||||
log.timestamp = new Date();
|
||||
await log.save();
|
||||
return log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query audit logs with filters
|
||||
*/
|
||||
public static async query(filters: {
|
||||
actorId?: string;
|
||||
organizationId?: string;
|
||||
repositoryId?: string;
|
||||
resourceType?: TAuditResourceType;
|
||||
action?: TAuditAction[];
|
||||
success?: boolean;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}): Promise<{ logs: AuditLog[]; total: number }> {
|
||||
const query: Record<string, unknown> = {};
|
||||
|
||||
if (filters.actorId) query.actorId = filters.actorId;
|
||||
if (filters.organizationId) query.organizationId = filters.organizationId;
|
||||
if (filters.repositoryId) query.repositoryId = filters.repositoryId;
|
||||
if (filters.resourceType) query.resourceType = filters.resourceType;
|
||||
if (filters.action) query.action = { $in: filters.action };
|
||||
if (filters.success !== undefined) query.success = filters.success;
|
||||
|
||||
if (filters.startDate || filters.endDate) {
|
||||
query.timestamp = {};
|
||||
if (filters.startDate) (query.timestamp as Record<string, unknown>).$gte = filters.startDate;
|
||||
if (filters.endDate) (query.timestamp as Record<string, unknown>).$lte = filters.endDate;
|
||||
}
|
||||
|
||||
// Get total count
|
||||
const allLogs = await AuditLog.getInstances(query);
|
||||
const total = allLogs.length;
|
||||
|
||||
// Apply pagination
|
||||
const offset = filters.offset || 0;
|
||||
const limit = filters.limit || 100;
|
||||
const logs = allLogs.slice(offset, offset + limit);
|
||||
|
||||
return { logs, total };
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
if (!this.id) {
|
||||
this.id = await AuditLog.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
ts/models/db.ts
Normal file
57
ts/models/db.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Database connection singleton
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
|
||||
let dbInstance: plugins.smartdata.SmartdataDb | null = null;
|
||||
|
||||
/**
|
||||
* Initialize database connection
|
||||
*/
|
||||
export async function initDb(config: {
|
||||
mongoDbUrl: string;
|
||||
mongoDbName?: string;
|
||||
}): Promise<plugins.smartdata.SmartdataDb> {
|
||||
if (dbInstance) {
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
dbInstance = new plugins.smartdata.SmartdataDb({
|
||||
mongoDbUrl: config.mongoDbUrl,
|
||||
mongoDbName: config.mongoDbName || 'stackregistry',
|
||||
});
|
||||
|
||||
await dbInstance.init();
|
||||
console.log('Database connected successfully');
|
||||
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database instance (must call initDb first)
|
||||
*/
|
||||
export function getDb(): plugins.smartdata.SmartdataDb {
|
||||
if (!dbInstance) {
|
||||
throw new Error('Database not initialized. Call initDb() first.');
|
||||
}
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*/
|
||||
export async function closeDb(): Promise<void> {
|
||||
if (dbInstance) {
|
||||
await dbInstance.close();
|
||||
dbInstance = null;
|
||||
console.log('Database connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database is connected
|
||||
*/
|
||||
export function isDbConnected(): boolean {
|
||||
return dbInstance !== null;
|
||||
}
|
||||
16
ts/models/index.ts
Normal file
16
ts/models/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Model exports
|
||||
*/
|
||||
|
||||
export { initDb, getDb, closeDb, isDbConnected } from './db.ts';
|
||||
export { User } from './user.ts';
|
||||
export { Organization } from './organization.ts';
|
||||
export { OrganizationMember } from './organization.member.ts';
|
||||
export { Team } from './team.ts';
|
||||
export { TeamMember } from './team.member.ts';
|
||||
export { Repository } from './repository.ts';
|
||||
export { RepositoryPermission } from './repository.permission.ts';
|
||||
export { Package } from './package.ts';
|
||||
export { ApiToken } from './apitoken.ts';
|
||||
export { Session } from './session.ts';
|
||||
export { AuditLog } from './auditlog.ts';
|
||||
109
ts/models/organization.member.ts
Normal file
109
ts/models/organization.member.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* OrganizationMember model - links users to organizations with roles
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type { IOrganizationMember, TOrganizationRole } from '../interfaces/auth.interfaces.ts';
|
||||
import { getDb } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class OrganizationMember
|
||||
extends plugins.smartdata.SmartDataDbDoc<OrganizationMember, OrganizationMember>
|
||||
implements IOrganizationMember
|
||||
{
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public organizationId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public userId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public role: TOrganizationRole = 'member';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public invitedBy?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public joinedAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
/**
|
||||
* Add a member to an organization
|
||||
*/
|
||||
public static async addMember(data: {
|
||||
organizationId: string;
|
||||
userId: string;
|
||||
role: TOrganizationRole;
|
||||
invitedBy?: string;
|
||||
}): Promise<OrganizationMember> {
|
||||
// Check if member already exists
|
||||
const existing = await OrganizationMember.getInstance({
|
||||
organizationId: data.organizationId,
|
||||
userId: data.userId,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new Error('User is already a member of this organization');
|
||||
}
|
||||
|
||||
const member = new OrganizationMember();
|
||||
member.id = await OrganizationMember.getNewId();
|
||||
member.organizationId = data.organizationId;
|
||||
member.userId = data.userId;
|
||||
member.role = data.role;
|
||||
member.invitedBy = data.invitedBy;
|
||||
member.joinedAt = new Date();
|
||||
member.createdAt = new Date();
|
||||
await member.save();
|
||||
return member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find membership for user in organization
|
||||
*/
|
||||
public static async findMembership(
|
||||
organizationId: string,
|
||||
userId: string
|
||||
): Promise<OrganizationMember | null> {
|
||||
return await OrganizationMember.getInstance({
|
||||
organizationId,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all members of an organization
|
||||
*/
|
||||
public static async getOrgMembers(organizationId: string): Promise<OrganizationMember[]> {
|
||||
return await OrganizationMember.getInstances({
|
||||
organizationId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all organizations a user belongs to
|
||||
*/
|
||||
public static async getUserOrganizations(userId: string): Promise<OrganizationMember[]> {
|
||||
return await OrganizationMember.getInstances({
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
if (!this.id) {
|
||||
this.id = await OrganizationMember.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
138
ts/models/organization.ts
Normal file
138
ts/models/organization.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 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<Organization, Organization>
|
||||
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<Organization> {
|
||||
// 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<Organization | null> {
|
||||
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<void> {
|
||||
this.usedStorageBytes = Math.max(0, this.usedStorageBytes + deltaBytes);
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook: Update timestamps before save
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
this.updatedAt = new Date();
|
||||
if (!this.id) {
|
||||
this.id = await Organization.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
195
ts/models/package.ts
Normal file
195
ts/models/package.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Package model for Stack.Gallery Registry
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type {
|
||||
IPackage,
|
||||
IPackageVersion,
|
||||
IProtocolMetadata,
|
||||
} from '../interfaces/package.interfaces.ts';
|
||||
import type { TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
|
||||
import { getDb } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class Package extends plugins.smartdata.SmartDataDbDoc<Package, Package> implements IPackage {
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = ''; // {protocol}:{org}:{name}
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public organizationId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public repositoryId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public protocol: TRegistryProtocol = 'npm';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.searchable()
|
||||
@plugins.smartdata.index()
|
||||
public name: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.searchable()
|
||||
public description?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public versions: Record<string, IPackageVersion> = {};
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public distTags: Record<string, string> = {}; // e.g., { latest: "1.0.0" }
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public metadata: IProtocolMetadata = { type: 'npm' };
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public isPrivate: boolean = true;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public storageBytes: number = 0;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public downloadCount: number = 0;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public starCount: number = 0;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public cacheExpiresAt?: Date;
|
||||
|
||||
@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 = '';
|
||||
|
||||
/**
|
||||
* Generate package ID
|
||||
*/
|
||||
public static generateId(protocol: TRegistryProtocol, orgName: string, name: string): string {
|
||||
return `${protocol}:${orgName}:${name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find package by ID
|
||||
*/
|
||||
public static async findById(id: string): Promise<Package | null> {
|
||||
return await Package.getInstance({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find package by protocol, org, and name
|
||||
*/
|
||||
public static async findByName(
|
||||
protocol: TRegistryProtocol,
|
||||
orgName: string,
|
||||
name: string
|
||||
): Promise<Package | null> {
|
||||
const id = Package.generateId(protocol, orgName, name);
|
||||
return await Package.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get packages in an organization
|
||||
*/
|
||||
public static async getOrgPackages(organizationId: string): Promise<Package[]> {
|
||||
return await Package.getInstances({ organizationId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Search packages
|
||||
*/
|
||||
public static async search(
|
||||
query: string,
|
||||
options?: {
|
||||
protocol?: TRegistryProtocol;
|
||||
organizationId?: string;
|
||||
isPrivate?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
): Promise<Package[]> {
|
||||
const filter: Record<string, unknown> = {};
|
||||
if (options?.protocol) filter.protocol = options.protocol;
|
||||
if (options?.organizationId) filter.organizationId = options.organizationId;
|
||||
if (options?.isPrivate !== undefined) filter.isPrivate = options.isPrivate;
|
||||
|
||||
// Simple text search - in production, would use MongoDB text index
|
||||
const allPackages = await Package.getInstances(filter);
|
||||
|
||||
// Filter by query
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const filtered = allPackages.filter(
|
||||
(pkg) =>
|
||||
pkg.name.toLowerCase().includes(lowerQuery) ||
|
||||
pkg.description?.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
|
||||
// Apply pagination
|
||||
const offset = options?.offset || 0;
|
||||
const limit = options?.limit || 50;
|
||||
return filtered.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new version
|
||||
*/
|
||||
public addVersion(version: IPackageVersion): void {
|
||||
this.versions[version.version] = version;
|
||||
this.storageBytes += version.size;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific version
|
||||
*/
|
||||
public getVersion(version: string): IPackageVersion | undefined {
|
||||
return this.versions[version];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest version
|
||||
*/
|
||||
public getLatestVersion(): IPackageVersion | undefined {
|
||||
const latest = this.distTags['latest'];
|
||||
if (latest) {
|
||||
return this.versions[latest];
|
||||
}
|
||||
// Fallback to most recent
|
||||
const versionList = Object.keys(this.versions);
|
||||
if (versionList.length === 0) return undefined;
|
||||
return this.versions[versionList[versionList.length - 1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment download count
|
||||
*/
|
||||
public async incrementDownloads(version?: string): Promise<void> {
|
||||
this.downloadCount += 1;
|
||||
if (version && this.versions[version]) {
|
||||
this.versions[version].downloads += 1;
|
||||
}
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
this.updatedAt = new Date();
|
||||
if (!this.id) {
|
||||
this.id = await Package.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
162
ts/models/repository.permission.ts
Normal file
162
ts/models/repository.permission.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* RepositoryPermission model - grants access to repositories
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type { IRepositoryPermission, TRepositoryRole } from '../interfaces/auth.interfaces.ts';
|
||||
import { getDb } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class RepositoryPermission
|
||||
extends plugins.smartdata.SmartDataDbDoc<RepositoryPermission, RepositoryPermission>
|
||||
implements IRepositoryPermission
|
||||
{
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public repositoryId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public teamId?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public userId?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public role: TRepositoryRole = 'reader';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public grantedById: string = '';
|
||||
|
||||
/**
|
||||
* Grant permission to a user
|
||||
*/
|
||||
public static async grantToUser(data: {
|
||||
repositoryId: string;
|
||||
userId: string;
|
||||
role: TRepositoryRole;
|
||||
grantedById: string;
|
||||
}): Promise<RepositoryPermission> {
|
||||
// Check for existing permission
|
||||
const existing = await RepositoryPermission.getInstance({
|
||||
repositoryId: data.repositoryId,
|
||||
userId: data.userId,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Update existing permission
|
||||
existing.role = data.role;
|
||||
await existing.save();
|
||||
return existing;
|
||||
}
|
||||
|
||||
const perm = new RepositoryPermission();
|
||||
perm.id = await RepositoryPermission.getNewId();
|
||||
perm.repositoryId = data.repositoryId;
|
||||
perm.userId = data.userId;
|
||||
perm.role = data.role;
|
||||
perm.grantedById = data.grantedById;
|
||||
perm.createdAt = new Date();
|
||||
await perm.save();
|
||||
return perm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant permission to a team
|
||||
*/
|
||||
public static async grantToTeam(data: {
|
||||
repositoryId: string;
|
||||
teamId: string;
|
||||
role: TRepositoryRole;
|
||||
grantedById: string;
|
||||
}): Promise<RepositoryPermission> {
|
||||
// Check for existing permission
|
||||
const existing = await RepositoryPermission.getInstance({
|
||||
repositoryId: data.repositoryId,
|
||||
teamId: data.teamId,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Update existing permission
|
||||
existing.role = data.role;
|
||||
await existing.save();
|
||||
return existing;
|
||||
}
|
||||
|
||||
const perm = new RepositoryPermission();
|
||||
perm.id = await RepositoryPermission.getNewId();
|
||||
perm.repositoryId = data.repositoryId;
|
||||
perm.teamId = data.teamId;
|
||||
perm.role = data.role;
|
||||
perm.grantedById = data.grantedById;
|
||||
perm.createdAt = new Date();
|
||||
await perm.save();
|
||||
return perm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's direct permission on repository
|
||||
*/
|
||||
public static async getUserPermission(
|
||||
repositoryId: string,
|
||||
userId: string
|
||||
): Promise<RepositoryPermission | null> {
|
||||
return await RepositoryPermission.getInstance({
|
||||
repositoryId,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get team's permission on repository
|
||||
*/
|
||||
public static async getTeamPermission(
|
||||
repositoryId: string,
|
||||
teamId: string
|
||||
): Promise<RepositoryPermission | null> {
|
||||
return await RepositoryPermission.getInstance({
|
||||
repositoryId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions for a repository
|
||||
*/
|
||||
public static async getRepoPermissions(repositoryId: string): Promise<RepositoryPermission[]> {
|
||||
return await RepositoryPermission.getInstances({
|
||||
repositoryId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions for user's teams on a repository
|
||||
*/
|
||||
public static async getTeamPermissionsForRepo(
|
||||
repositoryId: string,
|
||||
teamIds: string[]
|
||||
): Promise<RepositoryPermission[]> {
|
||||
if (teamIds.length === 0) return [];
|
||||
return await RepositoryPermission.getInstances({
|
||||
repositoryId,
|
||||
teamId: { $in: teamIds } as unknown as string,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
if (!this.id) {
|
||||
this.id = await RepositoryPermission.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
158
ts/models/repository.ts
Normal file
158
ts/models/repository.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Repository model for Stack.Gallery Registry
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type { IRepository, TRepositoryVisibility, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
|
||||
import { getDb } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class Repository
|
||||
extends plugins.smartdata.SmartDataDbDoc<Repository, Repository>
|
||||
implements IRepository
|
||||
{
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public organizationId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.searchable()
|
||||
public name: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public description?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public protocol: TRegistryProtocol = 'npm';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public visibility: TRepositoryVisibility = 'private';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public storageNamespace: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public downloadCount: number = 0;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public starCount: 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 repository
|
||||
*/
|
||||
public static async createRepository(data: {
|
||||
organizationId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
protocol: TRegistryProtocol;
|
||||
visibility?: TRepositoryVisibility;
|
||||
createdById: string;
|
||||
}): Promise<Repository> {
|
||||
// Validate name
|
||||
const nameRegex = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/;
|
||||
if (!nameRegex.test(data.name.toLowerCase())) {
|
||||
throw new Error('Repository name must be lowercase alphanumeric with optional dots, hyphens, or underscores');
|
||||
}
|
||||
|
||||
// Check for duplicate name in org + protocol
|
||||
const existing = await Repository.getInstance({
|
||||
organizationId: data.organizationId,
|
||||
name: data.name.toLowerCase(),
|
||||
protocol: data.protocol,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new Error('Repository with this name and protocol already exists');
|
||||
}
|
||||
|
||||
const repo = new Repository();
|
||||
repo.id = await Repository.getNewId();
|
||||
repo.organizationId = data.organizationId;
|
||||
repo.name = data.name.toLowerCase();
|
||||
repo.description = data.description;
|
||||
repo.protocol = data.protocol;
|
||||
repo.visibility = data.visibility || 'private';
|
||||
repo.storageNamespace = `${data.protocol}/${data.organizationId}/${data.name.toLowerCase()}`;
|
||||
repo.createdById = data.createdById;
|
||||
repo.createdAt = new Date();
|
||||
repo.updatedAt = new Date();
|
||||
await repo.save();
|
||||
return repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find repository by org, name, and protocol
|
||||
*/
|
||||
public static async findByName(
|
||||
organizationId: string,
|
||||
name: string,
|
||||
protocol: TRegistryProtocol
|
||||
): Promise<Repository | null> {
|
||||
return await Repository.getInstance({
|
||||
organizationId,
|
||||
name: name.toLowerCase(),
|
||||
protocol,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all repositories in an organization
|
||||
*/
|
||||
public static async getOrgRepositories(organizationId: string): Promise<Repository[]> {
|
||||
return await Repository.getInstances({
|
||||
organizationId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all public repositories
|
||||
*/
|
||||
public static async getPublicRepositories(protocol?: TRegistryProtocol): Promise<Repository[]> {
|
||||
const query: Record<string, unknown> = { visibility: 'public' };
|
||||
if (protocol) {
|
||||
query.protocol = protocol;
|
||||
}
|
||||
return await Repository.getInstances(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment download count
|
||||
*/
|
||||
public async incrementDownloads(): Promise<void> {
|
||||
this.downloadCount += 1;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full path (org/repo)
|
||||
*/
|
||||
public getFullPath(orgName: string): string {
|
||||
return `${orgName}/${this.name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
this.updatedAt = new Date();
|
||||
if (!this.id) {
|
||||
this.id = await Repository.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
135
ts/models/session.ts
Normal file
135
ts/models/session.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Session model for Stack.Gallery Registry
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type { ISession } from '../interfaces/auth.interfaces.ts';
|
||||
import { getDb } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class Session
|
||||
extends plugins.smartdata.SmartDataDbDoc<Session, Session>
|
||||
implements ISession
|
||||
{
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public userId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public userAgent: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public ipAddress: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public isValid: boolean = true;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public invalidatedAt?: Date;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public invalidatedReason?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastActivityAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
/**
|
||||
* Create a new session
|
||||
*/
|
||||
public static async createSession(data: {
|
||||
userId: string;
|
||||
userAgent: string;
|
||||
ipAddress: string;
|
||||
}): Promise<Session> {
|
||||
const session = new Session();
|
||||
session.id = await Session.getNewId();
|
||||
session.userId = data.userId;
|
||||
session.userAgent = data.userAgent;
|
||||
session.ipAddress = data.ipAddress;
|
||||
session.isValid = true;
|
||||
session.lastActivityAt = new Date();
|
||||
session.createdAt = new Date();
|
||||
await session.save();
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find valid session by ID
|
||||
*/
|
||||
public static async findValidSession(sessionId: string): Promise<Session | null> {
|
||||
const session = await Session.getInstance({
|
||||
id: sessionId,
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
// Check if session is expired (7 days)
|
||||
const maxAge = 7 * 24 * 60 * 60 * 1000;
|
||||
if (Date.now() - session.createdAt.getTime() > maxAge) {
|
||||
await session.invalidate('expired');
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all valid sessions for a user
|
||||
*/
|
||||
public static async getUserSessions(userId: string): Promise<Session[]> {
|
||||
return await Session.getInstances({
|
||||
userId,
|
||||
isValid: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all sessions for a user
|
||||
*/
|
||||
public static async invalidateAllUserSessions(
|
||||
userId: string,
|
||||
reason: string = 'logout_all'
|
||||
): Promise<number> {
|
||||
const sessions = await Session.getUserSessions(userId);
|
||||
for (const session of sessions) {
|
||||
await session.invalidate(reason);
|
||||
}
|
||||
return sessions.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate this session
|
||||
*/
|
||||
public async invalidate(reason: string): Promise<void> {
|
||||
this.isValid = false;
|
||||
this.invalidatedAt = new Date();
|
||||
this.invalidatedReason = reason;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last activity
|
||||
*/
|
||||
public async touchActivity(): Promise<void> {
|
||||
this.lastActivityAt = new Date();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
if (!this.id) {
|
||||
this.id = await Session.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
97
ts/models/team.member.ts
Normal file
97
ts/models/team.member.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* TeamMember model - links users to teams with roles
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type { ITeamMember, TTeamRole } from '../interfaces/auth.interfaces.ts';
|
||||
import { getDb } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class TeamMember
|
||||
extends plugins.smartdata.SmartDataDbDoc<TeamMember, TeamMember>
|
||||
implements ITeamMember
|
||||
{
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public teamId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public userId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public role: TTeamRole = 'member';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
/**
|
||||
* Add a member to a team
|
||||
*/
|
||||
public static async addMember(data: {
|
||||
teamId: string;
|
||||
userId: string;
|
||||
role: TTeamRole;
|
||||
}): Promise<TeamMember> {
|
||||
// Check if member already exists
|
||||
const existing = await TeamMember.getInstance({
|
||||
teamId: data.teamId,
|
||||
userId: data.userId,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new Error('User is already a member of this team');
|
||||
}
|
||||
|
||||
const member = new TeamMember();
|
||||
member.id = await TeamMember.getNewId();
|
||||
member.teamId = data.teamId;
|
||||
member.userId = data.userId;
|
||||
member.role = data.role;
|
||||
member.createdAt = new Date();
|
||||
await member.save();
|
||||
return member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find membership for user in team
|
||||
*/
|
||||
public static async findMembership(teamId: string, userId: string): Promise<TeamMember | null> {
|
||||
return await TeamMember.getInstance({
|
||||
teamId,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all members of a team
|
||||
*/
|
||||
public static async getTeamMembers(teamId: string): Promise<TeamMember[]> {
|
||||
return await TeamMember.getInstances({
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all teams a user belongs to
|
||||
*/
|
||||
public static async getUserTeams(userId: string): Promise<TeamMember[]> {
|
||||
return await TeamMember.getInstances({
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
if (!this.id) {
|
||||
this.id = await TeamMember.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
100
ts/models/team.ts
Normal file
100
ts/models/team.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Team model for Stack.Gallery Registry
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type { ITeam } from '../interfaces/auth.interfaces.ts';
|
||||
import { getDb } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class Team extends plugins.smartdata.SmartDataDbDoc<Team, Team> implements ITeam {
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public organizationId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.searchable()
|
||||
public name: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public description?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public isDefaultTeam: boolean = false;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public updatedAt: Date = new Date();
|
||||
|
||||
/**
|
||||
* Create a new team
|
||||
*/
|
||||
public static async createTeam(data: {
|
||||
organizationId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
isDefaultTeam?: boolean;
|
||||
}): Promise<Team> {
|
||||
// Validate name
|
||||
const nameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
||||
if (!nameRegex.test(data.name.toLowerCase())) {
|
||||
throw new Error('Team name must be lowercase alphanumeric with optional hyphens');
|
||||
}
|
||||
|
||||
// Check for duplicate name in org
|
||||
const existing = await Team.getInstance({
|
||||
organizationId: data.organizationId,
|
||||
name: data.name.toLowerCase(),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new Error('Team with this name already exists in the organization');
|
||||
}
|
||||
|
||||
const team = new Team();
|
||||
team.id = await Team.getNewId();
|
||||
team.organizationId = data.organizationId;
|
||||
team.name = data.name.toLowerCase();
|
||||
team.description = data.description;
|
||||
team.isDefaultTeam = data.isDefaultTeam || false;
|
||||
team.createdAt = new Date();
|
||||
team.updatedAt = new Date();
|
||||
await team.save();
|
||||
return team;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find team by name in organization
|
||||
*/
|
||||
public static async findByName(organizationId: string, name: string): Promise<Team | null> {
|
||||
return await Team.getInstance({
|
||||
organizationId,
|
||||
name: name.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all teams in an organization
|
||||
*/
|
||||
public static async getOrgTeams(organizationId: string): Promise<Team[]> {
|
||||
return await Team.getInstances({
|
||||
organizationId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
this.updatedAt = new Date();
|
||||
if (!this.id) {
|
||||
this.id = await Team.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
115
ts/models/user.ts
Normal file
115
ts/models/user.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* User model for Stack.Gallery Registry
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type { IUser, TUserStatus } from '../interfaces/auth.interfaces.ts';
|
||||
import { getDb } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class User extends plugins.smartdata.SmartDataDbDoc<User, User> implements IUser {
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.searchable()
|
||||
@plugins.smartdata.index({ unique: true })
|
||||
public email: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.searchable()
|
||||
@plugins.smartdata.index({ unique: true })
|
||||
public username: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public passwordHash: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.searchable()
|
||||
public displayName: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public avatarUrl?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public status: TUserStatus = 'pending_verification';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public emailVerified: boolean = false;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public mfaEnabled: boolean = false;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public mfaSecret?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastLoginAt?: Date;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastLoginIp?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public failedLoginAttempts: number = 0;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lockedUntil?: Date;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public isPlatformAdmin: boolean = false;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public updatedAt: Date = new Date();
|
||||
|
||||
/**
|
||||
* Create a new user instance
|
||||
*/
|
||||
public static async createUser(data: {
|
||||
email: string;
|
||||
username: string;
|
||||
passwordHash: string;
|
||||
displayName?: string;
|
||||
}): Promise<User> {
|
||||
const user = new User();
|
||||
user.id = await User.getNewId();
|
||||
user.email = data.email.toLowerCase();
|
||||
user.username = data.username.toLowerCase();
|
||||
user.passwordHash = data.passwordHash;
|
||||
user.displayName = data.displayName || data.username;
|
||||
user.status = 'pending_verification';
|
||||
user.createdAt = new Date();
|
||||
user.updatedAt = new Date();
|
||||
await user.save();
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by email
|
||||
*/
|
||||
public static async findByEmail(email: string): Promise<User | null> {
|
||||
return await User.getInstance({ email: email.toLowerCase() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by username
|
||||
*/
|
||||
public static async findByUsername(username: string): Promise<User | null> {
|
||||
return await User.getInstance({ username: username.toLowerCase() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook: Update timestamps before save
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
this.updatedAt = new Date();
|
||||
if (!this.id) {
|
||||
this.id = await User.getNewId();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user