diff --git a/.env.example b/.env.example index b73bd827..eca30d77 100644 --- a/.env.example +++ b/.env.example @@ -57,19 +57,13 @@ DEBUG_DB=false # Log http requests LOG_HTTP=false -# ─── Branding (AcadeDoc selfhost) ──────────────────────────────────── -# Override the visible product name (UI, emails, PWA manifest). -# Leave unset to keep the upstream "Docmost" default for pure selfhosters. -BRAND_NAME=AcadeDoc -# Optional: absolute URL to a custom logo asset served by your CDN or nginx. -# Default: the built-in Docmost favicon (no override needed for development). -# BRAND_LOGO_URL=/branding/acadedoc-logo.svg -# Optional CSS hex colors. Drives the Mantine theme at runtime (no rebuild). -# Default values match the Acadenice brand palette. -BRAND_PRIMARY_COLOR=#2563eb -BRAND_ACCENT_COLOR=#7c3aed +# ─── Branding ────────────────────────────────────────────────────────── +# Le branding (nom, couleurs, logo) se gere desormais UNIQUEMENT via l'UI +# admin: /settings/branding (couleurs + nom workspace) et /settings/general +# (logo). Pas de variable d'env a definir ici. Le defaut hardcode est +# "AcadeDoc" + bleu #2563eb / violet #7c3aed. -# ─── SMTP Brevo (recommande pour AcadeDoc) ───────────────────── +# ─── SMTP Brevo (recommande pour AcadeDoc) ───────────────────────────── # Compte Brevo : https://app.brevo.com/settings/keys/smtp # Pas le mot de passe du compte — generer une "SMTP key" dans le dashboard. # Plan free Brevo : 300 emails/jour (suffisant pour usage interne). @@ -84,7 +78,6 @@ BRAND_ACCENT_COLOR=#7c3aed # # MAIL_FROM_ADDRESS=noreply@acadenice.fr # MAIL_FROM_NAME=AcadeDoc -# (MAIL_FROM_NAME defaults to BRAND_NAME if unset) # ─── OIDC (Authentik) — Bloc 4b ────────────────────────────────────── # Disabled by default. Set OIDC_ENABLED=true and fill the block below diff --git a/apps/client/src/lib/config.ts b/apps/client/src/lib/config.ts index ea2a8f84..4f170302 100644 --- a/apps/client/src/lib/config.ts +++ b/apps/client/src/lib/config.ts @@ -9,20 +9,22 @@ declare global { } } +// Branding hardcoded defaults (override at runtime via /settings/branding). +// .env is reserved for server-side config (SMTP, DB, OIDC) — not branding. export function getAppName(): string { - return getConfigValue("BRAND_NAME", "AcadeDoc"); + return "AcadeDoc"; } export function getBrandLogoUrl(): string | null { - return getConfigValue("BRAND_LOGO_URL") || null; + return null; } export function getBrandPrimaryColor(): string { - return getConfigValue("BRAND_PRIMARY_COLOR", "#2563eb"); + return "#2563eb"; } export function getBrandAccentColor(): string { - return getConfigValue("BRAND_ACCENT_COLOR", "#7c3aed"); + return "#7c3aed"; } export function getAppUrl(): string { diff --git a/apps/client/src/theme/__tests__/brand-theme.test.ts b/apps/client/src/theme/__tests__/brand-theme.test.ts index cce98f37..70a9eba5 100644 --- a/apps/client/src/theme/__tests__/brand-theme.test.ts +++ b/apps/client/src/theme/__tests__/brand-theme.test.ts @@ -1,10 +1,10 @@ /** - * Tests for brand-theme.ts — R4.4 + * Tests for brand-theme.ts — Patch 020 * - * Covers: tuple generation, env-var overrides, fallback on invalid input. + * Covers: tuple generation, runtime overrides (workspace UI), fallback on invalid input. */ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { describe, it, expect } from "vitest"; import { generateColorTuple, getBrandTheme } from "../brand-theme"; // Mantine color tuple has exactly 10 entries. @@ -46,45 +46,43 @@ describe("generateColorTuple", () => { }); describe("getBrandTheme", () => { - const originalEnv = { ...process.env }; - - afterEach(() => { - // Restore env after each test - Object.assign(process.env, originalEnv); - vi.unstubAllEnvs(); - }); - it("returns brand and accent color keys in theme.colors", () => { const theme = getBrandTheme(); expect(theme.colors).toHaveProperty("brand"); expect(theme.colors).toHaveProperty("accent"); }); - it("uses default primary color (#2563eb) when env is unset", () => { - delete process.env.BRAND_PRIMARY_COLOR; + it("uses default primary color (#2563eb) when no override is given", () => { const theme = getBrandTheme(); - // Shade[6] of the default primary should not be fully black or white const shade6 = (theme.colors!.brand as unknown as string[])[6]; expect(shade6).toBeTruthy(); expect(shade6).not.toBe("#ffffff"); expect(shade6).not.toBe("#000000"); }); - it("overrides primary color via BRAND_PRIMARY_COLOR env var", () => { - process.env.BRAND_PRIMARY_COLOR = "#ff0000"; - const theme = getBrandTheme(); + it("overrides primary color via runtime override", () => { + const theme = getBrandTheme({ primary: "#ff0000" }); // shade[6] of #ff0000 with 0% black mix = #ff0000 itself const shade6 = (theme.colors!.brand as unknown as string[])[6]; expect(shade6.toLowerCase()).toBe("#ff0000"); }); - it("falls back to default when BRAND_PRIMARY_COLOR is invalid", () => { - process.env.BRAND_PRIMARY_COLOR = "badvalue"; - const defaultTheme = getBrandTheme(); - delete process.env.BRAND_PRIMARY_COLOR; - const normalTheme = getBrandTheme(); - // Both should produce identical tuples (both use default) - expect(defaultTheme.colors!.brand).toEqual(normalTheme.colors!.brand); + it("falls back to default when override primary is invalid", () => { + const themeBad = getBrandTheme({ primary: "badvalue" }); + const themeDefault = getBrandTheme(); + expect(themeBad.colors!.brand).toEqual(themeDefault.colors!.brand); + }); + + it("falls back to default when override primary is null", () => { + const themeNull = getBrandTheme({ primary: null }); + const themeDefault = getBrandTheme(); + expect(themeNull.colors!.brand).toEqual(themeDefault.colors!.brand); + }); + + it("overrides accent color independently", () => { + const theme = getBrandTheme({ accent: "#00ff00" }); + const shade6 = (theme.colors!.accent as unknown as string[])[6]; + expect(shade6.toLowerCase()).toBe("#00ff00"); }); it("sets primaryColor to 'brand'", () => { diff --git a/apps/client/src/theme/brand-theme.ts b/apps/client/src/theme/brand-theme.ts index 3778013b..94fc71e8 100644 --- a/apps/client/src/theme/brand-theme.ts +++ b/apps/client/src/theme/brand-theme.ts @@ -1,9 +1,10 @@ /** - * brand-theme.ts — R4.4 + * brand-theme.ts — R4.4 / Patch 020 * - * Reads BRAND_PRIMARY_COLOR and BRAND_ACCENT_COLOR from vite define block - * (process.env.*) and produces a MantineThemeOverride with generated color - * tuples. Falls back to hardcoded defaults so the app is never misconfigured. + * Produces a MantineThemeOverride with hardcoded brand defaults. Per-workspace + * runtime overrides come from /settings/branding (workspace.primary_color + + * workspace.accent_color in the DB) and are merged at app startup before + * Mantine renders. * * We do not depend on @mantine/colors-generator (not installed). Instead we * generate a 10-shade tuple by progressively mixing the hex toward white (light @@ -105,22 +106,25 @@ export function generateColorTuple(hex: string): MantineColorsTuple { // --------------------------------------------------------------------------- /** - * Reads BRAND_PRIMARY_COLOR and BRAND_ACCENT_COLOR from the vite define block - * and returns a MantineThemeOverride that can be merged into the base theme. + * Returns a MantineThemeOverride with the hardcoded brand defaults. + * Optional `overrides` override the defaults at runtime (used by the + * per-workspace branding UI to apply DB-stored colors). * - * Called once at startup in main.tsx. + * Called at startup in main.tsx; the workspace branding hook calls it again + * with overrides once it has loaded the DB row. */ -export function getBrandTheme(): MantineThemeOverride { - const rawPrimary = - (typeof process !== "undefined" && process.env?.BRAND_PRIMARY_COLOR) || - DEFAULT_PRIMARY; - const rawAccent = - (typeof process !== "undefined" && process.env?.BRAND_ACCENT_COLOR) || - DEFAULT_ACCENT; - - // Validate: if the value is not a valid hex, fall back to the default. - const primary = hexToRgb(rawPrimary) ? rawPrimary : DEFAULT_PRIMARY; - const accent = hexToRgb(rawAccent) ? rawAccent : DEFAULT_ACCENT; +export function getBrandTheme(overrides?: { + primary?: string | null; + accent?: string | null; +}): MantineThemeOverride { + const primary = + overrides?.primary && hexToRgb(overrides.primary) + ? overrides.primary + : DEFAULT_PRIMARY; + const accent = + overrides?.accent && hexToRgb(overrides.accent) + ? overrides.accent + : DEFAULT_ACCENT; return { colors: { diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts index e19191c3..e6f9de48 100644 --- a/apps/client/vite.config.ts +++ b/apps/client/vite.config.ts @@ -16,10 +16,6 @@ export default defineConfig(({ mode }) => { BILLING_TRIAL_DAYS, POSTHOG_HOST, POSTHOG_KEY, - BRAND_NAME, - BRAND_LOGO_URL, - BRAND_PRIMARY_COLOR, - BRAND_ACCENT_COLOR, } = loadEnv(mode, envPath, ""); return { @@ -35,10 +31,6 @@ export default defineConfig(({ mode }) => { BILLING_TRIAL_DAYS, POSTHOG_HOST, POSTHOG_KEY, - BRAND_NAME: BRAND_NAME || "AcadeDoc", - BRAND_LOGO_URL: BRAND_LOGO_URL || "", - BRAND_PRIMARY_COLOR: BRAND_PRIMARY_COLOR || "#2563eb", - BRAND_ACCENT_COLOR: BRAND_ACCENT_COLOR || "#7c3aed", }, APP_VERSION: JSON.stringify(process.env.npm_package_version), }, diff --git a/apps/server/src/core/workspace/spec/workspace-branding.spec.ts b/apps/server/src/core/workspace/spec/workspace-branding.spec.ts index f37fa62d..33f5ffc4 100644 --- a/apps/server/src/core/workspace/spec/workspace-branding.spec.ts +++ b/apps/server/src/core/workspace/spec/workspace-branding.spec.ts @@ -2,7 +2,7 @@ * workspace-branding.spec.ts — R4.4 * * Unit tests for WorkspaceBrandingService + WorkspaceBrandingController. - * Uses vitest + vi.fn() stubs — no real DB or HTTP server needed. + * Uses vitest + jest.fn() stubs — no real DB or HTTP server needed. * * Covered: * 1. getBranding returns correct shape @@ -17,7 +17,6 @@ * 10. UpdateWorkspaceBrandingDto validates hex color correctly */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; import { NotFoundException, ForbiddenException } from '@nestjs/common'; import { WorkspaceBrandingService } from '../services/workspace-branding.service'; import { WorkspaceBrandingController } from '../controllers/workspace-branding.controller'; @@ -40,8 +39,8 @@ const fakeWorkspace = { function makeWorkspaceRepo(overrides?: Partial) { const ws = { ...fakeWorkspace, ...overrides }; return { - findById: vi.fn(async (id: string) => (id === ws.id ? ws : null)), - updateWorkspace: vi.fn(async (patch: any, id: string) => { + findById: jest.fn(async (id: string) => (id === ws.id ? ws : null)), + updateWorkspace: jest.fn(async (patch: any, id: string) => { Object.assign(ws, patch); }), }; @@ -49,8 +48,8 @@ function makeWorkspaceRepo(overrides?: Partial) { function makeAbility(canManage = true) { return { - createForUser: vi.fn(() => ({ - cannot: vi.fn(() => !canManage), + createForUser: jest.fn(() => ({ + cannot: jest.fn(() => !canManage), })), }; } @@ -123,7 +122,7 @@ describe('WorkspaceBrandingService', () => { describe('WorkspaceBrandingController', () => { let controller: WorkspaceBrandingController; - let brandingService: { getBranding: ReturnType; updateBranding: ReturnType }; + let brandingService: { getBranding: jest.Mock; updateBranding: jest.Mock }; let ability: ReturnType; const mockWorkspace = { id: 'ws-uuid' } as any; @@ -131,8 +130,8 @@ describe('WorkspaceBrandingController', () => { beforeEach(() => { brandingService = { - getBranding: vi.fn(async () => ({ logo: null, primaryColor: null, accentColor: null })), - updateBranding: vi.fn(async () => ({ logo: null, primaryColor: '#2563eb', accentColor: null })), + getBranding: jest.fn(async () => ({ logo: null, primaryColor: null, accentColor: null })), + updateBranding: jest.fn(async () => ({ logo: null, primaryColor: '#2563eb', accentColor: null })), }; ability = makeAbility(true); controller = new WorkspaceBrandingController( diff --git a/apps/server/src/integrations/environment/environment.service.spec.ts b/apps/server/src/integrations/environment/environment.service.spec.ts index cd2ad4bb..71583ac7 100644 --- a/apps/server/src/integrations/environment/environment.service.spec.ts +++ b/apps/server/src/integrations/environment/environment.service.spec.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; import { EnvironmentService } from './environment.service'; describe('EnvironmentService', () => { @@ -6,7 +7,13 @@ describe('EnvironmentService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [EnvironmentService], + providers: [ + EnvironmentService, + { + provide: ConfigService, + useValue: { get: (_key: string, def?: unknown) => def }, + }, + ], }).compile(); service = module.get(EnvironmentService); @@ -15,4 +22,8 @@ describe('EnvironmentService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); + + it('getMailFromName defaults to "AcadeDoc" when MAIL_FROM_NAME is unset', () => { + expect(service.getMailFromName()).toBe('AcadeDoc'); + }); }); diff --git a/apps/server/src/integrations/environment/environment.service.ts b/apps/server/src/integrations/environment/environment.service.ts index 62422a8c..a6e2b82d 100644 --- a/apps/server/src/integrations/environment/environment.service.ts +++ b/apps/server/src/integrations/environment/environment.service.ts @@ -131,11 +131,9 @@ export class EnvironmentService { } getMailFromName(): string { - // MAIL_FROM_NAME can be overridden explicitly; if not set, fall back to - // BRAND_NAME so that outgoing mail uses the configured brand by default. - const explicit = this.configService.get('MAIL_FROM_NAME'); - if (explicit) return explicit; - return this.configService.get('BRAND_NAME', 'AcadeDoc'); + // MAIL_FROM_NAME can be overridden explicitly in .env. Defaults to the + // hardcoded brand name (UI override is per-workspace, not per-mail). + return this.configService.get('MAIL_FROM_NAME', 'AcadeDoc'); } getMailBlockedRecipientDomains(): string[] {