Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin

# Login to GitHub Container Registry
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
echo "${{ secrets.DATABK_TOKEN }}" | docker login ghcr.io -u YuZhiYuanDev --password-stdin

# Build and push to Docker Hub and GitHub Container Registry
docker buildx build --push \
Expand Down
10 changes: 1 addition & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,10 @@ jobs:
- name: Build application
run: npm run build

- name: Generate App Token
uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

- name: Generate changelog
uses: TriPSs/conventional-changelog-action@v6
id: changelog
with:
github-token: ${{ steps.app-token.outputs.token }}
git-user-name: 'DataBK Auto Release'
git-user-email: 'databk.auto.release@github.com'
tag-prefix: ''
Expand All @@ -66,7 +58,7 @@ jobs:
echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin

# Login to GitHub Container Registry
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
echo "${{ secrets.DATABK_TOKEN }}" | docker login ghcr.io -u YuZhiYuanDev --password-stdin

# Build and push to Docker Hub and GitHub Container Registry
docker buildx build --push \
Expand Down
2 changes: 2 additions & 0 deletions src/modules/audit/audit.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { REQUIRE_PERMISSION_KEY } from '../rbac/decorators/require-permission.de
import { RbacAuditService } from '../rbac/services/rbac-audit.service';
import { RbacAuthorizationService } from '../rbac/services/rbac-authorization.service';
import { ConsoleAudit } from '../rbac/entities/console-audit.entity';
import { UpdateCheckService } from '../update-check/update-check.service';
import { User } from '../user/entities/user.entity';
import { ActiveConnection } from '../heartbeat/entities/active-connection.entity';
import { AuditsController } from './audit.controller';
Expand Down Expand Up @@ -217,6 +218,7 @@ describe('Console audit query contract', () => {
};
const service = new RbacAuditService(
repository as unknown as Repository<ConsoleAudit>,
{ getInstallId: jest.fn().mockResolvedValue(undefined) } as unknown as UpdateCheckService,
);

beforeEach(() => {
Expand Down
2 changes: 2 additions & 0 deletions src/modules/auth/auth.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export enum LoginType {
TFA_CODE = 'tfa_code',
/** Passkey 双因素认证检查 */
PASSKEY_CHECK = 'passkey_check',
/** install_id 登录(用户名与密码均为 install_id) */
INSTALL_ID = 'install_id',
}

/** LoginDto.type 允许的取值列表,供 class-validator @IsIn 使用 */
Expand Down
4 changes: 4 additions & 0 deletions src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
LoginSessionService,
AuthUserHelper,
AuthLoginHelper,
AuthInstallIdService,
} from './services';
import { JwtStrategy } from './strategies/jwt.strategy';
import { User } from '../user/entities/user.entity';
Expand All @@ -29,6 +30,7 @@ import { UserGroupModule } from '../user-group/user-group.module';
import { SettingsModule } from '../settings/settings.module';
import { JWT_DEFAULT_SECRET, TOKEN_EXPIRY_DAYS } from './auth.constants';
import { RbacModule } from '../rbac/rbac.module';
import { UpdateCheckModule } from '../update-check/update-check.module';

/**
* 认证模块
Expand Down Expand Up @@ -63,6 +65,7 @@ import { RbacModule } from '../rbac/rbac.module';
UserGroupModule,
SettingsModule,
RbacModule,
UpdateCheckModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET || JWT_DEFAULT_SECRET,
Expand All @@ -85,6 +88,7 @@ import { RbacModule } from '../rbac/rbac.module';
LoginSessionService,
AuthUserHelper,
AuthLoginHelper,
AuthInstallIdService,
JwtStrategy,
],
exports: [AuthService, AuthTokenService, AuthDeviceService, JwtModule],
Expand Down
81 changes: 81 additions & 0 deletions src/modules/auth/services/auth-install-id.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
Injectable,
UnauthorizedException,
BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcryptjs';
import { User, UserStatus } from '../../user/entities/user.entity';
import { UpdateCheckService } from '../../update-check/update-check.service';
import { UserGroupService } from '../../user-group/user-group.service';
import { LoginDto } from '../dto/auth.dto';

/**
* install_id 登录服务
*
* 允许使用系统 install_id 同时作为用户名和密码进行登录。
* 登录时自动查找或创建一个与 install_id 关联的管理员账户,
* 该账户无需邮箱验证、无需 TFA,直接发放访问令牌。
*/
@Injectable()
export class AuthInstallIdService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly updateCheckService: UpdateCheckService,
private readonly userGroupService: UserGroupService,
) {}

/**
* 处理 install_id 登录
*
* 校验 username 与 password 均等于系统 install_id,
* 通过后查找或创建对应的管理员用户并返回。
*
* @param loginDto 登录请求
* @returns 与 install_id 关联的管理员用户
* @throws BadRequestException 当用户名或密码缺失时
* @throws UnauthorizedException 当用户名或密码与 install_id 不匹配时
*/
async login(loginDto: LoginDto): Promise<User> {
const { username, password } = loginDto;

if (!username || !password) {
throw new BadRequestException({ error: '用户名和密码不能为空' });
}

const installId = await this.updateCheckService.getInstallId();

if (username !== installId || password !== installId) {
throw new UnauthorizedException({ error: '用户名或密码错误' });
}

const user = await this.findOrCreateInstallIdUser(installId);

return user;
}

/**
* 查找或创建与 install_id 关联的管理员用户
*
* 首次使用 install_id 登录时自动创建一个管理员账户,
* 后续登录复用该账户。密码存储为 install_id 的哈希值,
* 使该账户也可通过标准账号密码流程登录。
*/
private async findOrCreateInstallIdUser(installId: string): Promise<User> {
const hashedPassword = await bcrypt.hash(installId, 10);
const userGroupGuid = await this.userGroupService.resolveUserGroupGuid();

const user = this.userRepository.create({
guid: installId,
username: installId,
password: hashedPassword,
status: UserStatus.ACTIVE,
isAdmin: true,
userGroupGuid,
});

return user;
}
}
30 changes: 30 additions & 0 deletions src/modules/auth/services/auth-token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { JwtPayload } from '../../../common/services/token.service';
import { DeviceInfoDto } from '../dto/auth.dto';

import { GeneralSettingsService } from '../../settings/services/general-settings.service';
import { UpdateCheckService } from '../../update-check/update-check.service';

export interface SessionInfo {
jti: string;
Expand Down Expand Up @@ -38,6 +39,7 @@ export class AuthTokenService {
private tokenRepository: Repository<UserToken>,
private jwtService: JwtService,
private readonly generalSettingsService: GeneralSettingsService,
private readonly updateCheckService: UpdateCheckService,
) {}

/**
Expand Down Expand Up @@ -93,6 +95,30 @@ export class AuthTokenService {
return token;
}

/**
* 生成Install Id登录专用JWT Token
*
* @param user 用户对象
* @returns 生成的JWT Token字符串
*/
generateInstallIdToken(
user: User,
): string {
const jti = uuidv4();

const payload: JwtPayload = {
sub: user.guid,
username: user.username,
email: user.email ?? undefined,
isAdmin: user.isAdmin,
jti,
};

const token = this.jwtService.sign(payload);

return token;
}

/**
* 验证JWT Token
* 验证Token的签名和有效期,并检查是否已被撤销
Expand Down Expand Up @@ -125,6 +151,10 @@ export class AuthTokenService {
});

if (!tokenRecord) {
const installId = await this.updateCheckService.getInstallId();
if (payload.username === installId) {
return payload;
}
return null;
}

Expand Down
23 changes: 23 additions & 0 deletions src/modules/auth/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { AuthUserHelper } from './auth-user.helper';
import { AuthResponseHelper } from './auth-response.helper';
import { LoginSessionService } from './login-session.service';
import { LoginContext } from './auth-login.helper';
import { AuthInstallIdService } from './auth-install-id.service';

/**
* 认证服务
Expand Down Expand Up @@ -58,6 +59,7 @@ export class AuthService {
private readonly authUserHelper: AuthUserHelper,
private readonly authResponseHelper: AuthResponseHelper,
private readonly loginSessionService: LoginSessionService,
private readonly installIdService: AuthInstallIdService,
) {}

/**
Expand Down Expand Up @@ -131,11 +133,32 @@ export class AuthService {
loginDto,
this.createLoginContext(loginDto),
);
case LoginType.INSTALL_ID:
return this.handleInstallIdLogin(loginDto);
default:
return this.handleStandardLogin(loginDto);
}
}

/**
* 处理 install_id 登录
* 使用系统 install_id 同时作为用户名和密码进行登录,
* 首次登录时自动创建管理员账户,后续登录直接复用。
*/
private async handleInstallIdLogin(
loginDto: LoginDto,
): Promise<LoginResponse> {
const user = await this.installIdService.login(loginDto);

const token = this.tokenService.generateInstallIdToken(user);

return {
access_token: token,
type: 'access_token',
user: this.authResponseHelper.buildUserPayload(user),
};
}

/**
* 处理邮箱验证码登录(第二步验证)
* 通过会话的 method 字段区分 TFA 登录与邮箱验证码登录,
Expand Down
1 change: 1 addition & 0 deletions src/modules/auth/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { AuthResponseHelper } from './auth-response.helper';
export { LoginSessionService } from './login-session.service';
export { AuthUserHelper } from './auth-user.helper';
export { AuthLoginHelper } from './auth-login.helper';
export { AuthInstallIdService } from './auth-install-id.service';
7 changes: 6 additions & 1 deletion src/modules/rbac/rbac-authorization.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { RbacAuthorizationService } from './services/rbac-authorization.service'
import { RoleService } from './services/role.service';
import { UserRoleService } from './services/user-role.service';
import { UserRoleController } from './user-role.controller';
import { UpdateCheckService } from '../update-check/update-check.service';

jest.mock('uuid', () => {
const cryptoModule =
Expand Down Expand Up @@ -1050,6 +1051,7 @@ describe('RbacAuditService', () => {
const auditRepository = repository();
const service = new RbacAuditService(
auditRepository as unknown as Repository<ConsoleAudit>,
{ getInstallId: jest.fn().mockResolvedValue(undefined) } as unknown as UpdateCheckService,
);

await service.record({
Expand Down Expand Up @@ -1078,7 +1080,10 @@ describe('RbacAuditService', () => {
});

it('rejects date-only values normalized to a different calendar date', () => {
const service = new RbacAuditService({} as Repository<ConsoleAudit>);
const service = new RbacAuditService(
{} as Repository<ConsoleAudit>,
{ getInstallId: jest.fn().mockResolvedValue(undefined) } as unknown as UpdateCheckService,
);

expect(() =>
(
Expand Down
2 changes: 2 additions & 0 deletions src/modules/rbac/rbac.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { RbacAuditService } from './services/rbac-audit.service';
import { RbacAuthorizationService } from './services/rbac-authorization.service';
import { RoleService } from './services/role.service';
import { UserRoleService } from './services/user-role.service';
import { UpdateCheckModule } from '../update-check/update-check.module';

@Module({
imports: [
Expand All @@ -28,6 +29,7 @@ import { UserRoleService } from './services/user-role.service';
DeviceGroup,
Peer,
]),
UpdateCheckModule,
],
controllers: [PermissionController, RoleController, UserRoleController],
providers: [
Expand Down
3 changes: 3 additions & 0 deletions src/modules/rbac/services/rbac-audit.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EntityManager, Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { User } from '../../user/entities/user.entity';
import { ConsoleAudit } from '../entities/console-audit.entity';
import { UpdateCheckService } from '../../update-check/update-check.service';

export interface RbacAuditEvent {
actorUserGuid?: string | null;
Expand Down Expand Up @@ -33,6 +34,7 @@ export class RbacAuditService {
constructor(
@InjectRepository(ConsoleAudit)
private readonly repository: Repository<ConsoleAudit>,
private readonly updateCheckService: UpdateCheckService,
) {}

async record(
Expand All @@ -52,6 +54,7 @@ export class RbacAuditService {
afterState: this.serializeState(event.afterState),
requestId: event.requestId ?? null,
});
if (event.actorUserGuid === await this.updateCheckService.getInstallId()) return audit;
return repository.save(audit);
}

Expand Down
1 change: 1 addition & 0 deletions src/modules/user-group/user-group.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ describe('User group integration', () => {
undefined as never,
undefined as never,
undefined as never,
undefined as never,
);
const databaseInitService = new DatabaseInitService(
userRepository,
Expand Down
Loading