auth graph
This commit is contained in:
Generated
+985
-187
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,8 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@apollo/server": "^5.1.0",
|
||||||
|
"@as-integrations/fastify": "^3.1.0",
|
||||||
"@aws-sdk/client-s3": "^3.922.0",
|
"@aws-sdk/client-s3": "^3.922.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.922.0",
|
"@aws-sdk/s3-request-presigner": "^3.922.0",
|
||||||
"@fastify/multipart": "^9.3.0",
|
"@fastify/multipart": "^9.3.0",
|
||||||
@@ -33,12 +35,14 @@
|
|||||||
"@mikro-orm/nestjs": "^6.1.1",
|
"@mikro-orm/nestjs": "^6.1.1",
|
||||||
"@mikro-orm/postgresql": "^6.5.8",
|
"@mikro-orm/postgresql": "^6.5.8",
|
||||||
"@nest-lab/fastify-multer": "^1.3.0",
|
"@nest-lab/fastify-multer": "^1.3.0",
|
||||||
|
"@nestjs/apollo": "^13.2.1",
|
||||||
"@nestjs/axios": "^4.0.1",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/bullmq": "^11.0.4",
|
"@nestjs/bullmq": "^11.0.4",
|
||||||
"@nestjs/cache-manager": "^3.0.1",
|
"@nestjs/cache-manager": "^3.0.1",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/graphql": "^13.2.0",
|
||||||
"@nestjs/jwt": "^11.0.1",
|
"@nestjs/jwt": "^11.0.1",
|
||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
@@ -51,6 +55,7 @@
|
|||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"fastify": "^5.6.1",
|
"fastify": "^5.6.1",
|
||||||
|
"graphql": "^16.12.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"ulid": "^3.0.1"
|
"ulid": "^3.0.1"
|
||||||
|
|||||||
@@ -18,10 +18,19 @@ import { RestaurantsModule } from './modules/restaurants/restaurants.module';
|
|||||||
import { FoodModule } from './modules/foods/food.module';
|
import { FoodModule } from './modules/foods/food.module';
|
||||||
import { CartModule } from './modules/cart/cart.module';
|
import { CartModule } from './modules/cart/cart.module';
|
||||||
import { RolesModule } from './modules/roles/roles.module';
|
import { RolesModule } from './modules/roles/roles.module';
|
||||||
|
import { GraphQLModule } from '@nestjs/graphql';
|
||||||
|
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true, cache: true }),
|
ConfigModule.forRoot({ isGlobal: true, cache: true }),
|
||||||
|
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||||
|
driver: ApolloDriver,
|
||||||
|
autoSchemaFile: true,
|
||||||
|
playground: true,
|
||||||
|
introspection: true,
|
||||||
|
context: ({ req, reply }) => ({ request: req, reply }),
|
||||||
|
}),
|
||||||
MikroOrmModule.forRootAsync(dataBaseConfig),
|
MikroOrmModule.forRootAsync(dataBaseConfig),
|
||||||
// CacheModule.registerAsync(cacheConfig()),
|
// CacheModule.registerAsync(cacheConfig()),
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
|
|||||||
@@ -1,30 +1,94 @@
|
|||||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||||
import { FastifyReply } from 'fastify';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ResponseInterceptor implements NestInterceptor {
|
export class ResponseInterceptor implements NestInterceptor {
|
||||||
//
|
constructor(private reflector: Reflector) {}
|
||||||
intercept(context: ExecutionContext, next: CallHandler<Record<string, unknown>>): Observable<unknown> {
|
|
||||||
const ctx = context.switchToHttp().getResponse<FastifyReply>();
|
|
||||||
const statusCode = ctx.statusCode;
|
|
||||||
|
|
||||||
return next.handle().pipe(
|
intercept(context: ExecutionContext, next: CallHandler<Record<string, unknown>>): Observable<unknown> {
|
||||||
map(data => {
|
// Check if this is a GraphQL request
|
||||||
if (data && data.data !== undefined) {
|
// GraphQL requests should bypass this interceptor as they handle their own response format
|
||||||
|
|
||||||
|
// Method 1: Check context type (most reliable for GraphQL)
|
||||||
|
const contextType = context.getType();
|
||||||
|
if (contextType === 'graphql') {
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Check request URL and body for GraphQL endpoint
|
||||||
|
try {
|
||||||
|
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||||
|
// Check URL path
|
||||||
|
if (request.url?.includes('/graphql') || request.routerPath?.includes('/graphql')) {
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
// Check if request body contains GraphQL query/mutation
|
||||||
|
const body = request.body as any;
|
||||||
|
if (body && typeof body === 'object' && (body.query || body.mutation || body.operationName)) {
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not an HTTP context, might be GraphQL - skip wrapping
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Check if handler is a GraphQL resolver by checking the class name
|
||||||
|
const handler = context.getHandler();
|
||||||
|
const className = context.getClass()?.name || '';
|
||||||
|
const handlerName = handler?.name || '';
|
||||||
|
if (className.includes('Resolver') || handlerName.includes('Resolver')) {
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 4: Try to create GraphQL context - if it doesn't throw, it's GraphQL
|
||||||
|
try {
|
||||||
|
const gqlContext = GqlExecutionContext.create(context);
|
||||||
|
// If we can create it and it has GraphQL-specific properties, it's GraphQL
|
||||||
|
if (gqlContext && gqlContext.getInfo) {
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not a GraphQL context, continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// For REST endpoints, wrap the response
|
||||||
|
try {
|
||||||
|
const ctx = context.switchToHttp().getResponse<FastifyReply>();
|
||||||
|
const statusCode = ctx.statusCode;
|
||||||
|
|
||||||
|
// If statusCode is undefined, it might be a GraphQL request - skip wrapping
|
||||||
|
if (statusCode === undefined) {
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.handle().pipe(
|
||||||
|
map(data => {
|
||||||
|
// If data is already wrapped or doesn't need wrapping, return as-is
|
||||||
|
if (data && typeof data === 'object' && 'data' in data && 'success' in data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.data !== undefined) {
|
||||||
|
return {
|
||||||
|
statusCode,
|
||||||
|
success: true,
|
||||||
|
data: data.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
statusCode,
|
statusCode,
|
||||||
success: true,
|
success: true,
|
||||||
data: data.data,
|
data,
|
||||||
};
|
};
|
||||||
}
|
}),
|
||||||
return {
|
);
|
||||||
statusCode,
|
} catch {
|
||||||
success: true,
|
// If we can't get HTTP context either, return data as-is (likely GraphQL)
|
||||||
data,
|
return next.handle();
|
||||||
};
|
}
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { MikroOrmModule } from '@mikro-orm/nestjs';
|
|||||||
import { User } from '../users/entities/user.entity';
|
import { User } from '../users/entities/user.entity';
|
||||||
import { AdminAuthController } from './controllers/admin-auth.controller';
|
import { AdminAuthController } from './controllers/admin-auth.controller';
|
||||||
import { AdminAuthGuard } from './guards/adminAuth.guard';
|
import { AdminAuthGuard } from './guards/adminAuth.guard';
|
||||||
|
import { AuthResolver } from './graphql/resolvers/auth.resolver';
|
||||||
|
import { AdminAuthResolver } from './graphql/resolvers/admin-auth.resolver';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -32,7 +34,7 @@ import { AdminAuthGuard } from './guards/adminAuth.guard';
|
|||||||
MikroOrmModule.forFeature([User]),
|
MikroOrmModule.forFeature([User]),
|
||||||
],
|
],
|
||||||
controllers: [AuthController, AdminAuthController],
|
controllers: [AuthController, AdminAuthController],
|
||||||
providers: [AuthService, TokensService, AdminAuthGuard],
|
providers: [AuthService, TokensService, AdminAuthGuard, AuthResolver, AdminAuthResolver],
|
||||||
exports: [AdminAuthGuard],
|
exports: [AdminAuthGuard],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Inputs
|
||||||
|
export * from './inputs/request-otp.input';
|
||||||
|
export * from './inputs/verify-otp.input';
|
||||||
|
export * from './inputs/refresh-token.input';
|
||||||
|
|
||||||
|
// Objects
|
||||||
|
export * from './objects/token-response.object';
|
||||||
|
export * from './objects/restaurant-info.object';
|
||||||
|
export * from './objects/admin-auth-response.object';
|
||||||
|
export * from './objects/auth-response.object';
|
||||||
|
export * from './objects/otp-response.object';
|
||||||
|
|
||||||
|
// Resolvers
|
||||||
|
export * from './resolvers/auth.resolver';
|
||||||
|
export * from './resolvers/admin-auth.resolver';
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class RefreshTokenInput {
|
||||||
|
@Field(() => String, { description: 'Refresh token' })
|
||||||
|
@IsString({ message: 'Refresh token must be a string' })
|
||||||
|
@IsNotEmpty({ message: 'Refresh token is required' })
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class RequestOtpInput {
|
||||||
|
@Field(() => String, { description: 'Mobile number' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@IsMobilePhone('fa-IR')
|
||||||
|
phone: string;
|
||||||
|
|
||||||
|
@Field(() => String, { description: 'Restaurant slug' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
import { IsNotEmpty, IsString, Length, Matches } from 'class-validator';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class VerifyOtpInput {
|
||||||
|
@Field(() => String, { description: 'Mobile number' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^09\d{9}$/, {
|
||||||
|
message: 'Mobile number must be a valid Iranian phone number (e.g., 09123456789)',
|
||||||
|
})
|
||||||
|
phone: string;
|
||||||
|
|
||||||
|
@Field(() => String, { description: 'OTP code' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@Length(5)
|
||||||
|
otp: string;
|
||||||
|
|
||||||
|
@Field(() => String, { description: 'Restaurant slug' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { TokenResponse } from './token-response.object';
|
||||||
|
import { RestaurantInfo } from './restaurant-info.object';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class AdminInfo {
|
||||||
|
@Field(() => String)
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
firstName?: string;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
phone: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
role: string;
|
||||||
|
|
||||||
|
@Field(() => [String])
|
||||||
|
permissions: string[];
|
||||||
|
|
||||||
|
@Field(() => RestaurantInfo, { nullable: true })
|
||||||
|
restaurant?: RestaurantInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class AdminAuthResponse {
|
||||||
|
@Field(() => TokenResponse)
|
||||||
|
tokens: TokenResponse;
|
||||||
|
|
||||||
|
@Field(() => AdminInfo)
|
||||||
|
admin: AdminInfo;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { TokenResponse } from './token-response.object';
|
||||||
|
import { RestaurantInfo } from './restaurant-info.object';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class UserInfo {
|
||||||
|
@Field(() => String)
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
phone: string;
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
wallet: number;
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
points: number;
|
||||||
|
|
||||||
|
@Field(() => Boolean, { nullable: true })
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@Field(() => RestaurantInfo)
|
||||||
|
restaurant: RestaurantInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class AuthResponse {
|
||||||
|
@Field(() => TokenResponse)
|
||||||
|
tokens: TokenResponse;
|
||||||
|
|
||||||
|
@Field(() => UserInfo)
|
||||||
|
user: UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class OtpResponse {
|
||||||
|
@Field(() => String, { description: 'OTP code (for development/testing purposes)' })
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class RestaurantInfo {
|
||||||
|
@Field(() => String)
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class TokenInfo {
|
||||||
|
@Field(() => String)
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
expire: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class TokenResponse {
|
||||||
|
@Field(() => TokenInfo)
|
||||||
|
accessToken: TokenInfo;
|
||||||
|
|
||||||
|
@Field(() => TokenInfo)
|
||||||
|
refreshToken: TokenInfo;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Resolver, Mutation, Args } from '@nestjs/graphql';
|
||||||
|
import { AuthService } from '../../services/auth.service';
|
||||||
|
import { RequestOtpInput } from '../inputs/request-otp.input';
|
||||||
|
import { VerifyOtpInput } from '../inputs/verify-otp.input';
|
||||||
|
import { RefreshTokenInput } from '../inputs/refresh-token.input';
|
||||||
|
import { OtpResponse } from '../objects/otp-response.object';
|
||||||
|
import { AdminAuthResponse } from '../objects/admin-auth-response.object';
|
||||||
|
import { TokenResponse } from '../objects/token-response.object';
|
||||||
|
import { Throttle } from '@nestjs/throttler';
|
||||||
|
import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator';
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class AdminAuthResolver {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||||
|
@Mutation(() => OtpResponse, {
|
||||||
|
description: 'Request OTP for admin login',
|
||||||
|
})
|
||||||
|
async adminRequestOtp(@Args('input') input: RequestOtpInput): Promise<OtpResponse> {
|
||||||
|
const code = await this.authService.requestOtpAdmin(input);
|
||||||
|
return { code };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => AdminAuthResponse, {
|
||||||
|
description: 'Verify OTP code for admin login',
|
||||||
|
})
|
||||||
|
async adminVerifyOtp(@Args('input') input: VerifyOtpInput): Promise<AdminAuthResponse> {
|
||||||
|
return this.authService.verifyOtpAdmin(input.phone, input.slug, input.otp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RefreshTokenRateLimit()
|
||||||
|
@Mutation(() => TokenResponse, {
|
||||||
|
description: 'Refresh the admin access token and refresh token',
|
||||||
|
})
|
||||||
|
async adminRefreshToken(@Args('input') input: RefreshTokenInput): Promise<TokenResponse> {
|
||||||
|
return this.authService.refreshTokenAdmin(input.refreshToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Resolver, Mutation, Query, Args } from '@nestjs/graphql';
|
||||||
|
import { AuthService } from '../../services/auth.service';
|
||||||
|
import { RequestOtpInput } from '../inputs/request-otp.input';
|
||||||
|
import { VerifyOtpInput } from '../inputs/verify-otp.input';
|
||||||
|
import { RefreshTokenInput } from '../inputs/refresh-token.input';
|
||||||
|
import { OtpResponse } from '../objects/otp-response.object';
|
||||||
|
import { AuthResponse } from '../objects/auth-response.object';
|
||||||
|
import { TokenResponse } from '../objects/token-response.object';
|
||||||
|
import { Throttle } from '@nestjs/throttler';
|
||||||
|
import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator';
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class AuthResolver {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Query(() => String, {
|
||||||
|
description: 'Health check query',
|
||||||
|
})
|
||||||
|
async health(): Promise<string> {
|
||||||
|
return 'OK';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||||
|
@Mutation(() => OtpResponse, {
|
||||||
|
description: 'Request OTP for login or signup',
|
||||||
|
})
|
||||||
|
async requestOtp(@Args('input') input: RequestOtpInput): Promise<OtpResponse> {
|
||||||
|
return this.authService.requestOtp(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => AuthResponse, {
|
||||||
|
description: 'Verify OTP code',
|
||||||
|
})
|
||||||
|
async verifyOtp(@Args('input') input: VerifyOtpInput): Promise<AuthResponse> {
|
||||||
|
return this.authService.verifyOtp(input.phone, input.slug, input.otp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RefreshTokenRateLimit()
|
||||||
|
@Mutation(() => TokenResponse, {
|
||||||
|
description: 'Refresh the user access token and refresh token',
|
||||||
|
})
|
||||||
|
async refreshToken(@Args('input') input: RefreshTokenInput): Promise<TokenResponse> {
|
||||||
|
return this.authService.refreshToken(input.refreshToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user