Ports "تکمیل پروفایل" end to end (D22). API - POST customer/me/complete-profile — three required document ids, the customer taken from the token, verification step set to the reference's LIVE value (approved outright; its `= 1` line is commented out at :83 in favour of `= 2` at :85). Both SMS messages byte-identical and non-blocking per D20. - POST customer/me/documents — the reference let the app upload through the same file/create the admin used, inside its auth:api-customer group (routes/api.php:95); the target's copy is admin-guarded, so the page could not have uploaded at all. Reuses AdminUploadsService unchanged, so both paths share one size limit, MIME whitelist and naming scheme (UPL-01). - PROFILE_ADMIN_MOBILES replaces the reference's four hardcoded personal numbers; blank means nobody is notified and the upload still succeeds. Three reference behaviours deliberately NOT ported, all in ISSUES.md: - SEC-14: `$customer->update($request->all())` mass-assigned the whole request body, letting the app write its own credit limit, making charge, gold debt, active flag or mobile. Replaced with a three-field whitelist. - SEC-15: four real personal mobiles hardcoded in the source. - APP-03: refresh()'s unterminated setInterval page reload. Apps (both, template+script byte-identical, per-app styling only) - CompleteProfileView + SingleImageUploader - route /complete_profile, redirecting home once the documents are in, as the reference's guard did (router/index.ts:266) - the entry point on the profile screen, shown only while they are missing (profile.vue:96-102) Also (D23): JWT_CUSTOMER_SECRET now warns loudly at boot when unset. It falls back to JWT_SECRET, which signs app and admin tokens with the same key and leaves the `typ` check as the only barrier — and an expert's app token carries their AdminUser id in `sub`. Warn rather than refuse to boot (D19, lowest risk); DELIVERY.md lists it as the first pre-release step (REL-03). Verified: build 5/5 · jest 45/45 · complete-profile 38/38 (new) · expert login 36/36 · apps smoke 43/43 · admin smoke 43/43 · B2B scan 34 ok/0 broken · typecheck clean on all four apps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import {
|
|
Controller,
|
|
HttpCode,
|
|
Post,
|
|
UploadedFile,
|
|
UseInterceptors,
|
|
} from '@nestjs/common';
|
|
import { Body } from '@nestjs/common';
|
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
import { CustomerProfileService } from './customer-profile.service';
|
|
import { AdminUploadsService } from '../admin-uploads/admin-uploads.service';
|
|
import { CompleteProfileDto } from './dto/complete-profile.dto';
|
|
import { CurrentCustomer, CustomerAuth } from '../customer-auth/customer.guards';
|
|
import type { AuthenticatedCustomer } from '../customer-auth/customer-jwt.strategy';
|
|
|
|
/** See AdminUploadsController — the same locally-declared multer subset. */
|
|
interface UploadedMulterFile {
|
|
originalname: string;
|
|
mimetype: string;
|
|
size: number;
|
|
buffer: Buffer;
|
|
}
|
|
|
|
/**
|
|
* The customer's own profile writes.
|
|
*
|
|
* Legacy endpoints:
|
|
* POST file/create -> the PWA's SingleImageUploader posted here with the
|
|
* customer token (routes/api.php:95, inside the
|
|
* `auth:api-customer` group)
|
|
* POST major/complete -> Major\CustomerController::complete
|
|
* (routes/api.php:114)
|
|
*
|
|
* Both are mounted under /customer/me here so the customer surface stays in one
|
|
* place. The subject always comes from the token — no id is accepted from the
|
|
* client, per D21.
|
|
*/
|
|
@Controller('customer/me')
|
|
export class CustomerProfileController {
|
|
constructor(
|
|
private readonly profile: CustomerProfileService,
|
|
private readonly uploads: AdminUploadsService,
|
|
) {}
|
|
|
|
/**
|
|
* A customer uploading one of their own documents.
|
|
*
|
|
* The reference let the app upload through the very same `file/create` the
|
|
* admin panel used, behind the customer guard (routes/api.php:95). The
|
|
* target's copy of that route sits behind an ADMIN guard
|
|
* (AdminUploadsController), which no app token can pass — so without this
|
|
* route the page could not upload at all.
|
|
*
|
|
* It reuses AdminUploadsService unchanged, so the size limit, the MIME
|
|
* whitelist and the stored-name scheme are identical on both paths. The
|
|
* uploader is recorded as null rather than as an admin, because
|
|
* `UploadedFile.uploadedByAdminUserId` is an AdminUser FK and this caller is
|
|
* a customer; `purpose` carries the document kind instead.
|
|
*/
|
|
@CustomerAuth()
|
|
@Post('documents')
|
|
@HttpCode(200)
|
|
@UseInterceptors(FileInterceptor('file'))
|
|
upload(
|
|
@UploadedFile() file: UploadedMulterFile | undefined,
|
|
@Body('purpose') purpose: string | undefined,
|
|
@CurrentCustomer() _me: AuthenticatedCustomer,
|
|
) {
|
|
return this.uploads.store(file, purpose ?? 'customer-document', null);
|
|
}
|
|
|
|
/** Ports POST major/complete. The customer is the token's. */
|
|
@CustomerAuth()
|
|
@Post('complete-profile')
|
|
@HttpCode(200)
|
|
complete(@Body() dto: CompleteProfileDto, @CurrentCustomer() me: AuthenticatedCustomer) {
|
|
return this.profile.complete(me.id, dto);
|
|
}
|
|
}
|