Skip to content

Commit 7eef56f

Browse files
committed
feat: auth
1 parent 6a31f74 commit 7eef56f

File tree

17 files changed

+292
-23
lines changed

17 files changed

+292
-23
lines changed

apps/api/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"@nestjs/platform-express": "^10.0.0",
2929
"@nestjs/swagger": "^7.1.17",
3030
"argon2": "^0.31.2",
31+
"class-transformer": "^0.5.1",
32+
"class-validator": "^0.14.1",
3133
"reflect-metadata": "^0.1.13",
3234
"rxjs": "^7.8.1",
3335
"start": "^5.1.0"

apps/api/src/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
22
import { AppController } from './app.controller';
33
import { AppService } from './app.service';
44
import { GlobalModule } from './global/global.mudule';
5+
import { UserModule } from './user/user.module';
6+
import { AuthModule } from './auth/auth.module';
57

68
@Module({
7-
imports: [GlobalModule],
9+
imports: [GlobalModule, UserModule, AuthModule],
810
controllers: [AppController],
911
providers: [AppService],
1012
})

apps/api/src/auth/auth.controller.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {
2+
Controller,
3+
Post,
4+
Body,
5+
Get,
6+
UseGuards,
7+
Request,
8+
} from '@nestjs/common';
9+
import { SignDto } from './model/auth.dto';
10+
import { AuthService } from './auth.service';
11+
import { AuthGuard } from './auth.guard';
12+
import { CreateUserDto } from '../user/model/user.dto';
13+
14+
@Controller('auth')
15+
export class AuthController {
16+
constructor(private readonly authService: AuthService) {}
17+
18+
@Post('sign')
19+
sign(@Body() dto: SignDto) {
20+
return this.authService.signIn(dto);
21+
}
22+
23+
@Post('signup')
24+
signup(@Body() dto: CreateUserDto) {
25+
return this.authService.signup(dto);
26+
}
27+
28+
@UseGuards(AuthGuard)
29+
@Get('test')
30+
tets(@Request() req) {
31+
return req.user;
32+
}
33+
}

apps/api/src/auth/auth.guard.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {
2+
CanActivate,
3+
ExecutionContext,
4+
Injectable,
5+
UnauthorizedException,
6+
} from '@nestjs/common';
7+
import { JwtService } from '@nestjs/jwt';
8+
import { Request } from 'express';
9+
10+
@Injectable()
11+
export class AuthGuard implements CanActivate {
12+
constructor(private jwtService: JwtService) {}
13+
14+
async canActivate(context: ExecutionContext): Promise<boolean> {
15+
const request = context.switchToHttp().getRequest();
16+
const token = this.extractTokenFromHeader(request);
17+
if (!token) {
18+
throw new UnauthorizedException();
19+
}
20+
try {
21+
const payload = await this.jwtService.verifyAsync(token, {
22+
secret: process.env.SECRET,
23+
});
24+
// 💡 We're assigning the payload to the request object here
25+
// so that we can access it in our route handlers
26+
request['user'] = payload;
27+
} catch {
28+
throw new UnauthorizedException();
29+
}
30+
return true;
31+
}
32+
33+
private extractTokenFromHeader(request: Request): string | undefined {
34+
const [type, token] = request.headers.authorization?.split(' ') ?? [];
35+
return type === 'Bearer' ? token : undefined;
36+
}
37+
}

apps/api/src/auth/auth.module.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Module } from '@nestjs/common';
2+
import { AuthService } from './auth.service';
3+
import { AuthController } from './auth.controller';
4+
import { JwtModule } from '@nestjs/jwt';
5+
import { UserModule } from '../user/user.module';
6+
7+
@Module({
8+
imports: [
9+
UserModule,
10+
JwtModule.register({
11+
global: true,
12+
secret: process.env.SECRET,
13+
signOptions: { expiresIn: '7d' },
14+
}),
15+
],
16+
providers: [AuthService],
17+
controllers: [AuthController],
18+
})
19+
export class AuthModule {}

apps/api/src/auth/auth.service.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
HttpException,
3+
HttpStatus,
4+
Injectable,
5+
UnauthorizedException,
6+
} from '@nestjs/common';
7+
import { UserService } from '../user/user.service';
8+
import { JwtService } from '@nestjs/jwt';
9+
import { SignDto } from './model/auth.dto';
10+
import argon2 from 'argon2';
11+
import { CreateUserDto } from '../user/model/user.dto';
12+
13+
@Injectable()
14+
export class AuthService {
15+
constructor(
16+
private userService: UserService,
17+
private jwtService: JwtService,
18+
) {}
19+
20+
async signIn(dto: SignDto) {
21+
const user = await this.userService.findWithPhone(dto);
22+
if (!(await argon2.verify(user.password, dto.password))) {
23+
throw new UnauthorizedException();
24+
}
25+
26+
const payload = { userId: user.id, username: user.name };
27+
return {
28+
token: await this.jwtService.signAsync(payload),
29+
};
30+
}
31+
32+
async signup(dto: CreateUserDto) {
33+
const user = await this.userService.findWithPhone(dto);
34+
if (user) {
35+
throw new HttpException('User already exists', HttpStatus.BAD_REQUEST);
36+
}
37+
38+
const res = await this.userService.createUser(dto);
39+
const payload = { userId: res.id, username: dto.name };
40+
return {
41+
token: await this.jwtService.signAsync(payload),
42+
};
43+
}
44+
}

apps/api/src/auth/model/auth.dto.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { PickType } from '@nestjs/swagger';
2+
import '@nestjs/common';
3+
import { CreateUserDto } from '../../user/model/user.dto';
4+
5+
export class SignDto extends PickType(CreateUserDto, ['phone', 'password']) {}

apps/api/src/global/providers/db.provider.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@ import { type MySql2Database } from 'drizzle-orm/mysql2';
33
import { drizzle } from 'drizzle-orm/mysql2';
44
import * as mysql from 'mysql2/promise';
55
import { DefaultLogger, LogWriter } from 'drizzle-orm';
6+
import { schemas } from '@earthwrom/shared';
67

78
export const DB = Symbol('DB_SERVICE');
8-
export type DbType = MySql2Database;
9+
export type DbType = MySql2Database<typeof schemas>;
910

1011
const env = process.env;
1112

1213
export const DbProvider: FactoryProvider<DbType> = {
1314
provide: DB,
14-
useFactory: () => {
15+
useFactory: async () => {
1516
const logger = new Logger('DB');
1617

1718
logger.debug(`Connecting to ${env.DATABASE_URL}`);
19+
logger.debug(`SECRET: ${env.SECRET}`);
1820

19-
const connection = mysql.createPool({
21+
const connection = await mysql.createConnection({
2022
uri: env.DATABASE_URL,
2123
multipleStatements: true,
2224
waitForConnections: true,
@@ -36,8 +38,11 @@ export const DbProvider: FactoryProvider<DbType> = {
3638
}
3739
}
3840

39-
return drizzle(connection, {
41+
const db = drizzle(connection, {
42+
schema: schemas,
4043
logger: new DefaultLogger({ writer: new CustomDbLogWriter() }),
44+
mode: 'planetscale',
4145
});
46+
return db;
4247
},
4348
};

apps/api/src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { NestFactory } from '@nestjs/core';
22
import { AppModule } from './app.module';
33
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
4+
import { ValidationPipe } from '@nestjs/common';
45

56
async function bootstrap() {
67
const app = await NestFactory.create(AppModule);
78

9+
app.useGlobalPipes(new ValidationPipe());
810
const config = new DocumentBuilder()
911
.setTitle('Cats example')
1012
.setDescription('The cats API description')

apps/api/src/user/model/user.dto.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, Length } from 'class-validator';
3+
4+
export class CreateUserDto {
5+
@ApiProperty()
6+
@IsNotEmpty({ message: ' 用户名不能为空' })
7+
@Length(1, 8, { message: '用户名长度为1-8位' })
8+
name: string;
9+
10+
@ApiProperty()
11+
@IsNotEmpty({ message: '手机号码不能为空' })
12+
@Length(11, 11, { message: '手机号码长度为11位' })
13+
phone: string;
14+
15+
@ApiProperty()
16+
@IsNotEmpty({ message: '密码不能为空' })
17+
@Length(6, 20, { message: '密码长度为6-20位' })
18+
password: string;
19+
}
20+
21+
export class FindUserDto {
22+
@ApiProperty()
23+
@IsNotEmpty({ message: '手机号码不能为空' })
24+
@Length(11, 11, { message: '手机号码长度为11位' })
25+
phone: string;
26+
}

apps/api/src/user/user.controller.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Controller, Get, Post, Body } from '@nestjs/common';
2+
import { UserService } from './user.service';
3+
import { CreateUserDto } from './model/user.dto';
4+
5+
@Controller('user')
6+
export class UserController {
7+
constructor(private readonly userService: UserService) {}
8+
}

apps/api/src/user/user.module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Module } from '@nestjs/common';
2+
import { UserService } from './user.service';
3+
import { UserController } from './user.controller';
4+
5+
@Module({
6+
providers: [UserService],
7+
controllers: [UserController],
8+
exports: [UserService],
9+
})
10+
export class UserModule {}

apps/api/src/user/user.service.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Inject, Injectable } from '@nestjs/common';
2+
import { CreateUserDto, FindUserDto } from './model/user.dto';
3+
import { DB, DbType } from '../global/providers/db.provider';
4+
import { user } from '@earthwrom/shared';
5+
import { eq } from 'drizzle-orm';
6+
import * as argon2 from 'argon2';
7+
8+
@Injectable()
9+
export class UserService {
10+
constructor(@Inject(DB) private db: DbType) {}
11+
12+
async createUser(dto: CreateUserDto) {
13+
const [res] = await this.db.insert(user).values({
14+
...dto,
15+
password: await argon2.hash(dto.password),
16+
});
17+
return {
18+
id: res.insertId,
19+
};
20+
}
21+
22+
async findWithPhone(dto: FindUserDto) {
23+
return this.db.query.user.findFirst({
24+
where: eq(user.phone, dto.phone),
25+
});
26+
}
27+
}

libs/shared/src/schema/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1+
import { course } from "./course";
2+
import { statement } from "./statement";
3+
import { user } from "./user";
4+
import { userProgress } from "./userProgress";
5+
16
export * from "./course";
27
export * from "./statement";
38
export * from "./user";
49
export * from "./userProgress";
10+
export const schemas = {
11+
course,
12+
statement,
13+
user,
14+
userProgress,
15+
};

libs/shared/src/schema/user.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
import { int, mysqlTable, text, timestamp } from "drizzle-orm/mysql-core";
1+
import {
2+
int,
3+
mysqlTable,
4+
text,
5+
timestamp,
6+
varchar,
7+
} from "drizzle-orm/mysql-core";
28

3-
export const user = mysqlTable("courses", {
9+
export const user = mysqlTable("users", {
410
id: int("id").autoincrement().primaryKey(),
5-
phone: text("phone").notNull().unique(),
11+
phone: varchar("phone", { length: 11 }).notNull().unique(),
612
name: text("name").notNull(),
713
password: text("password").notNull(),
814
createdAt: timestamp("created_at").notNull().defaultNow(),
9-
updatedAt: timestamp("updated_at").notNull().onUpdateNow(),
15+
updatedAt: timestamp("updated_at").onUpdateNow(),
1016
});

0 commit comments

Comments
 (0)