Spaces:
Sleeping
Sleeping
Download src/modules/users/users-operator.controller.ts from mahrooshishaq/SmartfolioBackend: direct link, hf CLI and curl.
- Browser
- Download file 3.55 kB
-
https://huggingface.co/spaces/mahrooshishaq/SmartfolioBackend/resolve/main/src/modules/users/users-operator.controller.ts
- Command line
-
hf download hf://spaces/mahrooshishaq/SmartfolioBackend/src/modules/users/users-operator.controller.ts
-
curl -L -o users-operator.controller.ts https://huggingface.co/spaces/mahrooshishaq/SmartfolioBackend/resolve/main/src/modules/users/users-operator.controller.ts
3.55 kB
| import { | |
| Body, | |
| Controller, | |
| Logger, | |
| NotFoundException, | |
| Post, | |
| UseGuards, | |
| } from '@nestjs/common'; | |
| import { InjectRepository } from '@nestjs/typeorm'; | |
| import { Repository } from 'typeorm'; | |
| import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; | |
| import { IsEmail, IsIn } from 'class-validator'; | |
| import { ApiProperty } from '@nestjs/swagger'; | |
| import { User } from './user.entity'; | |
| import { AdminKeyGuard } from '../auth/admin-key.guard'; | |
| import { RateLimit, RateLimitGuard } from '../../common/guards/rate-limit.guard'; | |
| export class SetRoleDto { | |
| ({ example: 'operator@example.com' }) | |
| () | |
| email!: string; | |
| ({ enum: ['candidate', 'admin'] }) | |
| (['candidate', 'admin']) | |
| role!: 'candidate' | 'admin'; | |
| } | |
| /** | |
| * Granting and removing the admin role. | |
| * | |
| * There is no self-serve path to admin and there should not be, so the role has | |
| * to be set by an operator. Doing that through direct database access works | |
| * exactly once and then becomes a reason to keep production credentials on | |
| * somebody's laptop — which is a worse security position than this endpoint. | |
| * | |
| * Guarded by AdminKeyGuard, the same shared-secret guard the scraper | |
| * maintenance routes already use. That guard fails CLOSED (an unset | |
| * ADMIN_API_KEY makes this unreachable rather than open) and compares in | |
| * constant time, so the key cannot be recovered by timing. The rate limit is | |
| * what makes guessing it impractical rather than merely expensive. | |
| * | |
| * Deliberately NOT reachable with a JWT, admin or otherwise. An admin who could | |
| * mint other admins turns one compromised session into permanent access; this | |
| * way the blast radius of any stolen token stops at what that one account can | |
| * already do. | |
| * | |
| * The users module has no other controller on purpose — its docblock records | |
| * that the old /users routes returned the full entity, password and token | |
| * hashes included. So this returns four named fields and nothing else. | |
| */ | |
| ('Operator') | |
| ('admin-key') | |
| ('admin/users') | |
| (AdminKeyGuard, RateLimitGuard) | |
| export class UsersOperatorController { | |
| private readonly logger = new Logger(UsersOperatorController.name); | |
| constructor((User) private readonly users: Repository<User>) {} | |
| ('role') | |
| ({ limit: 5, windowMs: 60_000 }) | |
| ({ | |
| summary: 'Grant or remove the admin role', | |
| description: | |
| 'Requires the x-admin-key header. Not reachable with a JWT: an admin who could ' + | |
| 'mint other admins would turn one stolen session into permanent access.', | |
| }) | |
| async setRole(() dto: SetRoleDto) { | |
| const email = dto.email.trim().toLowerCase(); | |
| const user = await this.users | |
| .createQueryBuilder('u') | |
| .where('LOWER(u.email) = :email', { email }) | |
| .getOne(); | |
| if (!user) { | |
| // The account has to exist first: roles are granted to people who have | |
| // signed up, not reserved for addresses that might one day register. | |
| throw new NotFoundException( | |
| `No account for ${email}. Sign up on the site first, then grant the role.`, | |
| ); | |
| } | |
| const previous = user.role; | |
| user.role = dto.role; | |
| await this.users.save(user); | |
| // Audit trail. A privilege change with no record of it is the thing you | |
| // most want to look back at and cannot. | |
| this.logger.warn( | |
| `ROLE CHANGE: ${user.email} (${user.id}) ${previous} -> ${dto.role}`, | |
| ); | |
| return { id: user.id, email: user.email, name: user.name, role: user.role }; | |
| } | |
| } | |