From 9dd283ced6b142d2fa7df562ec93082c877b973e Mon Sep 17 00:00:00 2001 From: Corentin Date: Fri, 8 May 2026 14:52:49 +0200 Subject: [PATCH] =?UTF-8?q?refactor(acadedoc):=20rename=20API=20routes=20/?= =?UTF-8?q?api/acadenice=20->=20/api/v1=20=E2=80=94=20R5.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all @Controller('acadenice/...') decorators with 'v1/...' on 16 NestJS controllers. Update all client services, hooks, tests, extension-clipper, and doc comments to match. DB table names (acadenice_*) and folder structure untouched. Co-Authored-By: Claude Sonnet 4.6 --- ACADENICE_PATCHES.md | 22 ++++++------- .../api-keys/services/api-key.service.ts | 6 ++-- .../audit-log/services/audit-log.service.ts | 2 +- .../clipper/__tests__/clipper-client.test.ts | 8 ++--- .../clipper/services/clipper-client.ts | 2 +- .../__tests__/row-comments-client.test.ts | 24 +++++++------- .../comments/services/row-comments-client.ts | 12 +++---- .../database-view/hooks/use-permissions.ts | 2 +- .../graph/__tests__/graph-client.test.ts | 2 +- .../acadenice/graph/services/graph-client.ts | 2 +- .../__tests__/notifications-client.test.ts | 24 +++++++------- .../services/notifications-client.ts | 4 +-- .../services/oidc-status.service.ts | 2 +- .../rbac/hooks/use-acadenice-permissions.ts | 2 +- .../acadenice/rbac/services/rbac-service.ts | 24 +++++++------- .../acadenice/rbac/types/rbac.types.ts | 2 +- .../services/slash-commands-client.ts | 2 +- .../hooks/use-sync-block-realtime.ts | 4 +-- .../services/sync-blocks-client.ts | 2 +- .../__tests__/templates-client.test.ts | 32 +++++++++---------- .../services/templates-client.ts | 2 +- apps/extension-clipper/src/lib/api-client.ts | 2 +- apps/extension-clipper/src/popup/popup.ts | 2 +- .../tests/api-client.test.ts | 4 +-- .../acadenice/api-keys/api-keys.module.ts | 6 ++-- .../controllers/api-key.controller.ts | 8 ++--- .../acadenice/audit-log/audit-log.module.ts | 2 +- .../controllers/audit-log.controller.ts | 4 +-- .../acadenice/backlinks/backlinks.module.ts | 2 +- .../controllers/backlinks.controller.ts | 4 +-- .../clipper/controllers/clipper.controller.ts | 10 +++--- .../core/acadenice/clipper/dto/import.dto.ts | 4 +-- .../controllers/page-comments.controller.ts | 4 +-- .../controllers/row-comments.controller.ts | 14 ++++---- .../graph/controllers/graph.controller.ts | 4 +-- .../src/core/acadenice/graph/graph.module.ts | 2 +- .../notification-preferences.controller.ts | 2 +- .../controllers/notifications.controller.ts | 14 ++++---- .../notifications/notifications.module.ts | 14 ++++---- .../controllers/permissions.controller.ts | 2 +- .../rbac/controllers/roles.controller.ts | 2 +- .../rbac/controllers/user-roles.controller.ts | 2 +- .../controllers/oidc-status.controller.ts | 4 +-- .../acadenice/security/security.module.ts | 2 +- .../controllers/slash-commands.controller.ts | 2 +- .../controllers/sync-blocks.controller.ts | 12 +++---- .../controllers/templates.controller.ts | 16 +++++----- ...8T120000-create-acadenice-slash-command.ts | 2 +- 48 files changed, 164 insertions(+), 164 deletions(-) diff --git a/ACADENICE_PATCHES.md b/ACADENICE_PATCHES.md index c65f5e8c..b44ff3fa 100644 --- a/ACADENICE_PATCHES.md +++ b/ACADENICE_PATCHES.md @@ -30,8 +30,8 @@ The native Docmost system already provides the complete mention notification pip R3.7 adds: 1. `MentionDetectorService` — pure service that walks Tiptap JSON and extracts user mentions (no DB). Used by the emitter and independently testable. 2. `NotificationEmitterService` — listens to `acadenice.page.content.updated` (REST API save path, not collab) and queues `PAGE_MENTION_NOTIFICATION`. Bridges the gap between the collab-only native detection and pages saved via REST (templates instantiate, import, etc.). -3. `AcadeniceNotificationsController` — facade over native `NotificationService`, prefix `/api/acadenice/notifications`. -4. `NotificationPreferencesController` — GET/PUT `/api/acadenice/notification-preferences` (reads/writes native `users.settings.notifications`). +3. `AcadeniceNotificationsController` — facade over native `NotificationService`, prefix `/api/v1/notifications`. +4. `NotificationPreferencesController` — GET/PUT `/api/v1/notification-preferences` (reads/writes native `users.settings.notifications`). 5. Frontend `/notifications` page — full inbox using native `NotificationItem` component. 6. Frontend `/settings/notifications` preferences page — dedicated toggles via Acadenice API. @@ -47,8 +47,8 @@ R3.7 adds: | `apps/server/src/core/acadenice/notifications/services/mention-detector.service.ts` | Tiptap mention walker (pure) | | `apps/server/src/core/acadenice/notifications/services/notification-emitter.service.ts` | Event listener -> PAGE_MENTION_NOTIFICATION queue | | `apps/server/src/core/acadenice/notifications/services/notification-preferences.service.ts` | Read/write users.settings.notifications | -| `apps/server/src/core/acadenice/notifications/controllers/notifications.controller.ts` | REST facade /api/acadenice/notifications | -| `apps/server/src/core/acadenice/notifications/controllers/notification-preferences.controller.ts` | REST /api/acadenice/notification-preferences | +| `apps/server/src/core/acadenice/notifications/controllers/notifications.controller.ts` | REST facade /api/v1/notifications | +| `apps/server/src/core/acadenice/notifications/controllers/notification-preferences.controller.ts` | REST /api/v1/notification-preferences | | `apps/server/src/core/acadenice/notifications/spec/mention-detector.service.spec.ts` | 18 unit tests | | `apps/server/src/core/acadenice/notifications/spec/notifications.controller.spec.ts` | 10 unit tests | | `apps/server/src/core/acadenice/notifications/spec/notification-preferences.spec.ts` | 4 unit tests | @@ -77,13 +77,13 @@ R3.7 adds: ### Endpoints ``` -GET /api/acadenice/notifications paginated list -GET /api/acadenice/notifications/unread-count unread badge count (polled 30s) -POST /api/acadenice/notifications/read-all mark all read -POST /api/acadenice/notifications/mark-read bulk mark read -POST /api/acadenice/notifications/:id/read single mark read -GET /api/acadenice/notification-preferences get prefs -PUT /api/acadenice/notification-preferences update prefs +GET /api/v1/notifications paginated list +GET /api/v1/notifications/unread-count unread badge count (polled 30s) +POST /api/v1/notifications/read-all mark all read +POST /api/v1/notifications/mark-read bulk mark read +POST /api/v1/notifications/:id/read single mark read +GET /api/v1/notification-preferences get prefs +PUT /api/v1/notification-preferences update prefs ``` ### Tests diff --git a/apps/client/src/features/acadenice/api-keys/services/api-key.service.ts b/apps/client/src/features/acadenice/api-keys/services/api-key.service.ts index d437a53b..bed8a3e6 100644 --- a/apps/client/src/features/acadenice/api-keys/services/api-key.service.ts +++ b/apps/client/src/features/acadenice/api-keys/services/api-key.service.ts @@ -6,17 +6,17 @@ import { } from "../types/api-key.types"; export async function listAcadeniceApiKeys(): Promise { - const resp = await api.get("/acadenice/api-keys"); + const resp = await api.get("/v1/api-keys"); return resp.data; } export async function createAcadeniceApiKey( data: CreateAcadeniceApiKeyRequest, ): Promise { - const resp = await api.post("/acadenice/api-keys", data); + const resp = await api.post("/v1/api-keys", data); return resp.data; } export async function revokeAcadeniceApiKey(id: string): Promise { - await api.delete(`/acadenice/api-keys/${id}`); + await api.delete(`/v1/api-keys/${id}`); } diff --git a/apps/client/src/features/acadenice/audit-log/services/audit-log.service.ts b/apps/client/src/features/acadenice/audit-log/services/audit-log.service.ts index 3c8480a6..d88fa4e4 100644 --- a/apps/client/src/features/acadenice/audit-log/services/audit-log.service.ts +++ b/apps/client/src/features/acadenice/audit-log/services/audit-log.service.ts @@ -7,6 +7,6 @@ import { export async function getAcadeniceAuditLogs( params: AcadeniceAuditLogQuery = {}, ): Promise { - const resp = await api.get("/acadenice/audit-log", { params }); + const resp = await api.get("/v1/audit-log", { params }); return resp.data; } diff --git a/apps/client/src/features/acadenice/clipper/__tests__/clipper-client.test.ts b/apps/client/src/features/acadenice/clipper/__tests__/clipper-client.test.ts index 2a04839e..33a25183 100644 --- a/apps/client/src/features/acadenice/clipper/__tests__/clipper-client.test.ts +++ b/apps/client/src/features/acadenice/clipper/__tests__/clipper-client.test.ts @@ -18,12 +18,12 @@ describe('clipperClient', () => { afterEach(() => jest.resetAllMocks()); describe('listTokens', () => { - it('GETs /api/acadenice/clipper/tokens', async () => { + it('GETs /api/v1/clipper/tokens', async () => { mockedAxios.get = jest.fn().mockResolvedValue({ data: [sampleToken] }); const result = await clipperClient.listTokens(); expect(result).toHaveLength(1); expect(result[0].id).toBe('tk-1'); - expect(mockedAxios.get).toHaveBeenCalledWith('/api/acadenice/clipper/tokens'); + expect(mockedAxios.get).toHaveBeenCalledWith('/api/v1/clipper/tokens'); }); }); @@ -37,7 +37,7 @@ describe('clipperClient', () => { expect(result.token).toBe('clip_abc123'); expect(result.tokenInfo.label).toBe('My token'); expect(mockedAxios.post).toHaveBeenCalledWith( - '/api/acadenice/clipper/tokens', + '/api/v1/clipper/tokens', { label: 'My token', duration_days: 30 }, ); }); @@ -47,7 +47,7 @@ describe('clipperClient', () => { it('DELETEs the token by id', async () => { mockedAxios.delete = jest.fn().mockResolvedValue({}); await clipperClient.revokeToken('tk-1'); - expect(mockedAxios.delete).toHaveBeenCalledWith('/api/acadenice/clipper/tokens/tk-1'); + expect(mockedAxios.delete).toHaveBeenCalledWith('/api/v1/clipper/tokens/tk-1'); }); }); }); diff --git a/apps/client/src/features/acadenice/clipper/services/clipper-client.ts b/apps/client/src/features/acadenice/clipper/services/clipper-client.ts index b1269884..f186f45f 100644 --- a/apps/client/src/features/acadenice/clipper/services/clipper-client.ts +++ b/apps/client/src/features/acadenice/clipper/services/clipper-client.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -const BASE = '/api/acadenice/clipper'; +const BASE = '/api/v1/clipper'; export interface ClipperTokenInfo { id: string; diff --git a/apps/client/src/features/acadenice/comments/__tests__/row-comments-client.test.ts b/apps/client/src/features/acadenice/comments/__tests__/row-comments-client.test.ts index a63e559b..a28b414e 100644 --- a/apps/client/src/features/acadenice/comments/__tests__/row-comments-client.test.ts +++ b/apps/client/src/features/acadenice/comments/__tests__/row-comments-client.test.ts @@ -51,14 +51,14 @@ describe("row-comments-client", () => { vi.clearAllMocks(); }); - it("listRowComments posts to /acadenice/row-comments/list", async () => { + it("listRowComments posts to /v1/row-comments/list", async () => { const comment = makeComment(); mockApi.post.mockResolvedValueOnce({ data: [comment] }); const result = await listRowComments({ tableId: TABLE_ID, rowId: ROW_ID }); expect(mockApi.post).toHaveBeenCalledWith( - "/acadenice/row-comments/list", + "/v1/row-comments/list", { tableId: TABLE_ID, rowId: ROW_ID }, ); expect(result).toHaveLength(1); @@ -69,12 +69,12 @@ describe("row-comments-client", () => { mockApi.post.mockResolvedValueOnce({ data: [] }); await listRowComments({ tableId: TABLE_ID, rowId: ROW_ID, resolved: true }); expect(mockApi.post).toHaveBeenCalledWith( - "/acadenice/row-comments/list", + "/v1/row-comments/list", { tableId: TABLE_ID, rowId: ROW_ID, resolved: true }, ); }); - it("createRowComment posts to /acadenice/row-comments/create", async () => { + it("createRowComment posts to /v1/row-comments/create", async () => { const comment = makeComment(); mockApi.post.mockResolvedValueOnce({ data: comment }); @@ -86,42 +86,42 @@ describe("row-comments-client", () => { const result = await createRowComment(params); expect(mockApi.post).toHaveBeenCalledWith( - "/acadenice/row-comments/create", + "/v1/row-comments/create", params, ); expect(result.id).toBe(COMMENT_ID); }); - it("updateRowComment posts to /acadenice/row-comments/update", async () => { + it("updateRowComment posts to /v1/row-comments/update", async () => { const comment = makeComment(); mockApi.post.mockResolvedValueOnce({ data: comment }); await updateRowComment(COMMENT_ID, JSON.stringify({ type: "doc" })); expect(mockApi.post).toHaveBeenCalledWith( - "/acadenice/row-comments/update", + "/v1/row-comments/update", { commentId: COMMENT_ID, content: JSON.stringify({ type: "doc" }) }, ); }); - it("resolveRowComment posts to /acadenice/row-comments/resolve", async () => { + it("resolveRowComment posts to /v1/row-comments/resolve", async () => { const comment = { ...makeComment(), isResolved: true }; mockApi.post.mockResolvedValueOnce({ data: comment }); const result = await resolveRowComment(COMMENT_ID, true); expect(mockApi.post).toHaveBeenCalledWith( - "/acadenice/row-comments/resolve", + "/v1/row-comments/resolve", { commentId: COMMENT_ID, resolved: true }, ); expect(result.isResolved).toBe(true); }); - it("deleteRowComment posts to /acadenice/row-comments/delete", async () => { + it("deleteRowComment posts to /v1/row-comments/delete", async () => { mockApi.post.mockResolvedValueOnce({ data: undefined }); await deleteRowComment(COMMENT_ID); expect(mockApi.post).toHaveBeenCalledWith( - "/acadenice/row-comments/delete", + "/v1/row-comments/delete", { commentId: COMMENT_ID }, ); }); @@ -131,7 +131,7 @@ describe("row-comments-client", () => { const count = await countRowComments(TABLE_ID, ROW_ID); expect(count).toBe(7); expect(mockApi.post).toHaveBeenCalledWith( - "/acadenice/row-comments/count", + "/v1/row-comments/count", { tableId: TABLE_ID, rowId: ROW_ID }, ); }); diff --git a/apps/client/src/features/acadenice/comments/services/row-comments-client.ts b/apps/client/src/features/acadenice/comments/services/row-comments-client.ts index 617277e6..6af7c341 100644 --- a/apps/client/src/features/acadenice/comments/services/row-comments-client.ts +++ b/apps/client/src/features/acadenice/comments/services/row-comments-client.ts @@ -34,7 +34,7 @@ export async function listRowComments( params: ListRowCommentsParams, ): Promise { const res = await api.post( - "/acadenice/row-comments/list", + "/v1/row-comments/list", params, ); return res.data; @@ -44,7 +44,7 @@ export async function createRowComment( params: CreateRowCommentParams, ): Promise { const res = await api.post( - "/acadenice/row-comments/create", + "/v1/row-comments/create", params, ); return res.data; @@ -54,7 +54,7 @@ export async function updateRowComment( commentId: string, content: string, ): Promise { - const res = await api.post("/acadenice/row-comments/update", { + const res = await api.post("/v1/row-comments/update", { commentId, content, }); @@ -65,7 +65,7 @@ export async function resolveRowComment( commentId: string, resolved: boolean, ): Promise { - const res = await api.post("/acadenice/row-comments/resolve", { + const res = await api.post("/v1/row-comments/resolve", { commentId, resolved, }); @@ -73,7 +73,7 @@ export async function resolveRowComment( } export async function deleteRowComment(commentId: string): Promise { - await api.post("/acadenice/row-comments/delete", { commentId }); + await api.post("/v1/row-comments/delete", { commentId }); } export async function countRowComments( @@ -81,7 +81,7 @@ export async function countRowComments( rowId: string, ): Promise { const res = await api.post<{ count: number }>( - "/acadenice/row-comments/count", + "/v1/row-comments/count", { tableId, rowId }, ); return res.data.count; diff --git a/apps/client/src/features/acadenice/database-view/hooks/use-permissions.ts b/apps/client/src/features/acadenice/database-view/hooks/use-permissions.ts index bd7089bf..9fe6df4b 100644 --- a/apps/client/src/features/acadenice/database-view/hooks/use-permissions.ts +++ b/apps/client/src/features/acadenice/database-view/hooks/use-permissions.ts @@ -4,7 +4,7 @@ import { useMemo } from "react"; * Reads the user's Acadenice permissions from the auth context. * * Why not a server call here: - * The permissions are already resolved by R2.3a (`GET /api/acadenice/permissions/me`) + * The permissions are already resolved by R2.3a (`GET /api/v1/permissions/me`) * and cached in the application-level React Query cache. This hook reads from * that cache or falls back to parsing the `acadenice_permissions` claim from any * JS-readable cookie. It does NOT perform a new HTTP request — callers that need diff --git a/apps/client/src/features/acadenice/graph/__tests__/graph-client.test.ts b/apps/client/src/features/acadenice/graph/__tests__/graph-client.test.ts index 75c43bfe..5392125e 100644 --- a/apps/client/src/features/acadenice/graph/__tests__/graph-client.test.ts +++ b/apps/client/src/features/acadenice/graph/__tests__/graph-client.test.ts @@ -54,7 +54,7 @@ beforeEach(() => { describe("fetchGraph", () => { it("calls /acadenice/graph with no params when all optional", async () => { await fetchGraph({}); - expect(mockGet).toHaveBeenCalledWith("/acadenice/graph"); + expect(mockGet).toHaveBeenCalledWith("/v1/graph"); }); it("appends depth query param", async () => { diff --git a/apps/client/src/features/acadenice/graph/services/graph-client.ts b/apps/client/src/features/acadenice/graph/services/graph-client.ts index 000c8bde..06bbec0f 100644 --- a/apps/client/src/features/acadenice/graph/services/graph-client.ts +++ b/apps/client/src/features/acadenice/graph/services/graph-client.ts @@ -67,7 +67,7 @@ export async function fetchGraph( query.includeOrphans = String(params.includeOrphans); const qs = new URLSearchParams(query).toString(); - const url = qs ? `/acadenice/graph?${qs}` : "/acadenice/graph"; + const url = qs ? `/v1/graph?${qs}` : "/v1/graph"; return api.get(url) as unknown as Promise; } diff --git a/apps/client/src/features/acadenice/notifications/__tests__/notifications-client.test.ts b/apps/client/src/features/acadenice/notifications/__tests__/notifications-client.test.ts index d702c5f3..d867fdfa 100644 --- a/apps/client/src/features/acadenice/notifications/__tests__/notifications-client.test.ts +++ b/apps/client/src/features/acadenice/notifications/__tests__/notifications-client.test.ts @@ -29,11 +29,11 @@ describe("notificationsClient", () => { vi.clearAllMocks(); }); - it("list: calls GET /api/acadenice/notifications", async () => { + it("list: calls GET /api/v1/notifications", async () => { mockApi.get.mockResolvedValue({ data: { items: [], meta: {} } }); const result = await notificationsClient.list(); expect(mockApi.get).toHaveBeenCalledWith( - "/api/acadenice/notifications", + "/api/v1/notifications", expect.objectContaining({ params: {} }), ); expect(result).toEqual({ items: [], meta: {} }); @@ -43,16 +43,16 @@ describe("notificationsClient", () => { mockApi.get.mockResolvedValue({ data: { items: [] } }); await notificationsClient.list({ tab: "direct" }); expect(mockApi.get).toHaveBeenCalledWith( - "/api/acadenice/notifications", + "/api/v1/notifications", expect.objectContaining({ params: { tab: "direct" } }), ); }); - it("unreadCount: calls GET /api/acadenice/notifications/unread-count", async () => { + it("unreadCount: calls GET /api/v1/notifications/unread-count", async () => { mockApi.get.mockResolvedValue({ data: { count: 3 } }); const result = await notificationsClient.unreadCount(); expect(mockApi.get).toHaveBeenCalledWith( - "/api/acadenice/notifications/unread-count", + "/api/v1/notifications/unread-count", ); expect(result).toEqual({ count: 3 }); }); @@ -61,7 +61,7 @@ describe("notificationsClient", () => { mockApi.post.mockResolvedValue({}); await notificationsClient.markRead(["id-1", "id-2"]); expect(mockApi.post).toHaveBeenCalledWith( - "/api/acadenice/notifications/mark-read", + "/api/v1/notifications/mark-read", { notificationIds: ["id-1", "id-2"] }, ); }); @@ -70,7 +70,7 @@ describe("notificationsClient", () => { mockApi.post.mockResolvedValue({}); await notificationsClient.markAllRead(); expect(mockApi.post).toHaveBeenCalledWith( - "/api/acadenice/notifications/read-all", + "/api/v1/notifications/read-all", ); }); @@ -78,11 +78,11 @@ describe("notificationsClient", () => { mockApi.post.mockResolvedValue({}); await notificationsClient.markOne("notif-uuid"); expect(mockApi.post).toHaveBeenCalledWith( - "/api/acadenice/notifications/notif-uuid/read", + "/api/v1/notifications/notif-uuid/read", ); }); - it("getPreferences: calls GET /api/acadenice/notification-preferences", async () => { + it("getPreferences: calls GET /api/v1/notification-preferences", async () => { const prefs = { emailMentions: true, emailReplies: false, @@ -94,12 +94,12 @@ describe("notificationsClient", () => { mockApi.get.mockResolvedValue({ data: prefs }); const result = await notificationsClient.getPreferences(); expect(mockApi.get).toHaveBeenCalledWith( - "/api/acadenice/notification-preferences", + "/api/v1/notification-preferences", ); expect(result).toEqual(prefs); }); - it("updatePreferences: calls PUT /api/acadenice/notification-preferences", async () => { + it("updatePreferences: calls PUT /api/v1/notification-preferences", async () => { const payload = { emailMentions: false }; const updatedPrefs = { emailMentions: false, @@ -112,7 +112,7 @@ describe("notificationsClient", () => { mockApi.put.mockResolvedValue({ data: updatedPrefs }); const result = await notificationsClient.updatePreferences(payload); expect(mockApi.put).toHaveBeenCalledWith( - "/api/acadenice/notification-preferences", + "/api/v1/notification-preferences", payload, ); expect(result).toEqual(updatedPrefs); diff --git a/apps/client/src/features/acadenice/notifications/services/notifications-client.ts b/apps/client/src/features/acadenice/notifications/services/notifications-client.ts index 8b6c4574..6cb9f992 100644 --- a/apps/client/src/features/acadenice/notifications/services/notifications-client.ts +++ b/apps/client/src/features/acadenice/notifications/services/notifications-client.ts @@ -13,8 +13,8 @@ export interface NotificationPreferences { export type UpdatePreferencesPayload = Partial; -const BASE = "/api/acadenice/notifications"; -const PREFS_BASE = "/api/acadenice/notification-preferences"; +const BASE = "/api/v1/notifications"; +const PREFS_BASE = "/api/v1/notification-preferences"; /** * HTTP client for Acadenice notification endpoints (R3.7). diff --git a/apps/client/src/features/acadenice/oidc-status/services/oidc-status.service.ts b/apps/client/src/features/acadenice/oidc-status/services/oidc-status.service.ts index c3141d75..4dec1459 100644 --- a/apps/client/src/features/acadenice/oidc-status/services/oidc-status.service.ts +++ b/apps/client/src/features/acadenice/oidc-status/services/oidc-status.service.ts @@ -2,6 +2,6 @@ import api from "@/lib/api-client"; import { OidcStatusResponse } from "../types/oidc-status.types"; export async function getOidcStatus(): Promise { - const resp = await api.get("/acadenice/security/oidc-status"); + const resp = await api.get("/v1/security/oidc-status"); return resp.data; } diff --git a/apps/client/src/features/acadenice/rbac/hooks/use-acadenice-permissions.ts b/apps/client/src/features/acadenice/rbac/hooks/use-acadenice-permissions.ts index 6da2a724..aa1f2d09 100644 --- a/apps/client/src/features/acadenice/rbac/hooks/use-acadenice-permissions.ts +++ b/apps/client/src/features/acadenice/rbac/hooks/use-acadenice-permissions.ts @@ -12,7 +12,7 @@ export const MY_PERMISSIONS_QUERY_KEY = [ /** * Source-of-truth hook for the current user's Acadenice permissions. * - * Backed by `GET /api/acadenice/permissions/me` (R2.3a) — the backend resolves + * Backed by `GET /api/v1/permissions/me` (R2.3a) — the backend resolves * the effective union from the user's roles, with a Redis 60s cache server-side * (R2.1). We keep a 60s React Query staleTime to mirror that TTL: refetching * sooner only hits the same Redis value. diff --git a/apps/client/src/features/acadenice/rbac/services/rbac-service.ts b/apps/client/src/features/acadenice/rbac/services/rbac-service.ts index 68a0c23c..e542a628 100644 --- a/apps/client/src/features/acadenice/rbac/services/rbac-service.ts +++ b/apps/client/src/features/acadenice/rbac/services/rbac-service.ts @@ -11,14 +11,14 @@ import { /** * REST client for the Acadenice RBAC API (R2.1 backend). - * Endpoints under /api/acadenice — relative to api.baseURL ("/api"). + * Endpoints under /api/v1 — relative to api.baseURL ("/api"). * * Note : Docmost's axios interceptor returns `response.data` directly, so the * return value of `api.get(...)` is already the body payload. */ export async function getPermissionsCatalog(): Promise { - return api.get("/acadenice/permissions") as unknown as Promise< + return api.get("/v1/permissions") as unknown as Promise< IPermissionDescriptor[] >; } @@ -29,17 +29,17 @@ export async function getPermissionsCatalog(): Promise */ export async function getMyPermissions(): Promise { return api.get( - "/acadenice/permissions/me", + "/v1/permissions/me", ) as unknown as Promise; } export async function listRoles(): Promise { - return api.get("/acadenice/roles") as unknown as Promise; + return api.get("/v1/roles") as unknown as Promise; } export async function getRole(roleId: string): Promise { return api.get( - `/acadenice/roles/${roleId}`, + `/v1/roles/${roleId}`, ) as unknown as Promise; } @@ -47,7 +47,7 @@ export async function createRole( payload: ICreateRolePayload, ): Promise { return api.post( - "/acadenice/roles", + "/v1/roles", payload, ) as unknown as Promise; } @@ -57,20 +57,20 @@ export async function updateRole( payload: IUpdateRolePayload, ): Promise { return api.patch( - `/acadenice/roles/${roleId}`, + `/v1/roles/${roleId}`, payload, ) as unknown as Promise; } export async function deleteRole(roleId: string): Promise { - await api.delete(`/acadenice/roles/${roleId}`); + await api.delete(`/v1/roles/${roleId}`); } export async function setRolePermissions( roleId: string, permissions: string[], ): Promise { - return api.put(`/acadenice/roles/${roleId}/permissions`, { + return api.put(`/v1/roles/${roleId}/permissions`, { permissions, }) as unknown as Promise; } @@ -78,7 +78,7 @@ export async function setRolePermissions( export async function listUserRoles( userId: string, ): Promise { - return api.get(`/acadenice/users/${userId}/roles`) as unknown as Promise< + return api.get(`/v1/users/${userId}/roles`) as unknown as Promise< IUserRoleAssignment[] >; } @@ -87,7 +87,7 @@ export async function assignRolesToUser( userId: string, roleIds: string[], ): Promise<{ ok: true }> { - return api.post(`/acadenice/users/${userId}/roles`, { + return api.post(`/v1/users/${userId}/roles`, { roleIds, }) as unknown as Promise<{ ok: true }>; } @@ -96,5 +96,5 @@ export async function unassignRoleFromUser( userId: string, roleId: string, ): Promise { - await api.delete(`/acadenice/users/${userId}/roles/${roleId}`); + await api.delete(`/v1/users/${userId}/roles/${roleId}`); } diff --git a/apps/client/src/features/acadenice/rbac/types/rbac.types.ts b/apps/client/src/features/acadenice/rbac/types/rbac.types.ts index 800f8c92..d0841b01 100644 --- a/apps/client/src/features/acadenice/rbac/types/rbac.types.ts +++ b/apps/client/src/features/acadenice/rbac/types/rbac.types.ts @@ -59,7 +59,7 @@ export interface IUpdateRolePayload { } /** - * Response of `GET /api/acadenice/permissions/me` (R2.3a). + * Response of `GET /api/v1/permissions/me` (R2.3a). * `is_admin_wildcard` is a cheap boolean so the UI can branch without * scanning the array. */ diff --git a/apps/client/src/features/acadenice/slash-commands-admin/services/slash-commands-client.ts b/apps/client/src/features/acadenice/slash-commands-admin/services/slash-commands-client.ts index 1ed99eff..27c8a7cb 100644 --- a/apps/client/src/features/acadenice/slash-commands-admin/services/slash-commands-client.ts +++ b/apps/client/src/features/acadenice/slash-commands-admin/services/slash-commands-client.ts @@ -35,7 +35,7 @@ export interface CreateSlashCommandPayload { export type UpdateSlashCommandPayload = Partial; -const BASE = '/api/acadenice/slash-commands'; +const BASE = '/api/v1/slash-commands'; export const slashCommandsClient = { list(): Promise { diff --git a/apps/client/src/features/acadenice/sync-blocks/hooks/use-sync-block-realtime.ts b/apps/client/src/features/acadenice/sync-blocks/hooks/use-sync-block-realtime.ts index 1e6d36eb..203d8490 100644 --- a/apps/client/src/features/acadenice/sync-blocks/hooks/use-sync-block-realtime.ts +++ b/apps/client/src/features/acadenice/sync-blocks/hooks/use-sync-block-realtime.ts @@ -7,7 +7,7 @@ export const SYNC_BLOCK_QUERY_KEY = "sync-block"; * SSE hook for sync block realtime updates (R4.2). * * Listens to the NestJS EventEmitter2 via an SSE endpoint: - * GET /api/acadenice/sync-blocks/{masterId}/events + * GET /api/v1/sync-blocks/{masterId}/events * * When the server broadcasts a `sync-block.updated` event for this masterId, * we invalidate the React Query cache entry so the NodeView re-fetches the @@ -36,7 +36,7 @@ export function useSyncBlockRealtime(masterId: string | undefined): void { useEffect(() => { if (!masterId) return; - const sseUrl = `/api/acadenice/sync-blocks/${encodeURIComponent(masterId)}/events`; + const sseUrl = `/api/v1/sync-blocks/${encodeURIComponent(masterId)}/events`; let retryTimeout: ReturnType | null = null; diff --git a/apps/client/src/features/acadenice/sync-blocks/services/sync-blocks-client.ts b/apps/client/src/features/acadenice/sync-blocks/services/sync-blocks-client.ts index 023d107b..db0a37b7 100644 --- a/apps/client/src/features/acadenice/sync-blocks/services/sync-blocks-client.ts +++ b/apps/client/src/features/acadenice/sync-blocks/services/sync-blocks-client.ts @@ -17,7 +17,7 @@ export interface SyncBlockUsageDto { workspaceId: string; } -const BASE = '/api/acadenice/sync-blocks'; +const BASE = '/api/v1/sync-blocks'; export const syncBlocksClient = { create(content: Record = {}): Promise { diff --git a/apps/client/src/features/acadenice/templates-admin/__tests__/templates-client.test.ts b/apps/client/src/features/acadenice/templates-admin/__tests__/templates-client.test.ts index 8a74bb2f..ebadffa0 100644 --- a/apps/client/src/features/acadenice/templates-admin/__tests__/templates-client.test.ts +++ b/apps/client/src/features/acadenice/templates-admin/__tests__/templates-client.test.ts @@ -33,11 +33,11 @@ const sampleTemplate = { }; describe("templatesClient", () => { - it("list — GET /api/acadenice/templates", async () => { + it("list — GET /api/v1/templates", async () => { vi.mocked(axios.get).mockResolvedValueOnce({ data: [sampleTemplate] }); const client = await getClient(); const result = await client.list(); - expect(axios.get).toHaveBeenCalledWith("/api/acadenice/templates", { params: {} }); + expect(axios.get).toHaveBeenCalledWith("/api/v1/templates", { params: {} }); expect(result).toHaveLength(1); }); @@ -45,68 +45,68 @@ describe("templatesClient", () => { vi.mocked(axios.get).mockResolvedValueOnce({ data: [] }); const client = await getClient(); await client.list({ category: "meeting" }); - expect(axios.get).toHaveBeenCalledWith("/api/acadenice/templates", { + expect(axios.get).toHaveBeenCalledWith("/api/v1/templates", { params: { category: "meeting" }, }); }); - it("get — GET /api/acadenice/templates/:id", async () => { + it("get — GET /api/v1/templates/:id", async () => { vi.mocked(axios.get).mockResolvedValueOnce({ data: sampleTemplate }); const client = await getClient(); const result = await client.get("tmpl-1"); - expect(axios.get).toHaveBeenCalledWith("/api/acadenice/templates/tmpl-1"); + expect(axios.get).toHaveBeenCalledWith("/api/v1/templates/tmpl-1"); expect(result.id).toBe("tmpl-1"); }); - it("create — POST /api/acadenice/templates", async () => { + it("create — POST /api/v1/templates", async () => { vi.mocked(axios.post).mockResolvedValueOnce({ data: sampleTemplate }); const client = await getClient(); const result = await client.create({ name: "Meeting Note", category: "meeting" }); - expect(axios.post).toHaveBeenCalledWith("/api/acadenice/templates", { + expect(axios.post).toHaveBeenCalledWith("/api/v1/templates", { name: "Meeting Note", category: "meeting", }); expect(result.name).toBe("Meeting Note"); }); - it("update — PATCH /api/acadenice/templates/:id", async () => { + it("update — PATCH /api/v1/templates/:id", async () => { vi.mocked(axios.patch).mockResolvedValueOnce({ data: { ...sampleTemplate, name: "Updated" }, }); const client = await getClient(); const result = await client.update("tmpl-1", { name: "Updated" }); - expect(axios.patch).toHaveBeenCalledWith("/api/acadenice/templates/tmpl-1", { + expect(axios.patch).toHaveBeenCalledWith("/api/v1/templates/tmpl-1", { name: "Updated", }); expect(result.name).toBe("Updated"); }); - it("delete — DELETE /api/acadenice/templates/:id", async () => { + it("delete — DELETE /api/v1/templates/:id", async () => { vi.mocked(axios.delete).mockResolvedValueOnce({}); const client = await getClient(); await expect(client.delete("tmpl-1")).resolves.toBeUndefined(); - expect(axios.delete).toHaveBeenCalledWith("/api/acadenice/templates/tmpl-1"); + expect(axios.delete).toHaveBeenCalledWith("/api/v1/templates/tmpl-1"); }); - it("instantiate — POST /api/acadenice/templates/:id/instantiate", async () => { + it("instantiate — POST /api/v1/templates/:id/instantiate", async () => { vi.mocked(axios.post).mockResolvedValueOnce({ data: { pageId: "p1", slugId: "slug1" } }); const client = await getClient(); const result = await client.instantiate("tmpl-1", { spaceId: "space-1" }); expect(axios.post).toHaveBeenCalledWith( - "/api/acadenice/templates/tmpl-1/instantiate", + "/api/v1/templates/tmpl-1/instantiate", { spaceId: "space-1" }, ); expect(result.pageId).toBe("p1"); expect(result.slugId).toBe("slug1"); }); - it("setDefault — PATCH /api/acadenice/templates/:id/default", async () => { + it("setDefault — PATCH /api/v1/templates/:id/default", async () => { vi.mocked(axios.patch).mockResolvedValueOnce({ data: { ...sampleTemplate, isWorkspaceDefault: true }, }); const client = await getClient(); const result = await client.setDefault("tmpl-1"); - expect(axios.patch).toHaveBeenCalledWith("/api/acadenice/templates/tmpl-1/default"); + expect(axios.patch).toHaveBeenCalledWith("/api/v1/templates/tmpl-1/default"); expect(result.isWorkspaceDefault).toBe(true); }); @@ -122,7 +122,7 @@ describe("templatesClient", () => { const client = await getClient(); await client.instantiate("tmpl-1", { spaceId: "space-1", parentPageId: "parent-1" }); expect(axios.post).toHaveBeenCalledWith( - "/api/acadenice/templates/tmpl-1/instantiate", + "/api/v1/templates/tmpl-1/instantiate", { spaceId: "space-1", parentPageId: "parent-1" }, ); }); diff --git a/apps/client/src/features/acadenice/templates-admin/services/templates-client.ts b/apps/client/src/features/acadenice/templates-admin/services/templates-client.ts index a38542e1..6f78dfbf 100644 --- a/apps/client/src/features/acadenice/templates-admin/services/templates-client.ts +++ b/apps/client/src/features/acadenice/templates-admin/services/templates-client.ts @@ -36,7 +36,7 @@ export interface InstantiatePayload { name?: string; } -const BASE = '/api/acadenice/templates'; +const BASE = '/api/v1/templates'; export const templatesClient = { list(opts: { category?: string; search?: string } = {}): Promise { diff --git a/apps/extension-clipper/src/lib/api-client.ts b/apps/extension-clipper/src/lib/api-client.ts index 2954f4c8..d3193053 100644 --- a/apps/extension-clipper/src/lib/api-client.ts +++ b/apps/extension-clipper/src/lib/api-client.ts @@ -42,7 +42,7 @@ export async function sendClip( apiToken: string, payload: ClipPayload, ): Promise { - const url = `${apiUrl.replace(/\/$/, '')}/api/acadenice/clipper/import`; + const url = `${apiUrl.replace(/\/$/, '')}/api/v1/clipper/import`; let response: Response; try { diff --git a/apps/extension-clipper/src/popup/popup.ts b/apps/extension-clipper/src/popup/popup.ts index b939e82f..184790ee 100644 --- a/apps/extension-clipper/src/popup/popup.ts +++ b/apps/extension-clipper/src/popup/popup.ts @@ -8,7 +8,7 @@ * Flow: * 1. On open: load settings, query the active tab for page data. * 2. User fills in form and clicks "Clip". - * 3. sendClip() hits POST /api/acadenice/clipper/import. + * 3. sendClip() hits POST /api/v1/clipper/import. * 4. On success: show result link. On error: show typed error message. */ diff --git a/apps/extension-clipper/tests/api-client.test.ts b/apps/extension-clipper/tests/api-client.test.ts index 06844c14..aab68e98 100644 --- a/apps/extension-clipper/tests/api-client.test.ts +++ b/apps/extension-clipper/tests/api-client.test.ts @@ -43,14 +43,14 @@ describe('sendClip', () => { mockFetch(201, { pageId: 'p-1', slugId: 's-1', url: '' }); await sendClip(BASE_URL, TOKEN, samplePayload); const [url] = (global.fetch as any).mock.calls[0]; - expect(url).toBe(`${BASE_URL}/api/acadenice/clipper/import`); + expect(url).toBe(`${BASE_URL}/api/v1/clipper/import`); }); it('strips trailing slash from apiUrl', async () => { mockFetch(201, { pageId: 'p-1', slugId: 's-1', url: '' }); await sendClip(`${BASE_URL}/`, TOKEN, samplePayload); const [url] = (global.fetch as any).mock.calls[0]; - expect(url).toBe(`${BASE_URL}/api/acadenice/clipper/import`); + expect(url).toBe(`${BASE_URL}/api/v1/clipper/import`); }); it('throws ApiError with statusCode on 401', async () => { diff --git a/apps/server/src/core/acadenice/api-keys/api-keys.module.ts b/apps/server/src/core/acadenice/api-keys/api-keys.module.ts index fbb27079..e797684f 100644 --- a/apps/server/src/core/acadenice/api-keys/api-keys.module.ts +++ b/apps/server/src/core/acadenice/api-keys/api-keys.module.ts @@ -6,9 +6,9 @@ import { AcadeniceApiKeyService } from './services/api-key.service'; * AcadeniceApiKeysModule — personal access tokens (R4.5). * * Endpoints: - * GET /api/acadenice/api-keys - * POST /api/acadenice/api-keys - * DELETE /api/acadenice/api-keys/:id + * GET /api/v1/api-keys + * POST /api/v1/api-keys + * DELETE /api/v1/api-keys/:id * * Token format: acdk_<64 hex chars> * Storage: bcrypt hash only — plaintext returned once at creation. diff --git a/apps/server/src/core/acadenice/api-keys/controllers/api-key.controller.ts b/apps/server/src/core/acadenice/api-keys/controllers/api-key.controller.ts index 7be177d7..ee73f68e 100644 --- a/apps/server/src/core/acadenice/api-keys/controllers/api-key.controller.ts +++ b/apps/server/src/core/acadenice/api-keys/controllers/api-key.controller.ts @@ -25,12 +25,12 @@ import { CreateApiKeySchema } from '../dto/api-key.dto'; /** * AcadeniceApiKeyController — personal access tokens (R4.5). * - * GET /api/acadenice/api-keys List caller's tokens (no hashes) - * POST /api/acadenice/api-keys Create token — returns plain once - * DELETE /api/acadenice/api-keys/:id Revoke + * GET /api/v1/api-keys List caller's tokens (no hashes) + * POST /api/v1/api-keys Create token — returns plain once + * DELETE /api/v1/api-keys/:id Revoke */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/api-keys') +@Controller('v1/api-keys') export class AcadeniceApiKeyController { constructor(private readonly apiKeyService: AcadeniceApiKeyService) {} diff --git a/apps/server/src/core/acadenice/audit-log/audit-log.module.ts b/apps/server/src/core/acadenice/audit-log/audit-log.module.ts index 8998c3e0..2e9bad02 100644 --- a/apps/server/src/core/acadenice/audit-log/audit-log.module.ts +++ b/apps/server/src/core/acadenice/audit-log/audit-log.module.ts @@ -5,7 +5,7 @@ import { AcadeniceAuditLogService } from './services/audit-log.service'; /** * AcadeniceAuditLogModule — R4.5. * - * Exposes GET /api/acadenice/audit-log (admin/owner only). + * Exposes GET /api/v1/audit-log (admin/owner only). * Reads directly from the `audit` table via Kysely. * No EE dependency. */ diff --git a/apps/server/src/core/acadenice/audit-log/controllers/audit-log.controller.ts b/apps/server/src/core/acadenice/audit-log/controllers/audit-log.controller.ts index 3c1509aa..06eb7d4c 100644 --- a/apps/server/src/core/acadenice/audit-log/controllers/audit-log.controller.ts +++ b/apps/server/src/core/acadenice/audit-log/controllers/audit-log.controller.ts @@ -20,12 +20,12 @@ import { AuditLogQuerySchema } from '../dto/audit-log-query.dto'; /** * AcadeniceAuditLogController — R4.5 read-only audit log. * - * GET /api/acadenice/audit-log + * GET /api/v1/audit-log * Auth : JWT (admin or owner only) * Query : limit, offset, userId, action, since (ISO), until (ISO) */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/audit-log') +@Controller('v1/audit-log') export class AcadeniceAuditLogController { constructor(private readonly auditLogService: AcadeniceAuditLogService) {} diff --git a/apps/server/src/core/acadenice/backlinks/backlinks.module.ts b/apps/server/src/core/acadenice/backlinks/backlinks.module.ts index 8da74989..2b57d02b 100644 --- a/apps/server/src/core/acadenice/backlinks/backlinks.module.ts +++ b/apps/server/src/core/acadenice/backlinks/backlinks.module.ts @@ -12,7 +12,7 @@ import { PageContentUpdatedListener } from './events/page-content-updated.listen * - BacklinkParserService : walks Tiptap JSON, extracts links * - BacklinkIndexerService : delete-then-insert reindex per page * - BacklinkService : permission-aware query API - * - BacklinksController : REST GET /api/acadenice/pages/:id/backlinks + * - BacklinksController : REST GET /api/v1/pages/:id/backlinks * - PageContentUpdatedListener: reacts to collaboration saves * * Dependencies: diff --git a/apps/server/src/core/acadenice/backlinks/controllers/backlinks.controller.ts b/apps/server/src/core/acadenice/backlinks/controllers/backlinks.controller.ts index a7af5ff7..62725241 100644 --- a/apps/server/src/core/acadenice/backlinks/controllers/backlinks.controller.ts +++ b/apps/server/src/core/acadenice/backlinks/controllers/backlinks.controller.ts @@ -15,7 +15,7 @@ import { BacklinkService, BacklinksResult } from '../services/backlink.service'; /** * REST controller for the backlinks feature. * - * Route: GET /api/acadenice/pages/:pageId/backlinks + * Route: GET /api/v1/pages/:pageId/backlinks * * Authentication: JWT (JwtAuthGuard). The user's read access to each source * page is enforced inside BacklinkService (space_members / public space check). @@ -23,7 +23,7 @@ import { BacklinkService, BacklinksResult } from '../services/backlink.service'; * workspace member can query backlinks for pages they can read. */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/pages') +@Controller('v1/pages') export class BacklinksController { constructor(private readonly backlinkService: BacklinkService) {} diff --git a/apps/server/src/core/acadenice/clipper/controllers/clipper.controller.ts b/apps/server/src/core/acadenice/clipper/controllers/clipper.controller.ts index 05642651..9cb963d1 100644 --- a/apps/server/src/core/acadenice/clipper/controllers/clipper.controller.ts +++ b/apps/server/src/core/acadenice/clipper/controllers/clipper.controller.ts @@ -49,15 +49,15 @@ const TOKEN_HEADER = 'x-clipper-token'; /** * ClipperController — Web Clipper REST surface (R4.3). * - * POST /api/acadenice/clipper/import + * POST /api/v1/clipper/import * Auth: X-Clipper-Token header (token validated against DB hash). * Body: ImportClipDto (Zod). * - * POST /api/acadenice/clipper/tokens (JWT auth) - * GET /api/acadenice/clipper/tokens (JWT auth) - * DELETE /api/acadenice/clipper/tokens/:id (JWT auth) + * POST /api/v1/clipper/tokens (JWT auth) + * GET /api/v1/clipper/tokens (JWT auth) + * DELETE /api/v1/clipper/tokens/:id (JWT auth) */ -@Controller('acadenice/clipper') +@Controller('v1/clipper') export class ClipperController { constructor( private readonly clipperService: ClipperService, diff --git a/apps/server/src/core/acadenice/clipper/dto/import.dto.ts b/apps/server/src/core/acadenice/clipper/dto/import.dto.ts index 7d5e8c16..4a43d9e2 100644 --- a/apps/server/src/core/acadenice/clipper/dto/import.dto.ts +++ b/apps/server/src/core/acadenice/clipper/dto/import.dto.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; /** - * Zod schema for POST /api/acadenice/clipper/import. + * Zod schema for POST /api/v1/clipper/import. * * html_selection is expected to arrive pre-sanitized (DOMPurify on the * extension side). The server sanitizes again server-side via the existing @@ -22,7 +22,7 @@ export const ImportClipDto = z.object({ export type ImportClipDtoType = z.infer; /** - * Zod schema for POST /api/acadenice/clipper/tokens. + * Zod schema for POST /api/v1/clipper/tokens. * duration: days until expiry, or null for no expiry. */ export const CreateClipperTokenDto = z.object({ diff --git a/apps/server/src/core/acadenice/comments/controllers/page-comments.controller.ts b/apps/server/src/core/acadenice/comments/controllers/page-comments.controller.ts index 115e8462..9d588428 100644 --- a/apps/server/src/core/acadenice/comments/controllers/page-comments.controller.ts +++ b/apps/server/src/core/acadenice/comments/controllers/page-comments.controller.ts @@ -23,10 +23,10 @@ import { ResolvePageCommentDto } from '../dto/comment.dto'; * requiring an open collab websocket. * * Endpoints: - * POST /api/acadenice/page-comments/resolve + * POST /api/v1/page-comments/resolve */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/page-comments') +@Controller('v1/page-comments') export class PageCommentsController { constructor( private readonly pageCommentResolveService: PageCommentResolveService, diff --git a/apps/server/src/core/acadenice/comments/controllers/row-comments.controller.ts b/apps/server/src/core/acadenice/comments/controllers/row-comments.controller.ts index 5ec8a9a6..41b15cd8 100644 --- a/apps/server/src/core/acadenice/comments/controllers/row-comments.controller.ts +++ b/apps/server/src/core/acadenice/comments/controllers/row-comments.controller.ts @@ -29,15 +29,15 @@ import { * user's `acadenice_permissions` JWT claim. * * Endpoints: - * POST /api/acadenice/row-comments/list list thread for (tableId, rowId) - * POST /api/acadenice/row-comments/create create root or reply - * POST /api/acadenice/row-comments/update edit own comment - * POST /api/acadenice/row-comments/resolve resolve/unresolve root thread - * POST /api/acadenice/row-comments/delete delete own (or moderate) - * POST /api/acadenice/row-comments/count count comments for a row + * POST /api/v1/row-comments/list list thread for (tableId, rowId) + * POST /api/v1/row-comments/create create root or reply + * POST /api/v1/row-comments/update edit own comment + * POST /api/v1/row-comments/resolve resolve/unresolve root thread + * POST /api/v1/row-comments/delete delete own (or moderate) + * POST /api/v1/row-comments/count count comments for a row */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/row-comments') +@Controller('v1/row-comments') export class RowCommentsController { constructor(private readonly rowCommentService: RowCommentService) {} diff --git a/apps/server/src/core/acadenice/graph/controllers/graph.controller.ts b/apps/server/src/core/acadenice/graph/controllers/graph.controller.ts index 8b666e93..fbceb10a 100644 --- a/apps/server/src/core/acadenice/graph/controllers/graph.controller.ts +++ b/apps/server/src/core/acadenice/graph/controllers/graph.controller.ts @@ -15,7 +15,7 @@ import { GraphQuerySchema, GraphResponse } from '../dto/graph.dto'; /** * REST controller for the knowledge-graph endpoint (R3.5.1). * - * Route: GET /api/acadenice/graph + * Route: GET /api/v1/graph * * Authentication: JWT (JwtAuthGuard). Permission filtering is applied inside * GraphService using the same space_members / public visibility model as @@ -26,7 +26,7 @@ import { GraphQuerySchema, GraphResponse } from '../dto/graph.dto'; * the query param is accepted but ignored to prevent cross-workspace leaks. */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/graph') +@Controller('v1/graph') export class GraphController { constructor(private readonly graphService: GraphService) {} diff --git a/apps/server/src/core/acadenice/graph/graph.module.ts b/apps/server/src/core/acadenice/graph/graph.module.ts index e349948f..f6c22533 100644 --- a/apps/server/src/core/acadenice/graph/graph.module.ts +++ b/apps/server/src/core/acadenice/graph/graph.module.ts @@ -8,7 +8,7 @@ import { GraphService } from './services/graph.service'; * Provides: * - GraphService : builds { nodes, edges } from acadenice_backlink (R3.2) * with BFS traversal, permission filtering, Redis cache - * - GraphController : REST GET /api/acadenice/graph + * - GraphController : REST GET /api/v1/graph * * Dependencies: * - KyselyDB is global (AppModule). diff --git a/apps/server/src/core/acadenice/notifications/controllers/notification-preferences.controller.ts b/apps/server/src/core/acadenice/notifications/controllers/notification-preferences.controller.ts index 6b89d8c8..4cb6e5d4 100644 --- a/apps/server/src/core/acadenice/notifications/controllers/notification-preferences.controller.ts +++ b/apps/server/src/core/acadenice/notifications/controllers/notification-preferences.controller.ts @@ -28,7 +28,7 @@ import { ZodError } from 'zod'; * the native notification email pipeline. */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/notification-preferences') +@Controller('v1/notification-preferences') export class NotificationPreferencesController { constructor( private readonly prefsService: NotificationPreferencesService, diff --git a/apps/server/src/core/acadenice/notifications/controllers/notifications.controller.ts b/apps/server/src/core/acadenice/notifications/controllers/notifications.controller.ts index 558eda5a..ba0d5c85 100644 --- a/apps/server/src/core/acadenice/notifications/controllers/notifications.controller.ts +++ b/apps/server/src/core/acadenice/notifications/controllers/notifications.controller.ts @@ -45,19 +45,19 @@ function parseQuery(schema: { parse: (v: unknown) => T }, raw: unknown): T { * REST controller for Acadenice notification endpoints (R3.7). * * This controller is a thin facade over the native Docmost NotificationService. - * Endpoints are prefixed `/api/acadenice/notifications` to allow the Acadenice + * Endpoints are prefixed `/api/v1/notifications` to allow the Acadenice * frontend to discover and poll them without conflicting with the native * `/notifications` endpoints used by the upstream Docmost UI. * * All mutation endpoints use the same guards and service as the native path. */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/notifications') +@Controller('v1/notifications') export class AcadeniceNotificationsController { constructor(private readonly notificationService: NotificationService) {} /** - * GET /api/acadenice/notifications + * GET /api/v1/notifications * Paginated list of notifications for the authenticated user. */ @Get() @@ -82,7 +82,7 @@ export class AcadeniceNotificationsController { } /** - * GET /api/acadenice/notifications/unread-count + * GET /api/v1/notifications/unread-count */ @Get('unread-count') async unreadCount(@AuthUser() user: User) { @@ -90,7 +90,7 @@ export class AcadeniceNotificationsController { } /** - * POST /api/acadenice/notifications/read-all + * POST /api/v1/notifications/read-all */ @Post('read-all') @HttpCode(HttpStatus.NO_CONTENT) @@ -99,7 +99,7 @@ export class AcadeniceNotificationsController { } /** - * POST /api/acadenice/notifications/mark-read + * POST /api/v1/notifications/mark-read */ @Post('mark-read') @HttpCode(HttpStatus.NO_CONTENT) @@ -115,7 +115,7 @@ export class AcadeniceNotificationsController { } /** - * POST /api/acadenice/notifications/:id/read + * POST /api/v1/notifications/:id/read */ @Post(':id/read') @HttpCode(HttpStatus.NO_CONTENT) diff --git a/apps/server/src/core/acadenice/notifications/notifications.module.ts b/apps/server/src/core/acadenice/notifications/notifications.module.ts index ae4c4d0c..df4ee4e7 100644 --- a/apps/server/src/core/acadenice/notifications/notifications.module.ts +++ b/apps/server/src/core/acadenice/notifications/notifications.module.ts @@ -27,15 +27,15 @@ import { NotificationModule } from '../../notification/notification.module'; * Shared with native NotificationPref UI (same keys). * * 4. AcadeniceNotificationsController - * GET /api/acadenice/notifications (paginated) - * GET /api/acadenice/notifications/unread-count - * POST /api/acadenice/notifications/read-all - * POST /api/acadenice/notifications/mark-read - * POST /api/acadenice/notifications/:id/read + * GET /api/v1/notifications (paginated) + * GET /api/v1/notifications/unread-count + * POST /api/v1/notifications/read-all + * POST /api/v1/notifications/mark-read + * POST /api/v1/notifications/:id/read * * 5. NotificationPreferencesController - * GET /api/acadenice/notification-preferences - * PUT /api/acadenice/notification-preferences + * GET /api/v1/notification-preferences + * PUT /api/v1/notification-preferences * * Depends on: * - NotificationModule (exports NotificationService — also imports it) diff --git a/apps/server/src/core/acadenice/rbac/controllers/permissions.controller.ts b/apps/server/src/core/acadenice/rbac/controllers/permissions.controller.ts index 11c9588f..0aed9391 100644 --- a/apps/server/src/core/acadenice/rbac/controllers/permissions.controller.ts +++ b/apps/server/src/core/acadenice/rbac/controllers/permissions.controller.ts @@ -15,7 +15,7 @@ import { AcadeniceRoleService } from '../services/role.service'; const ADMIN_WILDCARD_KEY = 'admin:*'; @UseGuards(JwtAuthGuard) -@Controller('acadenice/permissions') +@Controller('v1/permissions') export class AcadenicePermissionsController { constructor(private readonly roleService: AcadeniceRoleService) {} diff --git a/apps/server/src/core/acadenice/rbac/controllers/roles.controller.ts b/apps/server/src/core/acadenice/rbac/controllers/roles.controller.ts index eb06b4ea..f578ad8e 100644 --- a/apps/server/src/core/acadenice/rbac/controllers/roles.controller.ts +++ b/apps/server/src/core/acadenice/rbac/controllers/roles.controller.ts @@ -26,7 +26,7 @@ import { AcadenicePermissionsGuard } from '../guards/permissions.guard'; import { RequirePermission } from '../guards/require-permission.decorator'; @UseGuards(JwtAuthGuard, AcadenicePermissionsGuard) -@Controller('acadenice/roles') +@Controller('v1/roles') export class AcadeniceRolesController { constructor(private readonly roleService: AcadeniceRoleService) {} diff --git a/apps/server/src/core/acadenice/rbac/controllers/user-roles.controller.ts b/apps/server/src/core/acadenice/rbac/controllers/user-roles.controller.ts index 6265ce97..5a0c2422 100644 --- a/apps/server/src/core/acadenice/rbac/controllers/user-roles.controller.ts +++ b/apps/server/src/core/acadenice/rbac/controllers/user-roles.controller.ts @@ -34,7 +34,7 @@ import { permissionMatches } from '../permissions-catalog'; * because the access logic depends on `userId` path param vs the actor. */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/users/:userId/roles') +@Controller('v1/users/:userId/roles') export class AcadeniceUserRolesController { constructor(private readonly roleService: AcadeniceRoleService) {} diff --git a/apps/server/src/core/acadenice/security/controllers/oidc-status.controller.ts b/apps/server/src/core/acadenice/security/controllers/oidc-status.controller.ts index cdeb6248..2d8408e4 100644 --- a/apps/server/src/core/acadenice/security/controllers/oidc-status.controller.ts +++ b/apps/server/src/core/acadenice/security/controllers/oidc-status.controller.ts @@ -22,12 +22,12 @@ export interface OidcStatusResponse { /** * AcadeniceOidcStatusController — R4.5 read-only OIDC status for admins. * - * GET /api/acadenice/security/oidc-status + * GET /api/v1/security/oidc-status * Auth : JWT (admin or owner only) * Returns OIDC configuration derived from env vars — no secrets exposed. */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/security') +@Controller('v1/security') export class AcadeniceOidcStatusController { constructor(private readonly env: EnvironmentService) {} diff --git a/apps/server/src/core/acadenice/security/security.module.ts b/apps/server/src/core/acadenice/security/security.module.ts index 58e980f8..ad4610c6 100644 --- a/apps/server/src/core/acadenice/security/security.module.ts +++ b/apps/server/src/core/acadenice/security/security.module.ts @@ -4,7 +4,7 @@ import { AcadeniceOidcStatusController } from './controllers/oidc-status.control /** * AcadeniceSecurityModule — R4.5. * - * Exposes GET /api/acadenice/security/oidc-status (admin/owner only). + * Exposes GET /api/v1/security/oidc-status (admin/owner only). * Returns OIDC configuration from environment — never exposes client_secret. */ @Module({ diff --git a/apps/server/src/core/acadenice/slash-commands/controllers/slash-commands.controller.ts b/apps/server/src/core/acadenice/slash-commands/controllers/slash-commands.controller.ts index fd303207..a11897d3 100644 --- a/apps/server/src/core/acadenice/slash-commands/controllers/slash-commands.controller.ts +++ b/apps/server/src/core/acadenice/slash-commands/controllers/slash-commands.controller.ts @@ -54,7 +54,7 @@ function parseBody(schema: { parse: (v: unknown) => T }, body: unknown): T { * default via the seed — see permissions-catalog.ts). */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/slash-commands') +@Controller('v1/slash-commands') export class SlashCommandsController { constructor(private readonly slashCommandService: SlashCommandService) {} diff --git a/apps/server/src/core/acadenice/sync-blocks/controllers/sync-blocks.controller.ts b/apps/server/src/core/acadenice/sync-blocks/controllers/sync-blocks.controller.ts index 6c95aa70..67f32661 100644 --- a/apps/server/src/core/acadenice/sync-blocks/controllers/sync-blocks.controller.ts +++ b/apps/server/src/core/acadenice/sync-blocks/controllers/sync-blocks.controller.ts @@ -31,14 +31,14 @@ import { * automatically scoped to the authenticated workspace. * * Routes: - * POST /api/acadenice/sync-blocks create master block - * GET /api/acadenice/sync-blocks/:id read content - * PATCH /api/acadenice/sync-blocks/:id update content - * DELETE /api/acadenice/sync-blocks/:id delete master - * GET /api/acadenice/sync-blocks/:id/usages list referencing pages + * POST /api/v1/sync-blocks create master block + * GET /api/v1/sync-blocks/:id read content + * PATCH /api/v1/sync-blocks/:id update content + * DELETE /api/v1/sync-blocks/:id delete master + * GET /api/v1/sync-blocks/:id/usages list referencing pages */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/sync-blocks') +@Controller('v1/sync-blocks') export class SyncBlocksController { constructor(private readonly syncBlocksService: SyncBlocksService) {} diff --git a/apps/server/src/core/acadenice/templates/controllers/templates.controller.ts b/apps/server/src/core/acadenice/templates/controllers/templates.controller.ts index 234295a6..18cfad6d 100644 --- a/apps/server/src/core/acadenice/templates/controllers/templates.controller.ts +++ b/apps/server/src/core/acadenice/templates/controllers/templates.controller.ts @@ -56,16 +56,16 @@ function parseBody(schema: { parse: (v: unknown) => T }, body: unknown): T { * REST controller for workspace page templates (R3.6). * * Permission matrix: - * GET /acadenice/templates templates:read - * GET /acadenice/templates/:id templates:read - * POST /acadenice/templates templates:create - * PATCH /acadenice/templates/:id owner-or-manage (service enforces) - * DELETE /acadenice/templates/:id owner-or-manage (service enforces) - * POST /acadenice/templates/:id/instantiate templates:read - * PATCH /acadenice/templates/:id/default templates:manage + * GET /v1/templates templates:read + * GET /v1/templates/:id templates:read + * POST /v1/templates templates:create + * PATCH /v1/templates/:id owner-or-manage (service enforces) + * DELETE /v1/templates/:id owner-or-manage (service enforces) + * POST /v1/templates/:id/instantiate templates:read + * PATCH /v1/templates/:id/default templates:manage */ @UseGuards(JwtAuthGuard) -@Controller('acadenice/templates') +@Controller('v1/templates') export class TemplatesController { constructor( private readonly templateService: TemplateService, diff --git a/apps/server/src/database/migrations/20260508T120000-create-acadenice-slash-command.ts b/apps/server/src/database/migrations/20260508T120000-create-acadenice-slash-command.ts index 4c434113..8f84eac9 100644 --- a/apps/server/src/database/migrations/20260508T120000-create-acadenice-slash-command.ts +++ b/apps/server/src/database/migrations/20260508T120000-create-acadenice-slash-command.ts @@ -15,7 +15,7 @@ import { Kysely, sql } from 'kysely'; * * UNIQUE(workspace_id, keyword) prevents collision within the same workspace. * Both systems and custom commands share the slash menu — the runtime fetches - * custom ones via GET /api/acadenice/slash-commands and merges at menu-open time. + * custom ones via GET /api/v1/slash-commands and merges at menu-open time. * * Idempotent: ifNotExists on every CREATE so migration re-runs never fail. */