Skip to content

Commit cd8222e

Browse files
committed
feat: v5.3.0
1 parent b1461bb commit cd8222e

File tree

7 files changed

+68
-38
lines changed

7 files changed

+68
-38
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
### 5.3.0 (2025-07-16)
6+
7+
**Feature**
8+
9+
- Improved password hash algorithm, using a more secure `bcrypt`
10+
511
### 5.1.0 (2025-07-16)
612

713
**Feature**

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nodepress",
3-
"version": "5.2.0",
3+
"version": "5.3.0",
44
"description": "RESTful API service for Surmon.me blog",
55
"author": "Surmon",
66
"license": "MIT",
@@ -43,6 +43,7 @@
4343
"@typegoose/typegoose": "^12.17.x",
4444
"akismet-api": "^6.x",
4545
"axios": "^1.10.x",
46+
"bcryptjs": "^3.0.x",
4647
"class-transformer": "^0.5.1",
4748
"class-validator": "^0.14.x",
4849
"cross-env": "^7.x",

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.config.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,21 @@ export const APP_BIZ = {
5151
STATIC_URL: 'https://static.surmon.me',
5252
/** Allowed CORS origins */
5353
CORS_ALLOWED_ORIGINS: ['https://surmon.me', /\.surmon\.me$/],
54-
/** Authentication config */
55-
AUTH: {
54+
/** Default admin password */
55+
PASSWORD: {
56+
/** Default password for the admin user, used when no password is set in the database */
57+
defaultPassword: arg({ key: 'default_password', default: 'root' }),
58+
/** Bcrypt salt rounds for hashing passwords */
59+
bcryptSaltRounds: arg({ key: 'bcrypt_salt_rounds', default: 10 })
60+
},
61+
/** Authentication config (JSON Web Token) */
62+
AUTH_JWT: {
5663
/** JWT token expiration time in seconds */
57-
expiresIn: arg({ key: 'auth_expires_in', default: 3600 }),
64+
expiresIn: arg({ key: 'auth_jwt_expires_in', default: 3600 }),
5865
/** JWT signing secret; must be secure in production */
59-
jwtSecret: arg({ key: 'auth_secret', default: 'nodepress' }),
60-
/** Default payload for issued tokens */
61-
data: arg<any>({ key: 'auth_data', default: { user: 'root' } }),
62-
/** Default admin password */
63-
defaultPassword: arg({ key: 'auth_default_password', default: 'root' })
66+
secret: arg({ key: 'auth_jwt_secret', default: 'nodepress' }),
67+
/** Default payload for issued JWT tokens (e.g., user role or ID) */
68+
data: arg<any>({ key: 'auth_jwt_data', default: { user: 'root' } })
6469
}
6570
}
6671

src/core/auth/auth.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import { AuthService } from './auth.service'
1515
// https://docs.nestjs.com/security/authentication#jwt-token
1616
JwtModule.register({
1717
global: true,
18-
secret: APP_BIZ.AUTH.jwtSecret,
19-
signOptions: { expiresIn: APP_BIZ.AUTH.expiresIn }
18+
secret: APP_BIZ.AUTH_JWT.secret,
19+
signOptions: { expiresIn: APP_BIZ.AUTH_JWT.expiresIn }
2020
})
2121
],
2222
providers: [AuthService],

src/core/auth/auth.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class AuthService {
3636
}
3737

3838
public signToken() {
39-
return this.jwtService.sign({ data: APP_BIZ.AUTH.data })
39+
return this.jwtService.sign({ data: APP_BIZ.AUTH_JWT.data })
4040
}
4141

4242
public async verifyToken(token: string): Promise<boolean> {
@@ -45,8 +45,8 @@ export class AuthService {
4545
}
4646

4747
try {
48-
const payload = this.jwtService.verify(token, { secret: APP_BIZ.AUTH.jwtSecret })
49-
return _isEqual(payload.data, APP_BIZ.AUTH.data)
48+
const payload = this.jwtService.verify(token, { secret: APP_BIZ.AUTH_JWT.secret })
49+
return _isEqual(payload.data, APP_BIZ.AUTH_JWT.data)
5050
} catch {
5151
return false
5252
}

src/modules/admin/admin.service.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
* @module module/admin/service
44
*/
55

6+
import bcrypt from 'bcryptjs'
67
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common'
78
import { InjectModel } from '@app/transformers/model.transformer'
89
import { MongooseModel } from '@app/interfaces/mongoose.interface'
910
import { AuthService } from '@app/core/auth/auth.service'
10-
import { decodeBase64, decodeMD5 } from '@app/transformers/codec.transformer'
11+
import { decodeBase64 } from '@app/transformers/codec.transformer'
1112
import { Admin, DEFAULT_ADMIN_PROFILE } from './admin.model'
1213
import { TokenResult } from './admin.interface'
1314
import { AdminUpdateDTO } from './admin.dto'
@@ -20,22 +21,31 @@ export class AdminService {
2021
@InjectModel(Admin) private readonly adminModel: MongooseModel<Admin>
2122
) {}
2223

23-
private async getExistedPassword(): Promise<string> {
24-
const auth = await this.adminModel.findOne(undefined, '+password').exec()
25-
return auth?.password || decodeMD5(APP_BIZ.AUTH.defaultPassword)
24+
/**
25+
* Validate the provided plain password against the stored password.
26+
* - If a hashed password exists in the database, verify with bcrypt.
27+
* - If no password exists (e.g., initial state), fall back to comparing with the default password.
28+
*/
29+
private async validatePassword(plainPassword: string): Promise<boolean> {
30+
const existedProfile = await this.adminModel.findOne(undefined, '+password').exec()
31+
if (existedProfile?.password) {
32+
// Password exists in database → validate using bcrypt
33+
return await bcrypt.compare(plainPassword, existedProfile.password)
34+
} else {
35+
// No password in database → compare directly with default password (no hashing)
36+
return plainPassword === APP_BIZ.PASSWORD.defaultPassword
37+
}
2638
}
2739

2840
public createToken(): TokenResult {
2941
return {
3042
access_token: this.authService.signToken(),
31-
expires_in: APP_BIZ.AUTH.expiresIn
43+
expires_in: APP_BIZ.AUTH_JWT.expiresIn
3244
}
3345
}
3446

35-
public async login(password: string): Promise<TokenResult> {
36-
const existedPassword = await this.getExistedPassword()
37-
const loginPassword = decodeMD5(decodeBase64(password))
38-
if (loginPassword === existedPassword) {
47+
public async login(base64Password: string): Promise<TokenResult> {
48+
if (await this.validatePassword(decodeBase64(base64Password))) {
3949
return this.createToken()
4050
} else {
4151
throw new UnauthorizedException('Password incorrect')
@@ -48,32 +58,31 @@ export class AdminService {
4858
}
4959

5060
public async updateProfile(adminProfile: AdminUpdateDTO): Promise<Admin> {
51-
const { password, new_password, ...profile } = adminProfile
52-
const payload: Admin = { ...profile }
61+
const { password: inputOldPassword, new_password: inputNewPassword, ...profile } = adminProfile
62+
const newProfile: Admin = { ...profile }
5363

54-
// verify password
55-
if (password || new_password) {
56-
if (!password || !new_password) {
64+
// Verify password
65+
if (inputOldPassword || inputNewPassword) {
66+
if (!inputOldPassword || !inputNewPassword) {
5767
throw new BadRequestException('Incomplete passwords')
5868
}
59-
if (password === new_password) {
69+
if (inputOldPassword === inputNewPassword) {
6070
throw new BadRequestException('Old password and new password cannot be the same')
6171
}
62-
const oldPassword = decodeMD5(decodeBase64(password))
63-
const existedPassword = await this.getExistedPassword()
64-
if (oldPassword !== existedPassword) {
72+
if (!(await this.validatePassword(decodeBase64(inputOldPassword)))) {
6573
throw new BadRequestException('Old password incorrect')
6674
}
67-
// set new password
68-
payload.password = decodeMD5(decodeBase64(new_password))
75+
// Set new password
76+
const plainNewPassword = decodeBase64(inputNewPassword)
77+
newProfile.password = await bcrypt.hash(plainNewPassword, APP_BIZ.PASSWORD.bcryptSaltRounds)
6978
}
7079

7180
// save
72-
const existedAuth = await this.adminModel.findOne(undefined, '+password').exec()
73-
if (existedAuth) {
74-
await Object.assign(existedAuth, payload).save()
81+
const existedProfile = await this.adminModel.findOne(undefined, '+password').exec()
82+
if (existedProfile) {
83+
await Object.assign(existedProfile, newProfile).save()
7584
} else {
76-
await this.adminModel.create(payload)
85+
await this.adminModel.create(newProfile)
7786
}
7887

7988
return this.getProfile()

0 commit comments

Comments
 (0)