feat(opsserver,web): replace the Angular UI and REST management layer with a TypedRequest-based ops server and bundled web frontend

This commit is contained in:
2026-03-20 16:43:44 +00:00
parent 0fc74ff995
commit d4f758ce0f
159 changed files with 12465 additions and 14861 deletions

View File

@@ -0,0 +1,548 @@
import * as plugins from '../../plugins.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
import { Organization, OrganizationMember, User } from '../../models/index.ts';
import { PermissionService } from '../../services/permission.service.ts';
import { AuditService } from '../../services/audit.service.ts';
export class OrganizationHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
private permissionService = new PermissionService();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
/**
* Helper to resolve organization by ID or name
*/
private async resolveOrganization(idOrName: string): Promise<Organization | null> {
return idOrName.startsWith('Organization:')
? await Organization.findById(idOrName)
: await Organization.findByName(idOrName);
}
private registerHandlers(): void {
// Get Organizations
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetOrganizations>(
'getOrganizations',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const userId = dataArg.identity.userId;
let organizations: Organization[];
if (dataArg.identity.isSystemAdmin) {
organizations = await Organization.getInstances({});
} else {
const memberships = await OrganizationMember.getUserOrganizations(userId);
const orgs: Organization[] = [];
for (const m of memberships) {
const org = await Organization.findById(m.organizationId);
if (org) orgs.push(org);
}
organizations = orgs;
}
return {
organizations: organizations.map((org) => ({
id: org.id,
name: org.name,
displayName: org.displayName,
description: org.description,
avatarUrl: org.avatarUrl,
website: org.website,
isPublic: org.isPublic,
memberCount: org.memberCount,
plan: (org as any).plan || 'free',
usedStorageBytes: org.usedStorageBytes || 0,
storageQuotaBytes: (org as any).storageQuotaBytes || 0,
createdAt: org.createdAt instanceof Date
? org.createdAt.toISOString()
: String(org.createdAt),
})),
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to list organizations');
}
},
),
);
// Get Organization
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetOrganization>(
'getOrganization',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Check access - public orgs visible to all, private requires membership
if (!org.isPublic) {
const isMember = await OrganizationMember.findMembership(
org.id,
dataArg.identity.userId,
);
if (!isMember && !dataArg.identity.isSystemAdmin) {
throw new plugins.typedrequest.TypedResponseError('Access denied');
}
}
const orgData: interfaces.data.IOrganizationDetail = {
id: org.id,
name: org.name,
displayName: org.displayName,
description: org.description,
avatarUrl: org.avatarUrl,
website: org.website,
isPublic: org.isPublic,
memberCount: org.memberCount,
plan: (org as any).plan || 'free',
usedStorageBytes: org.usedStorageBytes || 0,
storageQuotaBytes: (org as any).storageQuotaBytes || 0,
createdAt: org.createdAt instanceof Date
? org.createdAt.toISOString()
: String(org.createdAt),
};
// Include settings for admins
if (dataArg.identity.isSystemAdmin && org.settings) {
orgData.settings = org.settings as any;
}
return { organization: orgData };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to get organization');
}
},
),
);
// Create Organization
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateOrganization>(
'createOrganization',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const { name, displayName, description, isPublic } = dataArg;
if (!name) {
throw new plugins.typedrequest.TypedResponseError('Organization name is required');
}
// Validate name format
if (!/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/.test(name)) {
throw new plugins.typedrequest.TypedResponseError(
'Name must be lowercase alphanumeric with optional hyphens and dots',
);
}
// Check uniqueness
const existing = await Organization.findByName(name);
if (existing) {
throw new plugins.typedrequest.TypedResponseError('Organization name already taken');
}
// Create organization
const org = new Organization();
org.id = await Organization.getNewId();
org.name = name;
org.displayName = displayName || name;
org.description = description;
org.isPublic = isPublic ?? false;
org.memberCount = 1;
org.createdAt = new Date();
org.createdById = dataArg.identity.userId;
await org.save();
// Add creator as owner
const membership = new OrganizationMember();
membership.id = await OrganizationMember.getNewId();
membership.organizationId = org.id;
membership.userId = dataArg.identity.userId;
membership.role = 'owner';
membership.invitedBy = dataArg.identity.userId;
membership.joinedAt = new Date();
await membership.save();
// Audit log
await AuditService.withContext({
actorId: dataArg.identity.userId,
actorType: 'user',
}).logOrganizationCreated(org.id, org.name);
return {
organization: {
id: org.id,
name: org.name,
displayName: org.displayName,
description: org.description,
isPublic: org.isPublic,
memberCount: org.memberCount,
plan: (org as any).plan || 'free',
usedStorageBytes: org.usedStorageBytes || 0,
storageQuotaBytes: (org as any).storageQuotaBytes || 0,
createdAt: org.createdAt instanceof Date
? org.createdAt.toISOString()
: String(org.createdAt),
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to create organization');
}
},
),
);
// Update Organization
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateOrganization>(
'updateOrganization',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
org.id,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
if (dataArg.displayName !== undefined) org.displayName = dataArg.displayName;
if (dataArg.description !== undefined) org.description = dataArg.description;
if (dataArg.avatarUrl !== undefined) org.avatarUrl = dataArg.avatarUrl;
if (dataArg.website !== undefined) org.website = dataArg.website;
if (dataArg.isPublic !== undefined) org.isPublic = dataArg.isPublic;
// Only system admins can change settings
if (dataArg.settings && dataArg.identity.isSystemAdmin) {
org.settings = { ...org.settings, ...dataArg.settings } as any;
}
await org.save();
return {
organization: {
id: org.id,
name: org.name,
displayName: org.displayName,
description: org.description,
avatarUrl: org.avatarUrl,
website: org.website,
isPublic: org.isPublic,
memberCount: org.memberCount,
plan: (org as any).plan || 'free',
usedStorageBytes: org.usedStorageBytes || 0,
storageQuotaBytes: (org as any).storageQuotaBytes || 0,
createdAt: org.createdAt instanceof Date
? org.createdAt.toISOString()
: String(org.createdAt),
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to update organization');
}
},
),
);
// Delete Organization
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteOrganization>(
'deleteOrganization',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Only owners and system admins can delete
const membership = await OrganizationMember.findMembership(
org.id,
dataArg.identity.userId,
);
if (membership?.role !== 'owner' && !dataArg.identity.isSystemAdmin) {
throw new plugins.typedrequest.TypedResponseError('Owner access required');
}
await org.delete();
return { message: 'Organization deleted successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to delete organization');
}
},
),
);
// Get Organization Members
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetOrganizationMembers>(
'getOrganizationMembers',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Check membership
const isMember = await OrganizationMember.findMembership(
org.id,
dataArg.identity.userId,
);
if (!isMember && !dataArg.identity.isSystemAdmin) {
throw new plugins.typedrequest.TypedResponseError('Access denied');
}
const members = await OrganizationMember.getOrgMembers(org.id);
const membersWithUsers = await Promise.all(
members.map(async (m) => {
const user = await User.findById(m.userId);
return {
userId: m.userId,
role: m.role as interfaces.data.TOrganizationRole,
addedAt: m.joinedAt instanceof Date
? m.joinedAt.toISOString()
: String(m.joinedAt),
user: user
? {
username: user.username,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
}
: null,
};
}),
);
return { members: membersWithUsers };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to list members');
}
},
),
);
// Add Organization Member
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AddOrganizationMember>(
'addOrganizationMember',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
org.id,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
const { userId, role } = dataArg;
if (!userId || !role) {
throw new plugins.typedrequest.TypedResponseError('userId and role are required');
}
if (!['owner', 'admin', 'member'].includes(role)) {
throw new plugins.typedrequest.TypedResponseError('Invalid role');
}
// Check user exists
const user = await User.findById(userId);
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
// Check if already a member
const existing = await OrganizationMember.findMembership(org.id, userId);
if (existing) {
throw new plugins.typedrequest.TypedResponseError('User is already a member');
}
// Add member
const membership = new OrganizationMember();
membership.id = await OrganizationMember.getNewId();
membership.organizationId = org.id;
membership.userId = userId;
membership.role = role;
membership.invitedBy = dataArg.identity.userId;
membership.joinedAt = new Date();
await membership.save();
// Update member count
org.memberCount += 1;
await org.save();
return {
member: {
userId: membership.userId,
role: membership.role as interfaces.data.TOrganizationRole,
addedAt: membership.joinedAt instanceof Date
? membership.joinedAt.toISOString()
: String(membership.joinedAt),
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to add member');
}
},
),
);
// Update Organization Member
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateOrganizationMember>(
'updateOrganizationMember',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
org.id,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
const { userId, role } = dataArg;
if (!role || !['owner', 'admin', 'member'].includes(role)) {
throw new plugins.typedrequest.TypedResponseError('Valid role is required');
}
const membership = await OrganizationMember.findMembership(org.id, userId);
if (!membership) {
throw new plugins.typedrequest.TypedResponseError('Member not found');
}
// Cannot change last owner
if (membership.role === 'owner' && role !== 'owner') {
const members = await OrganizationMember.getOrgMembers(org.id);
const ownerCount = members.filter((m) => m.role === 'owner').length;
if (ownerCount <= 1) {
throw new plugins.typedrequest.TypedResponseError('Cannot remove the last owner');
}
}
membership.role = role;
await membership.save();
return {
member: {
userId: membership.userId,
role: membership.role as interfaces.data.TOrganizationRole,
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to update member');
}
},
),
);
// Remove Organization Member
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveOrganizationMember>(
'removeOrganizationMember',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Users can remove themselves, admins can remove others
if (dataArg.userId !== dataArg.identity.userId) {
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
org.id,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
}
const membership = await OrganizationMember.findMembership(org.id, dataArg.userId);
if (!membership) {
throw new plugins.typedrequest.TypedResponseError('Member not found');
}
// Cannot remove last owner
if (membership.role === 'owner') {
const members = await OrganizationMember.getOrgMembers(org.id);
const ownerCount = members.filter((m) => m.role === 'owner').length;
if (ownerCount <= 1) {
throw new plugins.typedrequest.TypedResponseError('Cannot remove the last owner');
}
}
await membership.delete();
// Update member count
org.memberCount = Math.max(0, org.memberCount - 1);
await org.save();
return { message: 'Member removed successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to remove member');
}
},
),
);
}
}