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:
307
ts/services/permission.service.ts
Normal file
307
ts/services/permission.service.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* PermissionService - RBAC resolution across org → team → repo hierarchy
|
||||
*/
|
||||
|
||||
import type {
|
||||
TOrganizationRole,
|
||||
TTeamRole,
|
||||
TRepositoryRole,
|
||||
TRegistryProtocol,
|
||||
} from '../interfaces/auth.interfaces.ts';
|
||||
import {
|
||||
User,
|
||||
Organization,
|
||||
OrganizationMember,
|
||||
Team,
|
||||
TeamMember,
|
||||
Repository,
|
||||
RepositoryPermission,
|
||||
} from '../models/index.ts';
|
||||
|
||||
export type TAction = 'read' | 'write' | 'delete' | 'admin';
|
||||
|
||||
export interface IPermissionContext {
|
||||
userId: string;
|
||||
organizationId?: string;
|
||||
repositoryId?: string;
|
||||
protocol?: TRegistryProtocol;
|
||||
}
|
||||
|
||||
export interface IResolvedPermissions {
|
||||
canRead: boolean;
|
||||
canWrite: boolean;
|
||||
canDelete: boolean;
|
||||
canAdmin: boolean;
|
||||
effectiveRole: TRepositoryRole | null;
|
||||
organizationRole: TOrganizationRole | null;
|
||||
teamRoles: Array<{ teamId: string; role: TTeamRole }>;
|
||||
repositoryRole: TRepositoryRole | null;
|
||||
}
|
||||
|
||||
export class PermissionService {
|
||||
/**
|
||||
* Resolve all permissions for a user in a specific context
|
||||
*/
|
||||
public async resolvePermissions(context: IPermissionContext): Promise<IResolvedPermissions> {
|
||||
const result: IResolvedPermissions = {
|
||||
canRead: false,
|
||||
canWrite: false,
|
||||
canDelete: false,
|
||||
canAdmin: false,
|
||||
effectiveRole: null,
|
||||
organizationRole: null,
|
||||
teamRoles: [],
|
||||
repositoryRole: null,
|
||||
};
|
||||
|
||||
// Get user
|
||||
const user = await User.findById(context.userId);
|
||||
if (!user || !user.isActive) return result;
|
||||
|
||||
// System admins have full access
|
||||
if (user.isSystemAdmin) {
|
||||
result.canRead = true;
|
||||
result.canWrite = true;
|
||||
result.canDelete = true;
|
||||
result.canAdmin = true;
|
||||
result.effectiveRole = 'admin';
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!context.organizationId) return result;
|
||||
|
||||
// Get organization membership
|
||||
const orgMember = await OrganizationMember.findMembership(context.organizationId, context.userId);
|
||||
if (orgMember) {
|
||||
result.organizationRole = orgMember.role;
|
||||
|
||||
// Organization owners have full access to everything in the org
|
||||
if (orgMember.role === 'owner') {
|
||||
result.canRead = true;
|
||||
result.canWrite = true;
|
||||
result.canDelete = true;
|
||||
result.canAdmin = true;
|
||||
result.effectiveRole = 'admin';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Organization admins have admin access to all repos
|
||||
if (orgMember.role === 'admin') {
|
||||
result.canRead = true;
|
||||
result.canWrite = true;
|
||||
result.canDelete = true;
|
||||
result.canAdmin = true;
|
||||
result.effectiveRole = 'admin';
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If no repository specified, check org-level permissions
|
||||
if (!context.repositoryId) {
|
||||
if (orgMember) {
|
||||
result.canRead = true; // Members can read org info
|
||||
result.effectiveRole = 'reader';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get repository
|
||||
const repository = await Repository.findById(context.repositoryId);
|
||||
if (!repository) return result;
|
||||
|
||||
// Check if repository is public
|
||||
if (repository.isPublic) {
|
||||
result.canRead = true;
|
||||
}
|
||||
|
||||
// Get team memberships that grant access to this repository
|
||||
if (orgMember) {
|
||||
const teams = await Team.getOrgTeams(context.organizationId);
|
||||
for (const team of teams) {
|
||||
const teamMember = await TeamMember.findMembership(team.id, context.userId);
|
||||
if (teamMember) {
|
||||
result.teamRoles.push({ teamId: team.id, role: teamMember.role });
|
||||
|
||||
// Check if team has access to this repository
|
||||
if (team.repositoryIds.includes(context.repositoryId)) {
|
||||
// Team maintainers get maintainer access to repos
|
||||
if (teamMember.role === 'maintainer') {
|
||||
this.applyRole(result, 'maintainer');
|
||||
} else {
|
||||
// Team members get developer access
|
||||
this.applyRole(result, 'developer');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get direct repository permission (highest priority)
|
||||
const repoPerm = await RepositoryPermission.findPermission(context.repositoryId, context.userId);
|
||||
if (repoPerm) {
|
||||
result.repositoryRole = repoPerm.role;
|
||||
this.applyRole(result, repoPerm.role);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can perform a specific action
|
||||
*/
|
||||
public async checkPermission(
|
||||
context: IPermissionContext,
|
||||
action: TAction
|
||||
): Promise<boolean> {
|
||||
const permissions = await this.resolvePermissions(context);
|
||||
|
||||
switch (action) {
|
||||
case 'read':
|
||||
return permissions.canRead;
|
||||
case 'write':
|
||||
return permissions.canWrite;
|
||||
case 'delete':
|
||||
return permissions.canDelete;
|
||||
case 'admin':
|
||||
return permissions.canAdmin;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can access a package
|
||||
*/
|
||||
public async canAccessPackage(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
repositoryId: string,
|
||||
action: 'read' | 'write' | 'delete'
|
||||
): Promise<boolean> {
|
||||
return await this.checkPermission(
|
||||
{ userId, organizationId, repositoryId },
|
||||
action
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can manage organization
|
||||
*/
|
||||
public async canManageOrganization(userId: string, organizationId: string): Promise<boolean> {
|
||||
const user = await User.findById(userId);
|
||||
if (!user || !user.isActive) return false;
|
||||
if (user.isSystemAdmin) return true;
|
||||
|
||||
const orgMember = await OrganizationMember.findMembership(organizationId, userId);
|
||||
return orgMember?.role === 'owner' || orgMember?.role === 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can manage repository
|
||||
*/
|
||||
public async canManageRepository(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
repositoryId: string
|
||||
): Promise<boolean> {
|
||||
const permissions = await this.resolvePermissions({
|
||||
userId,
|
||||
organizationId,
|
||||
repositoryId,
|
||||
});
|
||||
return permissions.canAdmin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all repositories a user can access in an organization
|
||||
*/
|
||||
public async getAccessibleRepositories(
|
||||
userId: string,
|
||||
organizationId: string
|
||||
): Promise<Repository[]> {
|
||||
const user = await User.findById(userId);
|
||||
if (!user || !user.isActive) return [];
|
||||
|
||||
// System admins and org owners/admins can access all repos
|
||||
if (user.isSystemAdmin) {
|
||||
return await Repository.getOrgRepositories(organizationId);
|
||||
}
|
||||
|
||||
const orgMember = await OrganizationMember.findMembership(organizationId, userId);
|
||||
if (orgMember?.role === 'owner' || orgMember?.role === 'admin') {
|
||||
return await Repository.getOrgRepositories(organizationId);
|
||||
}
|
||||
|
||||
const allRepos = await Repository.getOrgRepositories(organizationId);
|
||||
const accessibleRepos: Repository[] = [];
|
||||
|
||||
for (const repo of allRepos) {
|
||||
// Public repos are always accessible
|
||||
if (repo.isPublic) {
|
||||
accessibleRepos.push(repo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check direct permission
|
||||
const directPerm = await RepositoryPermission.findPermission(repo.id, userId);
|
||||
if (directPerm) {
|
||||
accessibleRepos.push(repo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check team access
|
||||
const teams = await Team.getOrgTeams(organizationId);
|
||||
for (const team of teams) {
|
||||
if (team.repositoryIds.includes(repo.id)) {
|
||||
const teamMember = await TeamMember.findMembership(team.id, userId);
|
||||
if (teamMember) {
|
||||
accessibleRepos.push(repo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return accessibleRepos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a role's permissions to the result
|
||||
*/
|
||||
private applyRole(result: IResolvedPermissions, role: TRepositoryRole): void {
|
||||
const roleHierarchy: Record<TRepositoryRole, number> = {
|
||||
reader: 1,
|
||||
developer: 2,
|
||||
maintainer: 3,
|
||||
admin: 4,
|
||||
};
|
||||
|
||||
const currentLevel = result.effectiveRole ? roleHierarchy[result.effectiveRole] : 0;
|
||||
const newLevel = roleHierarchy[role];
|
||||
|
||||
if (newLevel > currentLevel) {
|
||||
result.effectiveRole = role;
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
result.canRead = true;
|
||||
result.canWrite = true;
|
||||
result.canDelete = true;
|
||||
result.canAdmin = true;
|
||||
break;
|
||||
case 'maintainer':
|
||||
result.canRead = true;
|
||||
result.canWrite = true;
|
||||
result.canDelete = true;
|
||||
break;
|
||||
case 'developer':
|
||||
result.canRead = true;
|
||||
result.canWrite = true;
|
||||
break;
|
||||
case 'reader':
|
||||
result.canRead = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user