fix: login secret (#6635)

* fix: login secret

* lock

* env template

* fix: ts

* fix: ts

* fix: ts
This commit is contained in:
Archer
2026-03-25 14:45:38 +08:00
committed by GitHub
parent e48a037f2d
commit bd966d479f
54 changed files with 2061 additions and 500 deletions
@@ -0,0 +1,165 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as checkPswExpiredApi from '@/pages/api/support/user/account/checkPswExpired';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import { Call } from '@test/utils/request';
describe('checkPswExpired API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
const originalEnv = process.env.PASSWORD_EXPIRED_MONTH;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'testpassword',
status: UserStatusEnum.active,
passwordUpdateTime: new Date()
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
vi.clearAllMocks();
});
afterEach(() => {
process.env.PASSWORD_EXPIRED_MONTH = originalEnv;
});
it('should return false when PASSWORD_EXPIRED_MONTH is not set', async () => {
delete process.env.PASSWORD_EXPIRED_MONTH;
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(false);
});
it('should return false when PASSWORD_EXPIRED_MONTH=0', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '0';
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(false);
});
it('should return false when password was updated recently', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '3';
// Update password time to now
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: new Date()
});
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(false);
});
it('should return true when password has expired (update time older than expiry period)', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '1';
// Set password update time to 2 months ago
const twoMonthsAgo = new Date();
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: twoMonthsAgo
});
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(true);
});
it('should return true when passwordUpdateTime is not set and env is configured', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '3';
// Remove passwordUpdateTime
await MongoUser.findByIdAndUpdate(testUser._id, {
$unset: { passwordUpdateTime: '' }
});
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(true);
});
it('should return false when user is not found', async () => {
const nonExistentId = '000000000000000000000001';
const res = await Call(checkPswExpiredApi.default, {
auth: {
userId: nonExistentId,
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBe(false);
});
it('should reject request without authentication', async () => {
const res = await Call(checkPswExpiredApi.default, {});
expect(res.code).toBe(500);
});
});
@@ -5,14 +5,12 @@ import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { authCode } from '@fastgpt/service/support/user/auth/controller';
import { createUserSession } from '@fastgpt/service/support/user/session';
import { setCookie } from '@fastgpt/service/support/permission/auth/common';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { UserErrEnum } from '@fastgpt/global/common/error/code/user';
import { Call } from '@test/utils/request';
import type { PostLoginProps } from '@fastgpt/global/support/user/api';
import type { LoginByPasswordBodyType } from '@fastgpt/global/openapi/support/user/account/login/api';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
describe('loginByPassword API', () => {
@@ -21,7 +19,6 @@ describe('loginByPassword API', () => {
let testTmb: any;
beforeEach(async () => {
// Create test user and team
testUser = await MongoUser.create({
username: 'testuser',
password: 'testpassword',
@@ -48,12 +45,11 @@ describe('loginByPassword API', () => {
lastLoginTmbId: testTmb._id
});
// Reset mocks before each test
vi.clearAllMocks();
});
it('should login successfully with valid credentials', async () => {
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'testpassword',
@@ -64,83 +60,66 @@ describe('loginByPassword API', () => {
expect(res.code).toBe(200);
expect(res.error).toBeUndefined();
expect(res.data).toBeDefined();
expect(res.data.user).toBeDefined();
expect(res.data.user.team).toBeDefined();
expect(res.data.user.team.teamId).toBe(String(testTeam._id));
expect(res.data.user.team.tmbId).toBe(String(testTmb._id));
expect(res.data.token).toBeDefined();
expect(typeof res.data.token).toBe('string');
expect(res.data.token.length).toBeGreaterThan(0);
// Verify authCode was called
expect(authCode).toHaveBeenCalledWith({
key: 'testuser',
code: '123456',
type: expect.any(String)
});
// Verify setCookie was called
expect(setCookie).toHaveBeenCalled();
// Verify tracking was called
expect(pushTrack.login).toHaveBeenCalledWith({
type: 'password',
uid: testUser._id,
teamId: String(testTeam._id),
tmbId: String(testTmb._id)
});
// Verify audit log was called
expect(addAuditLog).toHaveBeenCalled();
});
it('should reject login when username is missing', async () => {
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
it('should reject login when username is empty', async () => {
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
body: {
username: '',
password: 'testpassword',
code: '123456'
code: '123456',
language: 'zh-CN'
}
});
expect(res.code).toBe(500);
expect(res.error).toBe(CommonErrEnum.invalidParams);
});
it('should reject login when password is missing', async () => {
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
it('should reject login when password is empty', async () => {
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: '',
code: '123456'
code: '123456',
language: 'zh-CN'
}
});
// Empty password passes zod z.string() but won't match any user record
expect(res.code).toBe(500);
expect(res.error).toBe(CommonErrEnum.invalidParams);
});
it('should reject login when code is missing', async () => {
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'testpassword',
code: ''
}
});
expect(res.code).toBe(500);
expect(res.error).toBe(CommonErrEnum.invalidParams);
expect(res.error).toBe(UserErrEnum.account_psw_error);
});
it('should reject login when auth code verification fails', async () => {
// Mock authCode to reject
vi.mocked(authCode).mockRejectedValueOnce(new Error('Invalid code'));
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'testpassword',
code: 'wrongcode'
code: 'wrongcode',
language: 'zh-CN'
}
});
@@ -149,11 +128,12 @@ describe('loginByPassword API', () => {
});
it('should reject login when user does not exist', async () => {
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
body: {
username: 'nonexistentuser',
password: 'testpassword',
code: '123456'
code: '123456',
language: 'zh-CN'
}
});
@@ -162,16 +142,16 @@ describe('loginByPassword API', () => {
});
it('should reject login when user is forbidden', async () => {
// Update user status to forbidden
await MongoUser.findByIdAndUpdate(testUser._id, {
status: UserStatusEnum.forbidden
});
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'testpassword',
code: '123456'
code: '123456',
language: 'zh-CN'
}
});
@@ -180,11 +160,12 @@ describe('loginByPassword API', () => {
});
it('should reject login when password is incorrect', async () => {
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'wrongpassword',
code: '123456'
code: '123456',
language: 'zh-CN'
}
});
@@ -192,8 +173,8 @@ describe('loginByPassword API', () => {
expect(res.error).toBe(UserErrEnum.account_psw_error);
});
it('should accept language parameter on successful login', async () => {
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
it('should update language on successful login', async () => {
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'testpassword',
@@ -203,16 +184,13 @@ describe('loginByPassword API', () => {
});
expect(res.code).toBe(200);
expect(res.error).toBeUndefined();
// Verify user was updated with the language
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.language).toBe('en');
expect(updatedUser?.lastLoginTmbId).toEqual(testTmb._id);
});
it('should handle root user login correctly', async () => {
// Create root user
const rootUser = await MongoUser.create({
username: 'root',
password: 'rootpassword',
@@ -239,88 +217,125 @@ describe('loginByPassword API', () => {
lastLoginTmbId: rootTmb._id
});
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
body: {
username: 'root',
password: 'rootpassword',
code: '123456'
code: '123456',
language: 'zh-CN'
}
});
expect(res.code).toBe(200);
expect(res.error).toBeUndefined();
expect(res.data).toBeDefined();
expect(res.data.token).toBeDefined();
expect(typeof res.data.token).toBe('string');
});
it('should use default language when language is not provided', async () => {
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'testpassword',
code: '123456'
}
// ===== Security: NoSQL injection prevention (GHSA-jxvr-h2vx-p73r) =====
describe('NoSQL injection prevention', () => {
it('should reject password as object with MongoDB operator ($ne)', async () => {
// GHSA-jxvr-h2vx-p73r Step 2: password: {"$ne": ""} bypasses password check
const res = await Call<any, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: { $ne: '' },
code: '123456',
language: 'zh-CN'
}
});
// Zod z.string() must reject object-type password
expect(res.code).toBe(500);
expect(res.data?.token).toBeUndefined();
expect(res.data?.user).toBeUndefined();
});
expect(res.code).toBe(200);
expect(res.error).toBeUndefined();
it('should reject password with $regex operator', async () => {
const res = await Call<any, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: { $regex: '.*' },
code: '123456',
language: 'zh-CN'
}
});
// Verify user was updated with the default language 'zh-CN'
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.language).toBe('zh-CN');
});
it('should update lastLoginTmbId on successful login', async () => {
const updateOneSpy = vi.spyOn(MongoUser, 'updateOne');
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'testpassword',
code: '123456'
}
expect(res.code).toBe(500);
});
expect(res.code).toBe(200);
expect(res.error).toBeUndefined();
it('should reject password with $where injection', async () => {
const res = await Call<any, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: { $where: 'return true' },
code: '123456',
language: 'zh-CN'
}
});
// Verify user was updated with lastLoginTmbId
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.lastLoginTmbId).toEqual(testTmb._id);
});
it('should verify user authentication flow', async () => {
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'testpassword',
code: '123456'
}
expect(res.code).toBe(500);
});
expect(res.code).toBe(200);
it('should reject username as object with MongoDB operator', async () => {
const res = await Call<any, {}, any>(loginApi.default, {
body: {
username: { $ne: '' },
password: 'testpassword',
code: '123456',
language: 'zh-CN'
}
});
// Verify the full authentication flow
expect(authCode).toHaveBeenCalled();
expect(setCookie).toHaveBeenCalled();
expect(pushTrack.login).toHaveBeenCalled();
expect(addAuditLog).toHaveBeenCalled();
});
it('should return user details with correct structure', async () => {
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'testpassword',
code: '123456'
}
expect(res.code).toBe(500);
});
expect(res.code).toBe(200);
expect(res.data.user).toBeDefined();
expect(res.data.user.team).toBeDefined();
expect(res.data.user.team.teamId).toBe(String(testTeam._id));
expect(res.data.user.team.tmbId).toBe(String(testTmb._id));
it('should reject code as object with MongoDB operator', async () => {
const res = await Call<any, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 'testpassword',
code: { $regex: '.*' },
language: 'zh-CN'
}
});
expect(res.code).toBe(500);
});
it('should reject all fields as injection objects simultaneously', async () => {
const res = await Call<any, {}, any>(loginApi.default, {
body: {
username: { $ne: '' },
password: { $ne: '' },
code: { $ne: '' },
language: 'zh-CN'
}
});
expect(res.code).toBe(500);
});
it('should reject password as non-string types (array, number)', async () => {
const arrayRes = await Call<any, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: ['testpassword'],
code: '123456',
language: 'zh-CN'
}
});
expect(arrayRes.code).toBe(500);
const numberRes = await Call<any, {}, any>(loginApi.default, {
body: {
username: 'testuser',
password: 12345,
code: '123456',
language: 'zh-CN'
}
});
expect(numberRes.code).toBe(500);
});
});
});
@@ -0,0 +1,55 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as loginoutApi from '@/pages/api/support/user/account/loginout';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import { Call } from '@test/utils/request';
describe('loginout API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'testpassword',
status: UserStatusEnum.active
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
vi.clearAllMocks();
});
it('should logout successfully with valid auth', async () => {
const res = await Call(loginoutApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
});
it('should succeed even when unauthenticated (auth errors are caught)', async () => {
// loginout catches auth errors and always calls clearCookie
const res = await Call(loginoutApi.default, {});
expect(res.code).toBe(200);
});
});
@@ -0,0 +1,100 @@
import { describe, it, expect, beforeEach } from 'vitest';
import * as preLoginApi from '@/pages/api/support/user/account/preLogin';
import { MongoUserAuth } from '@fastgpt/service/support/user/auth/schema';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import { Call } from '@test/utils/request';
describe('preLogin API', () => {
beforeEach(async () => {
await MongoUserAuth.deleteMany({});
});
it('should return a 6-char verification code for valid username', async () => {
const res = await Call(preLoginApi.default, {
query: { username: 'testuser' }
});
expect(res.code).toBe(200);
expect(res.data).toBeDefined();
expect(typeof res.data.code).toBe('string');
expect(res.data.code.length).toBe(6);
});
it('should store the verification code in database', async () => {
const res = await Call(preLoginApi.default, {
query: { username: 'testuser' }
});
expect(res.code).toBe(200);
const record = await MongoUserAuth.findOne({
key: 'testuser',
type: UserAuthTypeEnum.login
});
expect(record).toBeDefined();
expect(record?.code).toBe(res.data.code);
});
it('should generate different codes for different usernames', async () => {
const res1 = await Call(preLoginApi.default, {
query: { username: 'user1' }
});
const res2 = await Call(preLoginApi.default, {
query: { username: 'user2' }
});
expect(res1.code).toBe(200);
expect(res2.code).toBe(200);
const record1 = await MongoUserAuth.findOne({ key: 'user1', type: UserAuthTypeEnum.login });
const record2 = await MongoUserAuth.findOne({ key: 'user2', type: UserAuthTypeEnum.login });
expect(record1?.key).toBe('user1');
expect(record2?.key).toBe('user2');
});
it('should overwrite previous code for the same username', async () => {
await Call(preLoginApi.default, { query: { username: 'testuser' } });
const res2 = await Call(preLoginApi.default, { query: { username: 'testuser' } });
const records = await MongoUserAuth.find({
key: 'testuser',
type: UserAuthTypeEnum.login
});
// upsert: only one record per key+type
expect(records.length).toBe(1);
expect(records[0].code).toBe(res2.data.code);
});
it('should set code expiredTime about 30 seconds from now', async () => {
const before = new Date();
const res = await Call(preLoginApi.default, {
query: { username: 'testuser' }
});
const after = new Date();
expect(res.code).toBe(200);
const record = await MongoUserAuth.findOne({ key: 'testuser', type: UserAuthTypeEnum.login });
expect(record?.expiredTime).toBeDefined();
const expiredTime = new Date(record!.expiredTime!).getTime();
// Should expire ~30 seconds from now (allow ±2s for test execution)
expect(expiredTime).toBeGreaterThanOrEqual(before.getTime() + 28000);
expect(expiredTime).toBeLessThanOrEqual(after.getTime() + 32000);
});
it('should reject when username is missing', async () => {
const res = await Call(preLoginApi.default, {
query: {}
});
expect(res.code).toBe(500);
});
it('should handle root username', async () => {
const res = await Call(preLoginApi.default, {
query: { username: 'root' }
});
expect(res.code).toBe(200);
expect(res.data.code).toBeDefined();
expect(res.data.code.length).toBe(6);
});
});
@@ -0,0 +1,185 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as resetExpiredPswApi from '@/pages/api/support/user/account/resetExpiredPsw';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import type { ResetExpiredPswBodyType } from '@fastgpt/global/openapi/support/user/account/password/api';
import { Call } from '@test/utils/request';
describe('resetExpiredPsw API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
const originalEnv = process.env.PASSWORD_EXPIRED_MONTH;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'oldpassword',
status: UserStatusEnum.active
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
vi.clearAllMocks();
});
afterEach(() => {
process.env.PASSWORD_EXPIRED_MONTH = originalEnv;
});
it('should successfully reset password when expired', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '1';
// Set password update time to 2 months ago (expired)
const twoMonthsAgo = new Date();
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: twoMonthsAgo
});
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: 'newhashedpassword' },
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
// Verify password was updated
const updatedUser = await MongoUser.findById(testUser._id).select(
'+password +passwordUpdateTime'
);
expect(updatedUser?.password).toBeDefined();
expect(updatedUser?.passwordUpdateTime).toBeDefined();
const newUpdateTime = new Date(updatedUser!.passwordUpdateTime!).getTime();
expect(newUpdateTime).toBeGreaterThan(twoMonthsAgo.getTime());
});
it('should reject when password is not expired (PASSWORD_EXPIRED_MONTH not set)', async () => {
delete process.env.PASSWORD_EXPIRED_MONTH;
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: new Date()
});
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: 'newhashedpassword' },
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
it('should reject when password is not expired (still within expiry period)', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '3';
// Update password just now — not expired
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: new Date()
});
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: 'newhashedpassword' },
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(500);
});
it('should reject when newPsw is missing', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '1';
const res = await Call<any, {}, any>(resetExpiredPswApi.default, {
body: {},
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(500);
});
it('should reject when user is not found', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '1';
const nonExistentId = '000000000000000000000001';
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: 'newhashedpassword' },
auth: {
userId: nonExistentId,
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(500);
expect(res.error).toBe('The password has not expired');
});
it('should reject request without authentication', async () => {
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: 'newhashedpassword' }
});
expect(res.code).toBe(500);
});
it('should reject newPsw as non-string (injection guard)', async () => {
process.env.PASSWORD_EXPIRED_MONTH = '1';
const twoMonthsAgo = new Date();
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
await MongoUser.findByIdAndUpdate(testUser._id, {
passwordUpdateTime: twoMonthsAgo
});
const res = await Call<any, {}, any>(resetExpiredPswApi.default, {
body: { newPsw: { $ne: '' } },
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(500);
});
});
@@ -0,0 +1,125 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as tokenLoginApi from '@/pages/api/support/user/account/tokenLogin';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import { Call } from '@test/utils/request';
describe('tokenLogin API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'testpassword',
status: UserStatusEnum.active
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
await MongoUser.findByIdAndUpdate(testUser._id, {
lastLoginTmbId: testTmb._id
});
vi.clearAllMocks();
});
it('should return user detail on valid token', async () => {
const res = await Call(tokenLoginApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
expect(res.data).toBeDefined();
expect(res.data.team).toBeDefined();
expect(res.data.team.teamId).toBe(String(testTeam._id));
expect(res.data.team.tmbId).toBe(String(testTmb._id));
});
it('should call pushTrack.dailyUserActive', async () => {
await Call(tokenLoginApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(pushTrack.dailyUserActive).toHaveBeenCalledWith({
uid: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id)
});
});
it('should mask openaiAccount key but keep baseUrl', async () => {
await MongoTeamMember.findByIdAndUpdate(testTmb._id, {
openaiAccount: { key: 'sk-secret-key', baseUrl: 'https://api.openai.com' }
});
const res = await Call(tokenLoginApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
if (res.data.team.openaiAccount) {
expect(res.data.team.openaiAccount.key).toBe('');
expect(res.data.team.openaiAccount.baseUrl).toBe('https://api.openai.com');
}
});
it('should mask all values in externalWorkflowVariables', async () => {
await MongoTeamMember.findByIdAndUpdate(testTmb._id, {
externalWorkflowVariables: { SECRET: 'top-secret', API_KEY: 'sk-123' }
});
const res = await Call(tokenLoginApi.default, {
auth: {
userId: String(testUser._id),
teamId: String(testTeam._id),
tmbId: String(testTmb._id),
isRoot: false,
sessionId: 'session123'
} as any
});
expect(res.code).toBe(200);
if (res.data.team.externalWorkflowVariables) {
Object.values(res.data.team.externalWorkflowVariables).forEach((val) => {
expect(val).toBe('');
});
}
});
it('should reject request without authentication', async () => {
const res = await Call(tokenLoginApi.default, {});
expect(res.code).toBe(500);
});
});
@@ -0,0 +1,122 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as updateApi from '@/pages/api/support/user/account/update';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import { Call } from '@test/utils/request';
describe('update (user account) API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'testpassword',
status: UserStatusEnum.active,
language: 'zh-CN',
timezone: 'Asia/Shanghai'
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
vi.clearAllMocks();
});
const makeAuth = (user: any, team: any, tmb: any) => ({
userId: String(user._id),
teamId: String(team._id),
tmbId: String(tmb._id),
isRoot: false,
sessionId: 'session123'
});
it('should update language successfully', async () => {
const res = await Call(updateApi.default, {
body: { language: 'en' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.language).toBe('en');
});
it('should update timezone successfully', async () => {
const res = await Call(updateApi.default, {
body: { timezone: 'America/New_York' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.timezone).toBe('America/New_York');
});
it('should update both language and timezone simultaneously', async () => {
const res = await Call(updateApi.default, {
body: { language: 'zh-Hant', timezone: 'Asia/Tokyo' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.language).toBe('zh-Hant');
expect(updatedUser?.timezone).toBe('Asia/Tokyo');
});
it('should update avatar on team member', async () => {
const newAvatar = '/avatar/test-avatar.png';
const res = await Call(updateApi.default, {
body: { avatar: newAvatar },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
const updatedTmb = await MongoTeamMember.findById(testTmb._id);
expect(updatedTmb?.avatar).toBe(newAvatar);
});
it('should return empty object on success', async () => {
const res = await Call(updateApi.default, {
body: { language: 'en' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
expect(res.data).toEqual({});
});
it('should handle empty body without error', async () => {
const res = await Call(updateApi.default, {
body: {},
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
// Nothing should change
const updatedUser = await MongoUser.findById(testUser._id);
expect(updatedUser?.language).toBe('zh-CN');
expect(updatedUser?.timezone).toBe('Asia/Shanghai');
});
it('should reject request without authentication', async () => {
const res = await Call(updateApi.default, {
body: { language: 'en' }
});
expect(res.code).toBe(500);
});
});
@@ -0,0 +1,139 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as updatePasswordApi from '@/pages/api/support/user/account/updatePasswordByOld';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
import type { UpdatePasswordByOldBodyType } from '@fastgpt/global/openapi/support/user/account/password/api';
import { Call } from '@test/utils/request';
describe('updatePasswordByOld API', () => {
let testUser: any;
let testTeam: any;
let testTmb: any;
beforeEach(async () => {
testUser = await MongoUser.create({
username: 'testuser',
password: 'oldhashpassword',
status: UserStatusEnum.active
});
testTeam = await MongoTeam.create({
name: 'Test Team',
ownerId: testUser._id
});
await initTeamFreePlan({ teamId: String(testTeam._id) });
testTmb = await MongoTeamMember.create({
teamId: testTeam._id,
userId: testUser._id,
status: 'active',
role: 'owner'
});
vi.clearAllMocks();
});
const makeAuth = (user: any, team: any, tmb: any) => ({
userId: String(user._id),
teamId: String(team._id),
tmbId: String(tmb._id),
isRoot: false,
sessionId: 'session123'
});
it('should update password successfully with correct old password', async () => {
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(200);
const updatedUser = await MongoUser.findById(testUser._id).select('+password');
expect(updatedUser?.password).toBeDefined();
expect(updatedUser?.passwordUpdateTime).toBeDefined();
});
it('should reject when old password is incorrect', async () => {
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'wrongpassword', newPsw: 'newhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
// Password should not change
const user = await MongoUser.findById(testUser._id).select('+passwordUpdateTime');
expect(user?.passwordUpdateTime).toBeUndefined(); // we didn't set it initially
});
it('should reject when old and new passwords are the same', async () => {
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'oldhashpassword', newPsw: 'oldhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
});
it('should reject when oldPsw is missing', async () => {
const res = await Call<any, {}, any>(updatePasswordApi.default, {
body: { newPsw: 'newhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
});
it('should reject when newPsw is missing', async () => {
const res = await Call<any, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'oldhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
});
it('should reject request without authentication', async () => {
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' }
});
expect(res.code).toBe(500);
});
// ===== Security: NoSQL injection prevention (GHSA-jxvr-h2vx-p73r Step 3) =====
it('should reject oldPsw as MongoDB operator object ($ne injection)', async () => {
// GHSA-jxvr-h2vx-p73r Step 3: oldPsw: {"$ne": ""} bypasses old password check
const res = await Call<any, {}, any>(updatePasswordApi.default, {
body: { oldPsw: { $ne: '' }, newPsw: 'newhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
// Zod z.string() must reject object-type oldPsw
expect(res.code).toBe(500);
// Password must NOT be changed
const user = await MongoUser.findById(testUser._id).select('+passwordUpdateTime');
expect(user?.passwordUpdateTime).toBeUndefined();
});
it('should reject oldPsw with $regex injection', async () => {
const res = await Call<any, {}, any>(updatePasswordApi.default, {
body: { oldPsw: { $regex: '.*' }, newPsw: 'newhashpassword' },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
});
it('should reject newPsw as non-string type', async () => {
const res = await Call<any, {}, any>(updatePasswordApi.default, {
body: { oldPsw: 'oldhashpassword', newPsw: { $ne: '' } },
auth: makeAuth(testUser, testTeam, testTmb) as any
});
expect(res.code).toBe(500);
});
});