feat: show tenancy scope (Org → Chapter) in chapter & member portals
Surface where the logged-in user sits in the hierarchy via a ScopeCard in the sidebar, below the brand mark. API: - Enrich chapter login/me responses with tenantName + organization (LoginResponse.admin, AdminProfile types updated) - Add GET /my/scope returning member's chapter + organization Web: - ScopeCard component (Organisation row + Chapter row, themed icons) - Chapter portal: scope from auth context, blue accent - Member portal: scope fetched server-side in layout, emerald accent - PortalShell gains a `scope` slot rendered under the brand Org and admin portals intentionally omit it — org IS the scope, admin is platform-wide. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,10 +26,13 @@ export class AuthService {
|
||||
) {}
|
||||
|
||||
async login(req: LoginRequest): Promise<LoginResponse> {
|
||||
let tenant: { id: string; slug: string } | null = null;
|
||||
let tenant:
|
||||
| { id: string; slug: string; name: string; organization: { id: string; slug: string; name: string } | null }
|
||||
| null = null;
|
||||
const tenantInclude = { organization: { select: { id: true, slug: true, name: true } } } as const;
|
||||
|
||||
if (req.tenantSlug) {
|
||||
tenant = await this.prisma.tenant.findUnique({ where: { slug: req.tenantSlug } });
|
||||
tenant = await this.prisma.tenant.findUnique({ where: { slug: req.tenantSlug }, include: tenantInclude });
|
||||
if (!tenant) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
@@ -48,7 +51,7 @@ export class AuthService {
|
||||
'Email is registered in multiple tenants — please specify tenantSlug',
|
||||
);
|
||||
}
|
||||
const found = await this.prisma.tenant.findUnique({ where: { id: matches[0].tenantId } });
|
||||
const found = await this.prisma.tenant.findUnique({ where: { id: matches[0].tenantId }, include: tenantInclude });
|
||||
if (!found) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
@@ -93,6 +96,8 @@ export class AuthService {
|
||||
role: admin.role as AdminRole,
|
||||
tenantId: admin.tenantId,
|
||||
tenantSlug: tenant.slug,
|
||||
tenantName: tenant.name,
|
||||
organization: tenant.organization,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -100,7 +105,7 @@ export class AuthService {
|
||||
async me(adminId: string, tenantId: string): Promise<{ admin: LoginResponse['admin'] }> {
|
||||
const admin = await this.prisma.admin.findFirst({
|
||||
where: { id: adminId, tenantId },
|
||||
include: { tenant: true },
|
||||
include: { tenant: { include: { organization: { select: { id: true, slug: true, name: true } } } } },
|
||||
});
|
||||
if (!admin) throw new UnauthorizedException('Admin not found');
|
||||
return {
|
||||
@@ -110,6 +115,8 @@ export class AuthService {
|
||||
role: admin.role as AdminRole,
|
||||
tenantId: admin.tenantId,
|
||||
tenantSlug: admin.tenant.slug,
|
||||
tenantName: admin.tenant.name,
|
||||
organization: admin.tenant.organization,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -192,6 +199,8 @@ export class AuthService {
|
||||
role: PrismaAdminRole.OWNER,
|
||||
tenantId: tenant.id,
|
||||
tenantSlug: tenant.slug,
|
||||
tenantName: tenant.name,
|
||||
organization: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,6 +105,11 @@ export class MyController {
|
||||
return this.service.getDashboard(member.sub, member.tenantId);
|
||||
}
|
||||
|
||||
@Get('scope')
|
||||
scope(@CurrentMember() member: MemberJwtPayload) {
|
||||
return this.service.getScope(member.sub, member.tenantId);
|
||||
}
|
||||
|
||||
@Get('digest')
|
||||
digests(@CurrentMember() member: MemberJwtPayload) {
|
||||
return this.service.getDigests(member.tenantId);
|
||||
|
||||
@@ -186,6 +186,29 @@ export class MyService {
|
||||
};
|
||||
}
|
||||
|
||||
async getScope(userId: string, tenantId: string) {
|
||||
const user = await this.prisma.towerUser.findFirst({
|
||||
where: { id: userId, tenantId },
|
||||
select: {
|
||||
displayName: true,
|
||||
tenant: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
organization: { select: { id: true, slug: true, name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
return {
|
||||
member: { displayName: user.displayName },
|
||||
chapter: { id: user.tenant.id, slug: user.tenant.slug, name: user.tenant.name },
|
||||
organization: user.tenant.organization,
|
||||
};
|
||||
}
|
||||
|
||||
async getDashboard(userId: string, tenantId: string) {
|
||||
const user = await this.prisma.towerUser.findFirst({ where: { id: userId, tenantId } });
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
|
||||
Reference in New Issue
Block a user