現在我們要在我們的User上實現authenication。
首先,在common新建一個guard
///// apps\iron-ecommerce-server\src\common\auth\gql-auth.guard.ts
import { ExecutionContext, Injectable } from "@nestjs/common";
import { GqlExecutionContext } from "@nestjs/graphql";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class GqlAuthGuard extends AuthGuard("jwt") {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
接著設定一下環境變數,打開.env
加入
JWT_ACCESS_SECRET=nestjsPrismaAccessSecret
JWT_REFRESH_SECRET=nestjsPrismaRefreshSecret
以及創建新api來處理auth
///// apps\iron-ecommerce-server\src\api\auth\jwt-strategy.ts
import { AuthService } from "./auth.service";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { User } from "@prisma/client";
import { ExtractJwt, Strategy } from "passport-jwt";
export interface JwtDto {
userId: string;
// Issued at
iat: number;
exp: number;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService, readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get("JWT_ACCESS_SECRET")
});
}
async validate(payload: JwtDto): Promise<User> {
const user = await this.authService.validateUser(payload.userId);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
///// apps\iron-ecommerce-server\src\api\auth\auth.model.ts
import { Field, ObjectType } from "@nestjs/graphql";
import { User } from "@prisma/client";
import { GraphQLJWT } from "graphql-scalars";
@ObjectType()
export class Token {
@Field(() => GraphQLJWT, { description: "JWT access token" })
accessToken: string;
@Field(() => GraphQLJWT, { description: "JWT refresh token" })
refreshToken: string;
}
@ObjectType()
export class Auth extends Token {
user: User;
}
///// apps\iron-ecommerce-server\src\api\auth\auth.input.ts
import { ArgsType, Field, InputType } from "@nestjs/graphql";
import { IsEmail, IsJWT, IsNotEmpty, MinLength } from "class-validator";
import { GraphQLJWT } from "graphql-scalars";
@InputType()
export class LoginInput {
@Field()
@IsEmail()
email: string;
@Field()
@IsNotEmpty()
@MinLength(8)
password: string;
}
@ArgsType()
export class RefreshTokenInput {
@IsNotEmpty()
@IsJWT()
@Field(() => GraphQLJWT)
token: string;
}
@InputType()
export class SignupInput {
@Field()
@IsEmail()
email: string;
@Field()
@IsNotEmpty()
@MinLength(8)
password: string;
@Field()
@IsNotEmpty()
@MinLength(4)
username: string;
@Field({ nullable: true })
firstname?: string;
@Field({ nullable: true })
lastname?: string;
}
///// apps\iron-ecommerce-server\src\api\auth\auth.service.ts
import { SecurityConfig } from "../../common/configs/config.interface";
import { SignupInput } from "./auth.input";
import { Token } from "./auth.model";
import { PasswordService } from "./password.service";
import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
UnauthorizedException
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { Prisma, User } from "@prisma/client";
import { PrismaService } from "nestjs-prisma";
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly prisma: PrismaService,
private readonly passwordService: PasswordService,
private readonly configService: ConfigService
) {}
async createUser(payload: SignupInput): Promise<Token> {
const hashedPassword = await this.passwordService.hashPassword(payload.password);
try {
const user = await this.prisma.user.create({
data: {
...payload,
firstname: payload.username,
password: hashedPassword,
role: "USER"
}
});
return this.generateTokens({
userId: user.id
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {
throw new ConflictException(`Email ${payload.email} already used.`);
} else {
throw new Error(e);
}
}
}
async login(email: string, password: string): Promise<Token> {
const user = await this.prisma.user.findUnique({ where: { email } });
if (!user) {
throw new NotFoundException(`No user found for email: ${email}`);
}
const passwordValid = await this.passwordService.validatePassword(password, user.password);
if (!passwordValid) {
throw new BadRequestException("Invalid password");
}
return this.generateTokens({
userId: user.id
});
}
validateUser(userId: string): Promise<User> {
return this.prisma.user.findUnique({ where: { id: userId } });
}
getUserFromToken(token: string): Promise<User> {
const id = this.jwtService.decode(token)["userId"];
return this.prisma.user.findUnique({ where: { id } });
}
generateTokens(payload: { userId: string }): Token {
return {
accessToken: this.generateAccessToken(payload),
refreshToken: this.generateRefreshToken(payload)
};
}
private generateAccessToken(payload: { userId: string }): string {
return this.jwtService.sign(payload);
}
private generateRefreshToken(payload: { userId: string }): string {
const securityConfig = this.configService.get<SecurityConfig>("security");
return this.jwtService.sign(payload, {
secret: this.configService.get("JWT_REFRESH_SECRET"),
expiresIn: securityConfig.refreshIn
});
}
refreshToken(token: string) {
try {
const { userId } = this.jwtService.verify(token, {
secret: this.configService.get("JWT_REFRESH_SECRET")
});
return this.generateTokens({
userId
});
} catch (e) {
throw new UnauthorizedException();
}
}
}
///// apps\iron-ecommerce-server\src\api\auth\auth.resolver.ts
import { User } from "../users/users.model";
import { LoginInput, RefreshTokenInput, SignupInput } from "./auth.input";
import { Auth, Token } from "./auth.model";
import { AuthService } from "./auth.service";
import { Args, Mutation, Parent, ResolveField, Resolver } from "@nestjs/graphql";
@Resolver(() => Auth)
export class AuthResolver {
constructor(private readonly auth: AuthService) {}
@Mutation(() => Auth)
async signup(@Args("data") data: SignupInput) {
data.email = data.email.toLowerCase();
const { accessToken, refreshToken } = await this.auth.createUser(data);
return {
accessToken,
refreshToken
};
}
@Mutation(() => Auth)
async login(@Args("data") { email, password }: LoginInput) {
const { accessToken, refreshToken } = await this.auth.login(email.toLowerCase(), password);
return {
accessToken,
refreshToken
};
}
@Mutation(() => Token)
async refreshToken(@Args() { token }: RefreshTokenInput) {
return this.auth.refreshToken(token);
}
@ResolveField("user", () => User)
async user(@Parent() auth: Auth) {
return await this.auth.getUserFromToken(auth.accessToken);
}
}
///// apps\iron-ecommerce-server\src\api\auth\auth.module.ts
import { GqlAuthGuard } from "../../common/auth/gql-auth.guard";
import { SecurityConfig } from "../../common/configs/config.interface";
import { AuthResolver } from "./auth.resolver";
import { AuthService } from "./auth.service";
import { JwtStrategy } from "./jwt-strategy";
import { PasswordService } from "./password.service";
import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
@Module({
imports: [
PassportModule.register({ defaultStrategy: "jwt" }),
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => {
const securityConfig = configService.get<SecurityConfig>("security");
return {
secret: configService.get<string>("JWT_ACCESS_SECRET"),
signOptions: {
expiresIn: securityConfig.expiresIn
}
};
},
inject: [ConfigService]
})
],
providers: [AuthService, AuthResolver, JwtStrategy, GqlAuthGuard, PasswordService],
exports: [GqlAuthGuard]
})
export class AuthModule {}
並將AuthModule加入到App Module裡面
///// apps\iron-ecommerce-server\src\app\app.module.ts
import { AuthModule } from "../api/auth/auth.module";
import { ProductsModule } from "../api/products/products.module";
import { UsersModule } from "../api/users/users.module";
import config from "../common/configs/config";
import { GraphQLSetupModule } from "../graphql/graphql-setup.module";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { PrismaModule } from "nestjs-prisma";
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [config] }),
PrismaModule.forRoot({
isGlobal: true,
prismaServiceOptions: {
middlewares: []
}
}),
GraphQLSetupModule,
ProductsModule,
UsersModule,
AuthModule
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
接下來我們加入guard。
///// apps\iron-ecommerce-server\src\api\users\users.resolver.ts
import { GqlAuthGuard } from "../../common/auth/gql-auth.guard";
import { User } from "./users.model";
import { UsersService } from "./users.service";
import { UseGuards } from "@nestjs/common";
import { Args, Mutation, Query, Resolver } from "@nestjs/graphql";
@Resolver(() => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query(() => User)
async me(@Args("userId") userId: string): Promise<User> {
return this.usersService.findUserById(userId);
}
@UseGuards(GqlAuthGuard)
@Mutation(() => User)
async updateUser(
@Args("userId") userId: string,
@Args("newUserData") newUserData: { firstname?: string; lastname?: string }
) {
return this.usersService.updateUser(userId, newUserData);
}
}
接下來進行測試
mutation {
signup(data: {
email: "test@test.com"
username: "tester"
password: "test"
}) {
user {
username
email
}
}
}
//// response
{
"data": {
"signup": {
"user": {
"username": "tester",
"email": "test@test.com"
}
}
}
}
能看到創建了user
現在我們進行登入
mutation {
login(data: {
email: "test@test.com"
password: "test"
}) {
user {
username
role
}
refreshToken
accessToken
}
}
///// response
{
"data": {
"login": {
"user": {
"username": "tester",
"role": "USER"
},
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIyOThkMDkyNy00NGEzLTQ2OTAtYjcxZC0yMmFhZDE5YjRlNDMiLCJpYXQiOjE2OTcxMjU0NDAsImV4cCI6MTY5NzczMDI0MH0.hGFRNGL89TKhibnxQtUwuRgYbqZDFb4Os8qTVaPJ5Ik",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIyOThkMDkyNy00NGEzLTQ2OTAtYjcxZC0yMmFhZDE5YjRlNDMiLCJpYXQiOjE2OTcxMjU0NDAsImV4cCI6MTY5NzEyNTU2MH0.TD2tq7LhMW-uSGfBFRoDOOWufnEY80mlVA20eaqSuUM"
}
}
}
現在我們進行測試,試著不使用accessToken來執行updateUser
mutation {
updateUser(
userId: "298d0927-44a3-4690-b71d-22aad19b4e43"
newUserData: {
username: "updated"
}) {
username
}
}
接下來使用
mutation {
updateUser(
userId: "298d0927-44a3-4690-b71d-22aad19b4e43"
newUserData: {
username: "updated"
}) {
username
}
}
///// header
{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIyOThkMDkyNy00NGEzLTQ2OTAtYjcxZC0yMmFhZDE5YjRlNDMiLCJpYXQiOjE2OTcxMjU0NDAsImV4cCI6MTY5NzEyNTU2MH0.TD2tq7LhMW-uSGfBFRoDOOWufnEY80mlVA20eaqSuUM"
}
我們能看到可以更新了,現在打開SQLTools觀看