Add SSO handoff tickets and expand seeded countries for address forms.

Support one-time Redis-backed dashboard sign-in handoff and seed European, Middle East, and Far East countries with major cities.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-19 19:46:57 +03:30
co-authored by Cursor
parent 7277817b6e
commit f61bba317e
6 changed files with 319 additions and 1 deletions
+68
View File
@@ -1,6 +1,7 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
HttpException,
Injectable,
ServiceUnavailableException,
@@ -9,6 +10,7 @@ import {
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { randomBytes } from 'node:crypto';
import { PrismaService } from '../prisma/prisma.service';
import { RedisService } from '../redis/redis.service';
import { TenantService } from '../tenant/tenant.service';
@@ -28,6 +30,9 @@ import { parseUserProfile } from './profile.util';
import { SmsService } from './sms.service';
const OTP_TTL_SECONDS = 300;
const HANDOFF_TTL_SECONDS = 60;
const HANDOFF_RATE_LIMIT = 10;
const HANDOFF_RATE_WINDOW_SECONDS = 60;
@Injectable()
export class AuthService {
@@ -216,6 +221,62 @@ export class AuthService {
};
}
async createHandoff(user: AuthUser) {
if (!this.canAccessBusinessDashboard(user)) {
throw new ForbiddenException(
'This account cannot open the business dashboard.',
);
}
const allowed = await this.redis.incrementWithLimit(
`sso:handoff:rate:${user.id.toString()}`,
HANDOFF_RATE_LIMIT,
HANDOFF_RATE_WINDOW_SECONDS,
);
if (!allowed) {
throw new HttpException('Too many dashboard handoff requests. Try again shortly.', 429);
}
const ticket = randomBytes(32).toString('hex');
await this.redis.setHandoffTicket(
ticket,
user.id.toString(),
HANDOFF_TTL_SECONDS,
);
return {
ticket,
expiresInSeconds: HANDOFF_TTL_SECONDS,
};
}
async consumeHandoff(ticket: string) {
const userId = await this.redis.consumeHandoffTicket(ticket);
if (!userId) {
throw new UnauthorizedException('Invalid or expired dashboard handoff');
}
const authUser = await this.getAuthUser(BigInt(userId));
if (!authUser || !this.canAccessBusinessDashboard(authUser)) {
throw new ForbiddenException(
'This account cannot access the business dashboard.',
);
}
await this.prisma.user.update({
where: { id: authUser.id },
data: { lastLoginAt: new Date() },
});
const tokens = await this.issueTokens(authUser);
return {
message: 'Login successful',
user: this.serializeUser(authUser),
...tokens,
};
}
async me(user: AuthUser) {
return { user: this.serializeUser(user) };
}
@@ -526,6 +587,13 @@ export class AuthService {
return 'customer';
}
private canAccessBusinessDashboard(user: AuthUser): boolean {
if (user.roles.includes('super_admin')) {
return true;
}
return user.businesses.length > 0;
}
private async issueTokens(user: AuthUser) {
const accessPayload: AuthJwtPayload = {
sub: user.id.toString(),