Skip to content

Commit

Permalink
feat(server): new authentication implements (#897)
Browse files Browse the repository at this point in the history
* feat(authentication): new authentication implements

* feat(authentication): impl bind username and phone

* feat(authentication): adjust to code review feedbacks

* feat(authentication): modify code according to cr-gpt

---------

Co-authored-by: maslow <wangfugen@126.com>
  • Loading branch information
sulnong and maslow committed Mar 19, 2023
1 parent a8f358c commit c4e3cf8
Show file tree
Hide file tree
Showing 25 changed files with 1,508 additions and 7 deletions.
335 changes: 331 additions & 4 deletions server/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@alicloud/dysmsapi20170525": "^2.0.23",
"@alicloud/openapi-client": "^0.4.5",
"@alicloud/tea-util": "^1.4.5",
"@aws-sdk/client-s3": "^3.245.0",
"@aws-sdk/client-sts": "^3.226.0",
"@kubernetes/client-node": "^0.17.1",
Expand Down
48 changes: 47 additions & 1 deletion server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,21 @@ model User {
personalAccessTokens PersonalAccessToken[]
}

model UserPassword {
id String @id @default(auto()) @map("_id") @db.ObjectId
uid String @db.ObjectId
password String
state String @default("Active") // Active, Inactive
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model UserProfile {
id String @id @default(auto()) @map("_id") @db.ObjectId
uid String @unique @db.ObjectId
openid String?
from String?
openData Json?
avatar String?
name String?
createdAt DateTime @default(now())
Expand All @@ -51,7 +61,7 @@ model PersonalAccessToken {
user User @relation(fields: [uid], references: [id])
}

// region schemas
// region schemas

type RegionClusterConf {
driver String // kubernetes
Expand Down Expand Up @@ -595,3 +605,39 @@ model WebsiteHosting {
bucket StorageBucket @relation(fields: [bucketName], references: [name])
}

enum AuthProviderState {
Enabled
Disabled
}

model AuthProvider {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String @unique
bind Json
register Boolean
default Boolean
state AuthProviderState
config Json
}

// Sms schemas
enum SmsVerifyCodeType {
Signin
Signup
ResetPassword
Bind
Unbind
ChangePhone
}

model SmsVerifyCode {
id String @id @default(auto()) @map("_id") @db.ObjectId
phone String
code String
ip String
type SmsVerifyCodeType
state Int @default(0) // 0: created, 1: used
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
25 changes: 23 additions & 2 deletions server/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SmsService } from 'src/auth/phone/sms.service'
import { Global, Module } from '@nestjs/common'
import { JwtModule } from '@nestjs/jwt'
import { PassportModule } from '@nestjs/passport'
Expand All @@ -9,6 +10,12 @@ import { JwtStrategy } from './jwt.strategy'
import { AuthController } from './auth.controller'
import { HttpModule } from '@nestjs/axios'
import { PatService } from 'src/user/pat.service'
import { UserPasswordController } from './user-passwd/user-password.controller'
import { UserPasswordService } from './user-passwd/user-password.service'
import { PhoneController } from './phone/phone.controller'
import { PhoneService } from './phone/phone.service'
import { AuthenticationController } from './authentication.controller'
import { AuthenticationService } from './authentication.service'

@Global()
@Module({
Expand All @@ -21,8 +28,22 @@ import { PatService } from 'src/user/pat.service'
UserModule,
HttpModule,
],
providers: [AuthService, JwtStrategy, CasdoorService, PatService],
providers: [
AuthService,
JwtStrategy,
CasdoorService,
PatService,
UserPasswordService,
PhoneService,
SmsService,
AuthenticationService,
],
exports: [AuthService],
controllers: [AuthController],
controllers: [
AuthController,
UserPasswordController,
PhoneController,
AuthenticationController,
],
})
export class AuthModule {}
105 changes: 105 additions & 0 deletions server/src/auth/authentication.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { AuthenticationService } from './authentication.service'
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common'
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'
import { ResponseUtil } from 'src/utils/response'
import { JwtAuthGuard } from './jwt.auth.guard'
import { BindUsernameDto } from './dto/bind-username.dto'
import { IRequest } from 'src/utils/interface'
import { BindPhoneDto } from './dto/bind-phone.dto'
import { SmsService } from './phone/sms.service'
import { SmsVerifyCodeType } from '@prisma/client'
import { UserService } from 'src/user/user.service'

@ApiTags('Authentication - New')
@Controller('auth')
export class AuthenticationController {
constructor(
private readonly authenticationService: AuthenticationService,
private readonly smsService: SmsService,
private readonly userService: UserService,
) {}

/**
* Auth providers
*/
@ApiOperation({ summary: 'Auth providers' })
@ApiResponse({ type: ResponseUtil })
@Get('providers')
async getProviders() {
const providers = await this.authenticationService.getProviders()
return ResponseUtil.ok(providers)
}

/**
* Bind phone
*/
@ApiOperation({ summary: 'Bind username' })
@ApiResponse({ type: ResponseUtil })
@UseGuards(JwtAuthGuard)
@Post('bind/phone')
async bindPhone(@Body() dto: BindPhoneDto, @Req() req: IRequest) {
const { phone, code } = dto
// check code valid
const err = await this.smsService.validCode(
phone,
code,
SmsVerifyCodeType.Bind,
)
if (err) {
return ResponseUtil.error(err)
}

// check phone if have already been bound
const user = await this.userService.find(phone)
if (user) {
return ResponseUtil.error('phone already been bound')
}

// bind phone
await this.userService.updateUser({
where: {
id: req.user.id,
},
data: {
phone,
},
})
}

/**
* Bind username, not support bind existed username
*/
@ApiOperation({ summary: 'Bind username' })
@ApiResponse({ type: ResponseUtil })
@UseGuards(JwtAuthGuard)
@Post('bind/username')
async bindUsername(@Body() dto: BindUsernameDto, @Req() req: IRequest) {
const { username, phone, code } = dto

// check code valid
const err = await this.smsService.validCode(
phone,
code,
SmsVerifyCodeType.Bind,
)
if (err) {
return ResponseUtil.error(err)
}

// check username if have already been bound
const user = await this.userService.find(username)
if (user) {
return ResponseUtil.error('username already been bound')
}

// bind username
await this.userService.updateUser({
where: {
id: req.user.id,
},
data: {
username,
},
})
}
}
64 changes: 64 additions & 0 deletions server/src/auth/authentication.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { JwtService } from '@nestjs/jwt'
import { PrismaService } from 'src/prisma/prisma.service'
import { Injectable, Logger } from '@nestjs/common'
import { AuthProviderState, User } from '@prisma/client'
import {
PASSWORD_AUTH_PROVIDER_NAME,
PHONE_AUTH_PROVIDER_NAME,
} from 'src/constants'

@Injectable()
export class AuthenticationService {
logger: Logger = new Logger(AuthenticationService.name)
constructor(
private readonly prismaService: PrismaService,
private readonly jwtService: JwtService,
) {}

/**
* Get all auth provides
* @returns
*/
async getProviders() {
return await this.prismaService.authProvider.findMany({
where: { state: AuthProviderState.Enabled },
select: {
id: false,
name: true,
bind: true,
register: true,
default: true,
state: true,
config: false,
},
})
}

async getPhoneProvider() {
return await this.getProvider(PHONE_AUTH_PROVIDER_NAME)
}

async getPasswdProvider() {
return await this.getProvider(PASSWORD_AUTH_PROVIDER_NAME)
}

// Get auth provider by name
async getProvider(name: string) {
return await this.prismaService.authProvider.findUnique({
where: { name },
})
}

/**
* Get access token by user
* @param user
* @returns
*/
getAccessTokenByUser(user: User): string {
const payload = {
sub: user.id,
}
const token = this.jwtService.sign(payload)
return token
}
}
21 changes: 21 additions & 0 deletions server/src/auth/dto/bind-phone.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsNotEmpty, IsString, Length, Matches } from 'class-validator'

export class BindPhoneDto {
@ApiProperty({
description: 'phone number',
example: '13805718888',
})
@IsString()
@IsNotEmpty()
@Matches(/^1[3-9]\d{9}$/)
phone: string

@ApiProperty({
description: 'sms verify code',
example: '032476',
})
@IsNotEmpty()
@Length(6, 6)
code: string
}
30 changes: 30 additions & 0 deletions server/src/auth/dto/bind-username.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsNotEmpty, IsString, Length, Matches } from 'class-validator'

export class BindUsernameDto {
@ApiProperty({
description: 'username',
example: 'laf-user',
})
@IsString()
@IsNotEmpty()
@Length(3, 64)
username: string

@ApiProperty({
description: 'phone',
example: '13805718888',
})
@IsString()
@IsNotEmpty()
@Matches(/^1[3-9]\d{9}$/)
phone: string

@ApiProperty({
description: 'sms verify code',
example: '032476',
})
@IsNotEmpty()
@Length(6, 6)
code: string
}
37 changes: 37 additions & 0 deletions server/src/auth/dto/passwd-reset.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ApiProperty } from '@nestjs/swagger'
import { SmsVerifyCodeType } from '@prisma/client'
import { IsEnum, IsNotEmpty, IsString, Length, Matches } from 'class-validator'

export class PasswdResetDto {
@ApiProperty({
description: 'new password, 8-64 characters',
example: 'laf-user-password',
})
@IsString()
@IsNotEmpty()
@Length(8, 64)
password: string

@ApiProperty({
description: 'phone',
example: '13805718888',
})
@IsString()
@Matches(/^1[3-9]\d{9}$/)
phone: string

@ApiProperty({
description: 'verify code',
example: '032456',
})
@IsString()
@Length(6, 6)
code: string

@ApiProperty({
description: 'type',
example: 'ResetPassword',
})
@IsEnum(SmsVerifyCodeType)
type: SmsVerifyCodeType
}

0 comments on commit c4e3cf8

Please sign in to comment.