今天我們來加入authenication。
我們先安裝使用的套件,我們使用的Authenication Library為passport,並且我們使用JWT strategy作為我們認證方法
pnpm add @nestjs/passport @nestjs/jwt passport passport-jwt bcrypt graphql-scalars class-validator
在開始創建auth前,我們先創建User API
首先,先對我們的graphql schema進行修改
scalar DateTime
scalar JWT
input NewProductInput {
name: String!
price: Float!
description: String!
imageUrl: String!
}
input UpdateProductInput {
id: ID!
name: String!
price: Float!
description: String!
imageUrl: String!
}
type Product {
id: ID!
name: String!
price: Float!
description: String!
imageUrl: String!
}
type ProductEdge {
cursor: String!
node: Product!
}
type ProductConnection {
edges: [ProductEdge!]!
nodes: [Product!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PageInfo {
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
}
type CartItem {
productId: ID!
productName: String!
price: Float!
quantity: Int!
}
input CartItemInput {
productId: ID!
quantity: Int!
}
type Order {
id: ID!
items: [CartItem!]!
orderDate: DateTime!
}
type Auth {
accessToken: JWT!
refreshToken: JWT!
user: User!
}
input LoginInput {
email: String!
password: String!
}
input SignupInput {
email: String!
password: String!
username: String!
}
input RefreshTokenInput {
token: JWT!
}
input UpdateUserDataInput {
username: String
}
enum Role {
ADMIN
CREATOR
USER
}
type User {
createdAt: DateTime!
email: String!
username: String!
id: ID!
role: Role!
updatedAt: DateTime!
}
type Token {
accessToken: JWT!
refreshToken: JWT!
}
type Query {
me(userId: ID!): User!
getProducts(first: Int, after: String, last: Int, before: String): ProductConnection!
getProduct(id: ID!): Product
getUserProfile: User
getCartItems: [CartItem!]!
}
type Mutation {
login(data: LoginInput!): Auth!
signup(data: SignupInput!): Auth!
refreshToken(token: RefreshTokenInput!): Token!
updateUser(userId: ID!, newUserData: UpdateUserDataInput!): User!
addProduct(input: NewProductInput!): Product
updateProduct(input: UpdateProductInput!): Product
deleteProduct(id: ID!): Boolean
addCartItem(productId: ID!, quantity: Int!): [CartItem!]!
removeCartItem(productId: ID!): [CartItem!]!
updateCartItem(productId: ID!, quantity: Int!): [CartItem!]!
checkout(cartItems: [CartItemInput!]!): Order
}
然後執行pnpm exec nx run iron-ecommerce-server:gen-gql-type
重新生成type。
並且修改我們的prisma schema,打開apps\iron-ecommerce-server\prisma\schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Product {
id String @id @default(cuid())
name String
price Float
description String
imageUrl String
}
model User {
id String @id @default(uuid())
email String @unique
username String
password String // Encrypted password
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum Role {
ADMIN
USER
}
執行pnpm exec nx run iron-ecommerce-server:prisma-generate
和npm exec nx run iron-ecommerce-server:prisma-db-push
,並且restart ts server
。
接下來,先在我們的config加入security
///// apps\iron-ecommerce-server\src\common\configs\config.interface.ts
export interface Config {
nest: NestConfig;
cors: CorsConfig;
graphql: GraphqlConfig;
security: SecurityConfig;
}
export interface NestConfig {
port: number;
}
export interface CorsConfig {
enabled: boolean;
}
export interface GraphqlConfig {
codefirst: {
playgroundEnabled: boolean;
debug: boolean;
schemaDestination: string;
sortSchema: boolean;
};
schemafirst: {
playgroundEnabled: boolean;
typePaths: string[];
definitions: {
path: string;
};
};
}
export interface SecurityConfig {
expiresIn: string;
refreshIn: string;
bcryptSaltOrRound: string | number;
}
///// apps\iron-ecommerce-server\src\common\configs\config.interface.ts
export interface Config {
nest: NestConfig;
cors: CorsConfig;
graphql: GraphqlConfig;
security: SecurityConfig;
}
// ....
export interface SecurityConfig {
expiresIn: string;
refreshIn: string;
bcryptSaltOrRound: string | number;
}
接著我們開始實現我們的User API
///// apps\iron-ecommerce-server\src\api\auth\password.service.ts
import { SecurityConfig } from "../../common/configs/config.interface";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { compare, hash } from "bcrypt";
@Injectable()
export class PasswordService {
get bcryptSaltRounds(): string | number {
const securityConfig = this.configService.get<SecurityConfig>("security");
const saltOrRounds = securityConfig.bcryptSaltOrRound;
return Number.isInteger(Number(saltOrRounds)) ? Number(saltOrRounds) : saltOrRounds;
}
constructor(private configService: ConfigService) {}
validatePassword(password: string, hashedPassword: string): Promise<boolean> {
return compare(password, hashedPassword);
}
hashPassword(password: string): Promise<string> {
return hash(password, this.bcryptSaltRounds);
}
}
/// USER
///// apps\iron-ecommerce-server\src\api\users\users.model.ts
import { Field, HideField, ID, ObjectType, registerEnumType } from "@nestjs/graphql";
import "@prisma/client";
import { Role } from "@prisma/client";
import { IsEmail } from "class-validator";
registerEnumType(Role, {
name: "Role",
description: "User role"
});
@ObjectType()
export class User {
@Field(() => ID)
id: string;
@Field()
@IsEmail()
email: string;
@Field(() => String, { nullable: true })
firstname?: string;
@Field(() => String, { nullable: true })
lastname?: string;
@Field(() => Role)
role: Role;
@HideField()
password: string;
@Field({
description: "Identifies the date and time when the object was created."
})
createdAt: Date;
@Field({
description: "Identifies the date and time when the object was last updated."
})
updatedAt: Date;
}
///// apps\iron-ecommerce-server\src\api\users\users.service.ts
import { Injectable } from "@nestjs/common";
import { PrismaService } from "nestjs-prisma";
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async findUserById(userId: string) {
return this.prisma.user.findUnique({
where: { id: userId }
});
}
async updateUser(userId: string, newUserData: { firstname?: string; lastname?: string }) {
return this.prisma.user.update({
data: {
...newUserData,
updatedAt: new Date()
},
where: {
id: userId
}
});
}
}
///// apps\iron-ecommerce-server\src\api\users\users.resolver.ts
import { User } from "./users.model";
import { UsersService } from "./users.service";
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);
}
@Mutation(() => User)
async updateUser(
@Args("userId") userId: string,
@Args("newUserData") newUserData: { firstname?: string; lastname?: string }
) {
return this.usersService.updateUser(userId, newUserData);
}
}
///// apps\iron-ecommerce-server\src\api\users\users.module.ts
import { PasswordService } from "../auth/password.service";
import { UsersResolver } from "./users.resolver";
import { UsersService } from "./users.service";
import { Module } from "@nestjs/common";
@Module({
imports: [],
providers: [UsersResolver, UsersService, PasswordService]
})
export class UsersModule {}
並且將module加入到我們的app module
///// apps\iron-ecommerce-server\src\app\app.module.ts
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";
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true, load: [config] }), GraphQLSetupModule, ProductsModule, UsersModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
本篇已經創建了user相關的API,下一篇會繼續創建Auth相關的API