feat(organization): add organization rename redirects and redirect management endpoints
This commit is contained in:
@@ -2,7 +2,7 @@ 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 { Organization, OrganizationMember, OrgRedirect, User } from '../../models/index.ts';
|
||||
import { PermissionService } from '../../services/permission.service.ts';
|
||||
import { AuditService } from '../../services/audit.service.ts';
|
||||
|
||||
@@ -19,9 +19,18 @@ export class OrganizationHandler {
|
||||
* 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);
|
||||
if (idOrName.startsWith('Organization:')) {
|
||||
return await Organization.findById(idOrName);
|
||||
}
|
||||
// Try direct name lookup first
|
||||
const org = await Organization.findByName(idOrName);
|
||||
if (org) return org;
|
||||
// Check redirects
|
||||
const redirect = await OrgRedirect.findByName(idOrName);
|
||||
if (redirect) {
|
||||
return await Organization.findById(redirect.organizationId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
@@ -232,6 +241,36 @@ export class OrganizationHandler {
|
||||
throw new plugins.typedrequest.TypedResponseError('Admin access required');
|
||||
}
|
||||
|
||||
// Handle rename
|
||||
if (dataArg.name && dataArg.name !== org.name) {
|
||||
const newName = dataArg.name;
|
||||
// Validate name format
|
||||
if (!/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/.test(newName)) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Name must be lowercase alphanumeric with optional hyphens and dots',
|
||||
);
|
||||
}
|
||||
// Check new name not taken by another org
|
||||
const existingOrg = await Organization.findByName(newName);
|
||||
if (existingOrg && existingOrg.id !== org.id) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Organization name already taken');
|
||||
}
|
||||
// Check new name not taken by a redirect pointing elsewhere
|
||||
const existingRedirect = await OrgRedirect.findByName(newName);
|
||||
if (existingRedirect && existingRedirect.organizationId !== org.id) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Name is reserved as a redirect for another organization',
|
||||
);
|
||||
}
|
||||
// If new name is one of our own redirects, delete that redirect
|
||||
if (existingRedirect && existingRedirect.organizationId === org.id) {
|
||||
await existingRedirect.delete();
|
||||
}
|
||||
// Create redirect from old name
|
||||
await OrgRedirect.create(org.name, org.id);
|
||||
org.name = newName;
|
||||
}
|
||||
|
||||
if (dataArg.displayName !== undefined) org.displayName = dataArg.displayName;
|
||||
if (dataArg.description !== undefined) org.description = dataArg.description;
|
||||
if (dataArg.avatarUrl !== undefined) org.avatarUrl = dataArg.avatarUrl;
|
||||
@@ -544,5 +583,69 @@ export class OrganizationHandler {
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Get Org Redirects
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetOrgRedirects>(
|
||||
'getOrgRedirects',
|
||||
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');
|
||||
}
|
||||
|
||||
const redirects = await OrgRedirect.getByOrgId(org.id);
|
||||
return {
|
||||
redirects: redirects.map((r) => ({
|
||||
id: r.id,
|
||||
oldName: r.oldName,
|
||||
organizationId: r.organizationId,
|
||||
createdAt: r.createdAt instanceof Date
|
||||
? r.createdAt.toISOString()
|
||||
: String(r.createdAt),
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to get redirects');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Delete Org Redirect
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteOrgRedirect>(
|
||||
'deleteOrgRedirect',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const redirect = await OrgRedirect.findById(dataArg.redirectId);
|
||||
if (!redirect) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Redirect not found');
|
||||
}
|
||||
|
||||
// Check permission on the org
|
||||
const canManage = await this.permissionService.canManageOrganization(
|
||||
dataArg.identity.userId,
|
||||
redirect.organizationId,
|
||||
);
|
||||
if (!canManage && !dataArg.identity.isSystemAdmin) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Admin access required');
|
||||
}
|
||||
|
||||
await redirect.delete();
|
||||
return { message: 'Redirect deleted successfully' };
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to delete redirect');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user