Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): new authentication implements #897

Merged
merged 6 commits into from
Mar 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
maslow marked this conversation as resolved.
Show resolved Hide resolved
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
}
maslow marked this conversation as resolved.
Show resolved Hide resolved
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 {}
maslow marked this conversation as resolved.
Show resolved Hide resolved
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
}
}
maslow marked this conversation as resolved.
Show resolved Hide resolved
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
maslow marked this conversation as resolved.
Show resolved Hide resolved

@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
}
Loading