Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02796e4e1f
|
||
|
|
951b38db67
|
@@ -24,7 +24,8 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noUnknownAtRules": "off"
|
"noUnknownAtRules": "off",
|
||||||
|
"noExplicitAny": "off"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"useImportType": "off"
|
"useImportType": "off"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@memegoat/backend",
|
"name": "@memegoat/backend",
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"lint": "biome check",
|
"lint": "biome check",
|
||||||
"lint:write": "biome check --write",
|
"lint:write": "biome check --write --unsafe",
|
||||||
"format": "biome format --write",
|
"format": "biome format --write",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { ConfigService } from "@nestjs/config";
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { AuthController } from "./auth.controller";
|
import { AuthController } from "./auth.controller";
|
||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from "./auth.service";
|
||||||
|
import { BootstrapService } from "./bootstrap.service";
|
||||||
|
|
||||||
jest.mock("iron-session", () => ({
|
jest.mock("iron-session", () => ({
|
||||||
getIronSession: jest.fn().mockResolvedValue({
|
getIronSession: jest.fn().mockResolvedValue({
|
||||||
@@ -44,6 +45,10 @@ describe("AuthController", () => {
|
|||||||
refresh: jest.fn(),
|
refresh: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockBootstrapService = {
|
||||||
|
consumeToken: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
const mockConfigService = {
|
const mockConfigService = {
|
||||||
get: jest
|
get: jest
|
||||||
.fn()
|
.fn()
|
||||||
@@ -55,6 +60,7 @@ describe("AuthController", () => {
|
|||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: AuthService, useValue: mockAuthService },
|
{ provide: AuthService, useValue: mockAuthService },
|
||||||
|
{ provide: BootstrapService, useValue: mockBootstrapService },
|
||||||
{ provide: ConfigService, useValue: mockConfigService },
|
{ provide: ConfigService, useValue: mockConfigService },
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
@@ -75,7 +81,6 @@ describe("AuthController", () => {
|
|||||||
password: "password",
|
password: "password",
|
||||||
username: "test",
|
username: "test",
|
||||||
};
|
};
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Necessary to avoid defining full DTO in test
|
|
||||||
await controller.register(dto as any);
|
await controller.register(dto as any);
|
||||||
expect(authService.register).toHaveBeenCalledWith(dto);
|
expect(authService.register).toHaveBeenCalledWith(dto);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { Body, Controller, Get, Headers, Post, Query, Req, Res } from "@nestjs/common";
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Headers,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Throttle } from "@nestjs/throttler";
|
import { Throttle } from "@nestjs/throttler";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { UnauthorizedException } from "@nestjs/common";
|
import { UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { UsersService } from "../users/users.service";
|
||||||
import { BootstrapService } from "./bootstrap.service";
|
import { BootstrapService } from "./bootstrap.service";
|
||||||
import { RbacService } from "./rbac.service";
|
import { RbacService } from "./rbac.service";
|
||||||
import { UsersService } from "../users/users.service";
|
|
||||||
|
|
||||||
describe("BootstrapService", () => {
|
describe("BootstrapService", () => {
|
||||||
let service: BootstrapService;
|
let service: BootstrapService;
|
||||||
let rbacService: RbacService;
|
let rbacService: RbacService;
|
||||||
let usersService: UsersService;
|
let _usersService: UsersService;
|
||||||
|
|
||||||
const mockRbacService = {
|
const mockRbacService = {
|
||||||
countAdmins: jest.fn(),
|
countAdmins: jest.fn(),
|
||||||
@@ -36,7 +36,7 @@ describe("BootstrapService", () => {
|
|||||||
|
|
||||||
service = module.get<BootstrapService>(BootstrapService);
|
service = module.get<BootstrapService>(BootstrapService);
|
||||||
rbacService = module.get<RbacService>(RbacService);
|
rbacService = module.get<RbacService>(RbacService);
|
||||||
usersService = module.get<UsersService>(UsersService);
|
_usersService = module.get<UsersService>(UsersService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be defined", () => {
|
it("should be defined", () => {
|
||||||
@@ -46,7 +46,10 @@ describe("BootstrapService", () => {
|
|||||||
describe("onApplicationBootstrap", () => {
|
describe("onApplicationBootstrap", () => {
|
||||||
it("should generate a token if no admin exists", async () => {
|
it("should generate a token if no admin exists", async () => {
|
||||||
mockRbacService.countAdmins.mockResolvedValue(0);
|
mockRbacService.countAdmins.mockResolvedValue(0);
|
||||||
const generateTokenSpy = jest.spyOn(service as any, "generateBootstrapToken");
|
const generateTokenSpy = jest.spyOn(
|
||||||
|
service as any,
|
||||||
|
"generateBootstrapToken",
|
||||||
|
);
|
||||||
|
|
||||||
await service.onApplicationBootstrap();
|
await service.onApplicationBootstrap();
|
||||||
|
|
||||||
@@ -56,7 +59,10 @@ describe("BootstrapService", () => {
|
|||||||
|
|
||||||
it("should not generate a token if admin exists", async () => {
|
it("should not generate a token if admin exists", async () => {
|
||||||
mockRbacService.countAdmins.mockResolvedValue(1);
|
mockRbacService.countAdmins.mockResolvedValue(1);
|
||||||
const generateTokenSpy = jest.spyOn(service as any, "generateBootstrapToken");
|
const generateTokenSpy = jest.spyOn(
|
||||||
|
service as any,
|
||||||
|
"generateBootstrapToken",
|
||||||
|
);
|
||||||
|
|
||||||
await service.onApplicationBootstrap();
|
await service.onApplicationBootstrap();
|
||||||
|
|
||||||
@@ -70,9 +76,9 @@ describe("BootstrapService", () => {
|
|||||||
mockRbacService.countAdmins.mockResolvedValue(0);
|
mockRbacService.countAdmins.mockResolvedValue(0);
|
||||||
await service.onApplicationBootstrap();
|
await service.onApplicationBootstrap();
|
||||||
|
|
||||||
await expect(
|
await expect(service.consumeToken("wrong-token", "user1")).rejects.toThrow(
|
||||||
service.consumeToken("wrong-token", "user1"),
|
UnauthorizedException,
|
||||||
).rejects.toThrow(UnauthorizedException);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw UnauthorizedException if user not found", async () => {
|
it("should throw UnauthorizedException if user not found", async () => {
|
||||||
@@ -97,7 +103,10 @@ describe("BootstrapService", () => {
|
|||||||
|
|
||||||
const result = await service.consumeToken(token, "user1");
|
const result = await service.consumeToken(token, "user1");
|
||||||
|
|
||||||
expect(rbacService.assignRoleToUser).toHaveBeenCalledWith("user-uuid", "admin");
|
expect(rbacService.assignRoleToUser).toHaveBeenCalledWith(
|
||||||
|
"user-uuid",
|
||||||
|
"admin",
|
||||||
|
);
|
||||||
expect((service as any).bootstrapToken).toBeNull();
|
expect((service as any).bootstrapToken).toBeNull();
|
||||||
expect(result.message).toContain("user1 is now an administrator");
|
expect(result.message).toContain("user1 is now an administrator");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as crypto from "node:crypto";
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
Logger,
|
Logger,
|
||||||
@@ -5,7 +6,6 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import * as crypto from "node:crypto";
|
|
||||||
import { UsersService } from "../users/users.service";
|
import { UsersService } from "../users/users.service";
|
||||||
import { RbacService } from "./rbac.service";
|
import { RbacService } from "./rbac.service";
|
||||||
|
|
||||||
@@ -34,9 +34,15 @@ export class BootstrapService implements OnApplicationBootstrap {
|
|||||||
const url = `${protocol}://${domain}/auth/bootstrap-admin`;
|
const url = `${protocol}://${domain}/auth/bootstrap-admin`;
|
||||||
|
|
||||||
this.logger.warn("SECURITY ALERT: No administrator found in database.");
|
this.logger.warn("SECURITY ALERT: No administrator found in database.");
|
||||||
this.logger.warn("To create the first administrator, use the following endpoint:");
|
this.logger.warn(
|
||||||
this.logger.warn(`Endpoint: GET ${url}?token=${this.bootstrapToken}&username=votre_nom_utilisateur`);
|
"To create the first administrator, use the following endpoint:",
|
||||||
this.logger.warn("Exemple: curl -X GET \"http://localhost/auth/bootstrap-admin?token=...&username=...\"");
|
);
|
||||||
|
this.logger.warn(
|
||||||
|
`Endpoint: GET ${url}?token=${this.bootstrapToken}&username=votre_nom_utilisateur`,
|
||||||
|
);
|
||||||
|
this.logger.warn(
|
||||||
|
'Exemple: curl -X GET "http://localhost/auth/bootstrap-admin?token=...&username=..."',
|
||||||
|
);
|
||||||
this.logger.warn("This token is one-time use only.");
|
this.logger.warn("This token is one-time use only.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +59,9 @@ export class BootstrapService implements OnApplicationBootstrap {
|
|||||||
await this.rbacService.assignRoleToUser(user.uuid, "admin");
|
await this.rbacService.assignRoleToUser(user.uuid, "admin");
|
||||||
this.bootstrapToken = null; // One-time use
|
this.bootstrapToken = null; // One-time use
|
||||||
|
|
||||||
this.logger.log(`User ${username} has been promoted to administrator via bootstrap token.`);
|
this.logger.log(
|
||||||
|
`User ${username} has been promoted to administrator via bootstrap token.`,
|
||||||
|
);
|
||||||
return { message: `User ${username} is now an administrator` };
|
return { message: `User ${username} is now an administrator` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ export class RbacService implements OnApplicationBootstrap {
|
|||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
this.logger.log("No roles found, seeding default roles...");
|
this.logger.log("No roles found, seeding default roles...");
|
||||||
const defaultRoles = [
|
const defaultRoles = [
|
||||||
{ name: "Administrator", slug: "admin", description: "Full system access" },
|
{
|
||||||
|
name: "Administrator",
|
||||||
|
slug: "admin",
|
||||||
|
description: "Full system access",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Moderator",
|
name: "Moderator",
|
||||||
slug: "moderator",
|
slug: "moderator",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ jest.mock("minio");
|
|||||||
describe("S3Service", () => {
|
describe("S3Service", () => {
|
||||||
let service: S3Service;
|
let service: S3Service;
|
||||||
let configService: ConfigService;
|
let configService: ConfigService;
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Fine for testing purposes
|
|
||||||
let minioClient: any;
|
let minioClient: any;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@memegoat/frontend",
|
"name": "@memegoat/frontend",
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "biome check",
|
"lint": "biome check",
|
||||||
|
"lint:write": "biome check --write",
|
||||||
"format": "biome format --write"
|
"format": "biome format --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@memegoat/source",
|
"name": "@memegoat/source",
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"version:get": "cmake -P version.cmake GET",
|
"version:get": "cmake -P version.cmake GET",
|
||||||
@@ -13,9 +13,13 @@
|
|||||||
"build:back": "pnpm run -F @memegoat/backend build",
|
"build:back": "pnpm run -F @memegoat/backend build",
|
||||||
"build:docs": "pnpm run -F @memegoat/documentation build",
|
"build:docs": "pnpm run -F @memegoat/documentation build",
|
||||||
"lint": "pnpm run lint:back && pnpm run lint:front && pnpm run lint:docs",
|
"lint": "pnpm run lint:back && pnpm run lint:front && pnpm run lint:docs",
|
||||||
|
"lint:fix": "pnpm run lint:back:fix && pnpm run lint:front:fix && pnpm run lint:docs:fix",
|
||||||
"lint:back": "pnpm run -F @memegoat/backend lint",
|
"lint:back": "pnpm run -F @memegoat/backend lint",
|
||||||
|
"lint:back:fix": "pnpm run -F @memegoat/backend lint:write",
|
||||||
"lint:front": "pnpm run -F @memegoat/frontend lint",
|
"lint:front": "pnpm run -F @memegoat/frontend lint",
|
||||||
|
"lint:front:fix": "pnpm run -F @memegoat/frontend lint:write",
|
||||||
"lint:docs": "pnpm run -F @memegoat/documentation lint",
|
"lint:docs": "pnpm run -F @memegoat/documentation lint",
|
||||||
|
"lint:docs:fix": "pnpm run -F @memegoat/documentation lint:write",
|
||||||
"test": "pnpm run test:back && pnpm run test:front",
|
"test": "pnpm run test:back && pnpm run test:front",
|
||||||
"test:back": "pnpm run -F @memegoat/backend test",
|
"test:back": "pnpm run -F @memegoat/backend test",
|
||||||
"test:front": "pnpm run -F @memegoat/frontend test",
|
"test:front": "pnpm run -F @memegoat/frontend test",
|
||||||
|
|||||||
Reference in New Issue
Block a user