refactor(acadedoc): rename API routes /api/acadenice -> /api/v1 — R5.1

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 <noreply@anthropic.com>
This commit is contained in:
Corentin JOGUET 2026-05-08 14:52:49 +02:00
parent 3af579498b
commit 9dd283ced6
48 changed files with 164 additions and 164 deletions

View file

@ -30,8 +30,8 @@ The native Docmost system already provides the complete mention notification pip
R3.7 adds: R3.7 adds:
1. `MentionDetectorService` — pure service that walks Tiptap JSON and extracts user mentions (no DB). Used by the emitter and independently testable. 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.). 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`. 3. `AcadeniceNotificationsController` — facade over native `NotificationService`, prefix `/api/v1/notifications`.
4. `NotificationPreferencesController` — GET/PUT `/api/acadenice/notification-preferences` (reads/writes native `users.settings.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. 5. Frontend `/notifications` page — full inbox using native `NotificationItem` component.
6. Frontend `/settings/notifications` preferences page — dedicated toggles via Acadenice API. 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/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-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/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/notifications.controller.ts` | REST facade /api/v1/notifications |
| `apps/server/src/core/acadenice/notifications/controllers/notification-preferences.controller.ts` | REST /api/acadenice/notification-preferences | | `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/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/notifications.controller.spec.ts` | 10 unit tests |
| `apps/server/src/core/acadenice/notifications/spec/notification-preferences.spec.ts` | 4 unit tests | | `apps/server/src/core/acadenice/notifications/spec/notification-preferences.spec.ts` | 4 unit tests |
@ -77,13 +77,13 @@ R3.7 adds:
### Endpoints ### Endpoints
``` ```
GET /api/acadenice/notifications paginated list GET /api/v1/notifications paginated list
GET /api/acadenice/notifications/unread-count unread badge count (polled 30s) GET /api/v1/notifications/unread-count unread badge count (polled 30s)
POST /api/acadenice/notifications/read-all mark all read POST /api/v1/notifications/read-all mark all read
POST /api/acadenice/notifications/mark-read bulk mark read POST /api/v1/notifications/mark-read bulk mark read
POST /api/acadenice/notifications/:id/read single mark read POST /api/v1/notifications/:id/read single mark read
GET /api/acadenice/notification-preferences get prefs GET /api/v1/notification-preferences get prefs
PUT /api/acadenice/notification-preferences update prefs PUT /api/v1/notification-preferences update prefs
``` ```
### Tests ### Tests

View file

@ -6,17 +6,17 @@ import {
} from "../types/api-key.types"; } from "../types/api-key.types";
export async function listAcadeniceApiKeys(): Promise<AcadeniceApiKey[]> { export async function listAcadeniceApiKeys(): Promise<AcadeniceApiKey[]> {
const resp = await api.get("/acadenice/api-keys"); const resp = await api.get("/v1/api-keys");
return resp.data; return resp.data;
} }
export async function createAcadeniceApiKey( export async function createAcadeniceApiKey(
data: CreateAcadeniceApiKeyRequest, data: CreateAcadeniceApiKeyRequest,
): Promise<CreateAcadeniceApiKeyResponse> { ): Promise<CreateAcadeniceApiKeyResponse> {
const resp = await api.post("/acadenice/api-keys", data); const resp = await api.post("/v1/api-keys", data);
return resp.data; return resp.data;
} }
export async function revokeAcadeniceApiKey(id: string): Promise<void> { export async function revokeAcadeniceApiKey(id: string): Promise<void> {
await api.delete(`/acadenice/api-keys/${id}`); await api.delete(`/v1/api-keys/${id}`);
} }

View file

@ -7,6 +7,6 @@ import {
export async function getAcadeniceAuditLogs( export async function getAcadeniceAuditLogs(
params: AcadeniceAuditLogQuery = {}, params: AcadeniceAuditLogQuery = {},
): Promise<AcadeniceAuditLogPage> { ): Promise<AcadeniceAuditLogPage> {
const resp = await api.get("/acadenice/audit-log", { params }); const resp = await api.get("/v1/audit-log", { params });
return resp.data; return resp.data;
} }

View file

@ -18,12 +18,12 @@ describe('clipperClient', () => {
afterEach(() => jest.resetAllMocks()); afterEach(() => jest.resetAllMocks());
describe('listTokens', () => { describe('listTokens', () => {
it('GETs /api/acadenice/clipper/tokens', async () => { it('GETs /api/v1/clipper/tokens', async () => {
mockedAxios.get = jest.fn().mockResolvedValue({ data: [sampleToken] }); mockedAxios.get = jest.fn().mockResolvedValue({ data: [sampleToken] });
const result = await clipperClient.listTokens(); const result = await clipperClient.listTokens();
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0].id).toBe('tk-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.token).toBe('clip_abc123');
expect(result.tokenInfo.label).toBe('My token'); expect(result.tokenInfo.label).toBe('My token');
expect(mockedAxios.post).toHaveBeenCalledWith( expect(mockedAxios.post).toHaveBeenCalledWith(
'/api/acadenice/clipper/tokens', '/api/v1/clipper/tokens',
{ label: 'My token', duration_days: 30 }, { label: 'My token', duration_days: 30 },
); );
}); });
@ -47,7 +47,7 @@ describe('clipperClient', () => {
it('DELETEs the token by id', async () => { it('DELETEs the token by id', async () => {
mockedAxios.delete = jest.fn().mockResolvedValue({}); mockedAxios.delete = jest.fn().mockResolvedValue({});
await clipperClient.revokeToken('tk-1'); 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');
}); });
}); });
}); });

View file

@ -1,6 +1,6 @@
import axios from 'axios'; import axios from 'axios';
const BASE = '/api/acadenice/clipper'; const BASE = '/api/v1/clipper';
export interface ClipperTokenInfo { export interface ClipperTokenInfo {
id: string; id: string;

View file

@ -51,14 +51,14 @@ describe("row-comments-client", () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
it("listRowComments posts to /acadenice/row-comments/list", async () => { it("listRowComments posts to /v1/row-comments/list", async () => {
const comment = makeComment(); const comment = makeComment();
mockApi.post.mockResolvedValueOnce({ data: [comment] }); mockApi.post.mockResolvedValueOnce({ data: [comment] });
const result = await listRowComments({ tableId: TABLE_ID, rowId: ROW_ID }); const result = await listRowComments({ tableId: TABLE_ID, rowId: ROW_ID });
expect(mockApi.post).toHaveBeenCalledWith( expect(mockApi.post).toHaveBeenCalledWith(
"/acadenice/row-comments/list", "/v1/row-comments/list",
{ tableId: TABLE_ID, rowId: ROW_ID }, { tableId: TABLE_ID, rowId: ROW_ID },
); );
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
@ -69,12 +69,12 @@ describe("row-comments-client", () => {
mockApi.post.mockResolvedValueOnce({ data: [] }); mockApi.post.mockResolvedValueOnce({ data: [] });
await listRowComments({ tableId: TABLE_ID, rowId: ROW_ID, resolved: true }); await listRowComments({ tableId: TABLE_ID, rowId: ROW_ID, resolved: true });
expect(mockApi.post).toHaveBeenCalledWith( expect(mockApi.post).toHaveBeenCalledWith(
"/acadenice/row-comments/list", "/v1/row-comments/list",
{ tableId: TABLE_ID, rowId: ROW_ID, resolved: true }, { 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(); const comment = makeComment();
mockApi.post.mockResolvedValueOnce({ data: comment }); mockApi.post.mockResolvedValueOnce({ data: comment });
@ -86,42 +86,42 @@ describe("row-comments-client", () => {
const result = await createRowComment(params); const result = await createRowComment(params);
expect(mockApi.post).toHaveBeenCalledWith( expect(mockApi.post).toHaveBeenCalledWith(
"/acadenice/row-comments/create", "/v1/row-comments/create",
params, params,
); );
expect(result.id).toBe(COMMENT_ID); 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(); const comment = makeComment();
mockApi.post.mockResolvedValueOnce({ data: comment }); mockApi.post.mockResolvedValueOnce({ data: comment });
await updateRowComment(COMMENT_ID, JSON.stringify({ type: "doc" })); await updateRowComment(COMMENT_ID, JSON.stringify({ type: "doc" }));
expect(mockApi.post).toHaveBeenCalledWith( expect(mockApi.post).toHaveBeenCalledWith(
"/acadenice/row-comments/update", "/v1/row-comments/update",
{ commentId: COMMENT_ID, content: JSON.stringify({ type: "doc" }) }, { 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 }; const comment = { ...makeComment(), isResolved: true };
mockApi.post.mockResolvedValueOnce({ data: comment }); mockApi.post.mockResolvedValueOnce({ data: comment });
const result = await resolveRowComment(COMMENT_ID, true); const result = await resolveRowComment(COMMENT_ID, true);
expect(mockApi.post).toHaveBeenCalledWith( expect(mockApi.post).toHaveBeenCalledWith(
"/acadenice/row-comments/resolve", "/v1/row-comments/resolve",
{ commentId: COMMENT_ID, resolved: true }, { commentId: COMMENT_ID, resolved: true },
); );
expect(result.isResolved).toBe(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 }); mockApi.post.mockResolvedValueOnce({ data: undefined });
await deleteRowComment(COMMENT_ID); await deleteRowComment(COMMENT_ID);
expect(mockApi.post).toHaveBeenCalledWith( expect(mockApi.post).toHaveBeenCalledWith(
"/acadenice/row-comments/delete", "/v1/row-comments/delete",
{ commentId: COMMENT_ID }, { commentId: COMMENT_ID },
); );
}); });
@ -131,7 +131,7 @@ describe("row-comments-client", () => {
const count = await countRowComments(TABLE_ID, ROW_ID); const count = await countRowComments(TABLE_ID, ROW_ID);
expect(count).toBe(7); expect(count).toBe(7);
expect(mockApi.post).toHaveBeenCalledWith( expect(mockApi.post).toHaveBeenCalledWith(
"/acadenice/row-comments/count", "/v1/row-comments/count",
{ tableId: TABLE_ID, rowId: ROW_ID }, { tableId: TABLE_ID, rowId: ROW_ID },
); );
}); });

View file

@ -34,7 +34,7 @@ export async function listRowComments(
params: ListRowCommentsParams, params: ListRowCommentsParams,
): Promise<RowComment[]> { ): Promise<RowComment[]> {
const res = await api.post<RowComment[]>( const res = await api.post<RowComment[]>(
"/acadenice/row-comments/list", "/v1/row-comments/list",
params, params,
); );
return res.data; return res.data;
@ -44,7 +44,7 @@ export async function createRowComment(
params: CreateRowCommentParams, params: CreateRowCommentParams,
): Promise<RowComment> { ): Promise<RowComment> {
const res = await api.post<RowComment>( const res = await api.post<RowComment>(
"/acadenice/row-comments/create", "/v1/row-comments/create",
params, params,
); );
return res.data; return res.data;
@ -54,7 +54,7 @@ export async function updateRowComment(
commentId: string, commentId: string,
content: string, content: string,
): Promise<RowComment> { ): Promise<RowComment> {
const res = await api.post<RowComment>("/acadenice/row-comments/update", { const res = await api.post<RowComment>("/v1/row-comments/update", {
commentId, commentId,
content, content,
}); });
@ -65,7 +65,7 @@ export async function resolveRowComment(
commentId: string, commentId: string,
resolved: boolean, resolved: boolean,
): Promise<RowComment> { ): Promise<RowComment> {
const res = await api.post<RowComment>("/acadenice/row-comments/resolve", { const res = await api.post<RowComment>("/v1/row-comments/resolve", {
commentId, commentId,
resolved, resolved,
}); });
@ -73,7 +73,7 @@ export async function resolveRowComment(
} }
export async function deleteRowComment(commentId: string): Promise<void> { export async function deleteRowComment(commentId: string): Promise<void> {
await api.post("/acadenice/row-comments/delete", { commentId }); await api.post("/v1/row-comments/delete", { commentId });
} }
export async function countRowComments( export async function countRowComments(
@ -81,7 +81,7 @@ export async function countRowComments(
rowId: string, rowId: string,
): Promise<number> { ): Promise<number> {
const res = await api.post<{ count: number }>( const res = await api.post<{ count: number }>(
"/acadenice/row-comments/count", "/v1/row-comments/count",
{ tableId, rowId }, { tableId, rowId },
); );
return res.data.count; return res.data.count;

View file

@ -4,7 +4,7 @@ import { useMemo } from "react";
* Reads the user's Acadenice permissions from the auth context. * Reads the user's Acadenice permissions from the auth context.
* *
* Why not a server call here: * 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 * 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 * 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 * JS-readable cookie. It does NOT perform a new HTTP request callers that need

View file

@ -54,7 +54,7 @@ beforeEach(() => {
describe("fetchGraph", () => { describe("fetchGraph", () => {
it("calls /acadenice/graph with no params when all optional", async () => { it("calls /acadenice/graph with no params when all optional", async () => {
await fetchGraph({}); await fetchGraph({});
expect(mockGet).toHaveBeenCalledWith("/acadenice/graph"); expect(mockGet).toHaveBeenCalledWith("/v1/graph");
}); });
it("appends depth query param", async () => { it("appends depth query param", async () => {

View file

@ -67,7 +67,7 @@ export async function fetchGraph(
query.includeOrphans = String(params.includeOrphans); query.includeOrphans = String(params.includeOrphans);
const qs = new URLSearchParams(query).toString(); 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<GraphResponse>; return api.get(url) as unknown as Promise<GraphResponse>;
} }

View file

@ -29,11 +29,11 @@ describe("notificationsClient", () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
it("list: calls GET /api/acadenice/notifications", async () => { it("list: calls GET /api/v1/notifications", async () => {
mockApi.get.mockResolvedValue({ data: { items: [], meta: {} } }); mockApi.get.mockResolvedValue({ data: { items: [], meta: {} } });
const result = await notificationsClient.list(); const result = await notificationsClient.list();
expect(mockApi.get).toHaveBeenCalledWith( expect(mockApi.get).toHaveBeenCalledWith(
"/api/acadenice/notifications", "/api/v1/notifications",
expect.objectContaining({ params: {} }), expect.objectContaining({ params: {} }),
); );
expect(result).toEqual({ items: [], meta: {} }); expect(result).toEqual({ items: [], meta: {} });
@ -43,16 +43,16 @@ describe("notificationsClient", () => {
mockApi.get.mockResolvedValue({ data: { items: [] } }); mockApi.get.mockResolvedValue({ data: { items: [] } });
await notificationsClient.list({ tab: "direct" }); await notificationsClient.list({ tab: "direct" });
expect(mockApi.get).toHaveBeenCalledWith( expect(mockApi.get).toHaveBeenCalledWith(
"/api/acadenice/notifications", "/api/v1/notifications",
expect.objectContaining({ params: { tab: "direct" } }), 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 } }); mockApi.get.mockResolvedValue({ data: { count: 3 } });
const result = await notificationsClient.unreadCount(); const result = await notificationsClient.unreadCount();
expect(mockApi.get).toHaveBeenCalledWith( expect(mockApi.get).toHaveBeenCalledWith(
"/api/acadenice/notifications/unread-count", "/api/v1/notifications/unread-count",
); );
expect(result).toEqual({ count: 3 }); expect(result).toEqual({ count: 3 });
}); });
@ -61,7 +61,7 @@ describe("notificationsClient", () => {
mockApi.post.mockResolvedValue({}); mockApi.post.mockResolvedValue({});
await notificationsClient.markRead(["id-1", "id-2"]); await notificationsClient.markRead(["id-1", "id-2"]);
expect(mockApi.post).toHaveBeenCalledWith( expect(mockApi.post).toHaveBeenCalledWith(
"/api/acadenice/notifications/mark-read", "/api/v1/notifications/mark-read",
{ notificationIds: ["id-1", "id-2"] }, { notificationIds: ["id-1", "id-2"] },
); );
}); });
@ -70,7 +70,7 @@ describe("notificationsClient", () => {
mockApi.post.mockResolvedValue({}); mockApi.post.mockResolvedValue({});
await notificationsClient.markAllRead(); await notificationsClient.markAllRead();
expect(mockApi.post).toHaveBeenCalledWith( expect(mockApi.post).toHaveBeenCalledWith(
"/api/acadenice/notifications/read-all", "/api/v1/notifications/read-all",
); );
}); });
@ -78,11 +78,11 @@ describe("notificationsClient", () => {
mockApi.post.mockResolvedValue({}); mockApi.post.mockResolvedValue({});
await notificationsClient.markOne("notif-uuid"); await notificationsClient.markOne("notif-uuid");
expect(mockApi.post).toHaveBeenCalledWith( 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 = { const prefs = {
emailMentions: true, emailMentions: true,
emailReplies: false, emailReplies: false,
@ -94,12 +94,12 @@ describe("notificationsClient", () => {
mockApi.get.mockResolvedValue({ data: prefs }); mockApi.get.mockResolvedValue({ data: prefs });
const result = await notificationsClient.getPreferences(); const result = await notificationsClient.getPreferences();
expect(mockApi.get).toHaveBeenCalledWith( expect(mockApi.get).toHaveBeenCalledWith(
"/api/acadenice/notification-preferences", "/api/v1/notification-preferences",
); );
expect(result).toEqual(prefs); 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 payload = { emailMentions: false };
const updatedPrefs = { const updatedPrefs = {
emailMentions: false, emailMentions: false,
@ -112,7 +112,7 @@ describe("notificationsClient", () => {
mockApi.put.mockResolvedValue({ data: updatedPrefs }); mockApi.put.mockResolvedValue({ data: updatedPrefs });
const result = await notificationsClient.updatePreferences(payload); const result = await notificationsClient.updatePreferences(payload);
expect(mockApi.put).toHaveBeenCalledWith( expect(mockApi.put).toHaveBeenCalledWith(
"/api/acadenice/notification-preferences", "/api/v1/notification-preferences",
payload, payload,
); );
expect(result).toEqual(updatedPrefs); expect(result).toEqual(updatedPrefs);

View file

@ -13,8 +13,8 @@ export interface NotificationPreferences {
export type UpdatePreferencesPayload = Partial<NotificationPreferences>; export type UpdatePreferencesPayload = Partial<NotificationPreferences>;
const BASE = "/api/acadenice/notifications"; const BASE = "/api/v1/notifications";
const PREFS_BASE = "/api/acadenice/notification-preferences"; const PREFS_BASE = "/api/v1/notification-preferences";
/** /**
* HTTP client for Acadenice notification endpoints (R3.7). * HTTP client for Acadenice notification endpoints (R3.7).

View file

@ -2,6 +2,6 @@ import api from "@/lib/api-client";
import { OidcStatusResponse } from "../types/oidc-status.types"; import { OidcStatusResponse } from "../types/oidc-status.types";
export async function getOidcStatus(): Promise<OidcStatusResponse> { export async function getOidcStatus(): Promise<OidcStatusResponse> {
const resp = await api.get("/acadenice/security/oidc-status"); const resp = await api.get("/v1/security/oidc-status");
return resp.data; return resp.data;
} }

View file

@ -12,7 +12,7 @@ export const MY_PERMISSIONS_QUERY_KEY = [
/** /**
* Source-of-truth hook for the current user's Acadenice permissions. * 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 * 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 * (R2.1). We keep a 60s React Query staleTime to mirror that TTL: refetching
* sooner only hits the same Redis value. * sooner only hits the same Redis value.

View file

@ -11,14 +11,14 @@ import {
/** /**
* REST client for the Acadenice RBAC API (R2.1 backend). * 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 * Note : Docmost's axios interceptor returns `response.data` directly, so the
* return value of `api.get(...)` is already the body payload. * return value of `api.get(...)` is already the body payload.
*/ */
export async function getPermissionsCatalog(): Promise<IPermissionDescriptor[]> { export async function getPermissionsCatalog(): Promise<IPermissionDescriptor[]> {
return api.get("/acadenice/permissions") as unknown as Promise< return api.get("/v1/permissions") as unknown as Promise<
IPermissionDescriptor[] IPermissionDescriptor[]
>; >;
} }
@ -29,17 +29,17 @@ export async function getPermissionsCatalog(): Promise<IPermissionDescriptor[]>
*/ */
export async function getMyPermissions(): Promise<IMyPermissionsResponse> { export async function getMyPermissions(): Promise<IMyPermissionsResponse> {
return api.get( return api.get(
"/acadenice/permissions/me", "/v1/permissions/me",
) as unknown as Promise<IMyPermissionsResponse>; ) as unknown as Promise<IMyPermissionsResponse>;
} }
export async function listRoles(): Promise<IRole[]> { export async function listRoles(): Promise<IRole[]> {
return api.get("/acadenice/roles") as unknown as Promise<IRole[]>; return api.get("/v1/roles") as unknown as Promise<IRole[]>;
} }
export async function getRole(roleId: string): Promise<IRoleWithPermissions> { export async function getRole(roleId: string): Promise<IRoleWithPermissions> {
return api.get( return api.get(
`/acadenice/roles/${roleId}`, `/v1/roles/${roleId}`,
) as unknown as Promise<IRoleWithPermissions>; ) as unknown as Promise<IRoleWithPermissions>;
} }
@ -47,7 +47,7 @@ export async function createRole(
payload: ICreateRolePayload, payload: ICreateRolePayload,
): Promise<IRoleWithPermissions> { ): Promise<IRoleWithPermissions> {
return api.post( return api.post(
"/acadenice/roles", "/v1/roles",
payload, payload,
) as unknown as Promise<IRoleWithPermissions>; ) as unknown as Promise<IRoleWithPermissions>;
} }
@ -57,20 +57,20 @@ export async function updateRole(
payload: IUpdateRolePayload, payload: IUpdateRolePayload,
): Promise<IRole> { ): Promise<IRole> {
return api.patch( return api.patch(
`/acadenice/roles/${roleId}`, `/v1/roles/${roleId}`,
payload, payload,
) as unknown as Promise<IRole>; ) as unknown as Promise<IRole>;
} }
export async function deleteRole(roleId: string): Promise<void> { export async function deleteRole(roleId: string): Promise<void> {
await api.delete(`/acadenice/roles/${roleId}`); await api.delete(`/v1/roles/${roleId}`);
} }
export async function setRolePermissions( export async function setRolePermissions(
roleId: string, roleId: string,
permissions: string[], permissions: string[],
): Promise<IRoleWithPermissions> { ): Promise<IRoleWithPermissions> {
return api.put(`/acadenice/roles/${roleId}/permissions`, { return api.put(`/v1/roles/${roleId}/permissions`, {
permissions, permissions,
}) as unknown as Promise<IRoleWithPermissions>; }) as unknown as Promise<IRoleWithPermissions>;
} }
@ -78,7 +78,7 @@ export async function setRolePermissions(
export async function listUserRoles( export async function listUserRoles(
userId: string, userId: string,
): Promise<IUserRoleAssignment[]> { ): Promise<IUserRoleAssignment[]> {
return api.get(`/acadenice/users/${userId}/roles`) as unknown as Promise< return api.get(`/v1/users/${userId}/roles`) as unknown as Promise<
IUserRoleAssignment[] IUserRoleAssignment[]
>; >;
} }
@ -87,7 +87,7 @@ export async function assignRolesToUser(
userId: string, userId: string,
roleIds: string[], roleIds: string[],
): Promise<{ ok: true }> { ): Promise<{ ok: true }> {
return api.post(`/acadenice/users/${userId}/roles`, { return api.post(`/v1/users/${userId}/roles`, {
roleIds, roleIds,
}) as unknown as Promise<{ ok: true }>; }) as unknown as Promise<{ ok: true }>;
} }
@ -96,5 +96,5 @@ export async function unassignRoleFromUser(
userId: string, userId: string,
roleId: string, roleId: string,
): Promise<void> { ): Promise<void> {
await api.delete(`/acadenice/users/${userId}/roles/${roleId}`); await api.delete(`/v1/users/${userId}/roles/${roleId}`);
} }

View file

@ -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 * `is_admin_wildcard` is a cheap boolean so the UI can branch without
* scanning the array. * scanning the array.
*/ */

View file

@ -35,7 +35,7 @@ export interface CreateSlashCommandPayload {
export type UpdateSlashCommandPayload = Partial<CreateSlashCommandPayload>; export type UpdateSlashCommandPayload = Partial<CreateSlashCommandPayload>;
const BASE = '/api/acadenice/slash-commands'; const BASE = '/api/v1/slash-commands';
export const slashCommandsClient = { export const slashCommandsClient = {
list(): Promise<SlashCommandDto[]> { list(): Promise<SlashCommandDto[]> {

View file

@ -7,7 +7,7 @@ export const SYNC_BLOCK_QUERY_KEY = "sync-block";
* SSE hook for sync block realtime updates (R4.2). * SSE hook for sync block realtime updates (R4.2).
* *
* Listens to the NestJS EventEmitter2 via an SSE endpoint: * 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, * 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 * 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(() => { useEffect(() => {
if (!masterId) return; if (!masterId) return;
const sseUrl = `/api/acadenice/sync-blocks/${encodeURIComponent(masterId)}/events`; const sseUrl = `/api/v1/sync-blocks/${encodeURIComponent(masterId)}/events`;
let retryTimeout: ReturnType<typeof setTimeout> | null = null; let retryTimeout: ReturnType<typeof setTimeout> | null = null;

View file

@ -17,7 +17,7 @@ export interface SyncBlockUsageDto {
workspaceId: string; workspaceId: string;
} }
const BASE = '/api/acadenice/sync-blocks'; const BASE = '/api/v1/sync-blocks';
export const syncBlocksClient = { export const syncBlocksClient = {
create(content: Record<string, unknown> = {}): Promise<SyncBlockDto> { create(content: Record<string, unknown> = {}): Promise<SyncBlockDto> {

View file

@ -33,11 +33,11 @@ const sampleTemplate = {
}; };
describe("templatesClient", () => { describe("templatesClient", () => {
it("list — GET /api/acadenice/templates", async () => { it("list — GET /api/v1/templates", async () => {
vi.mocked(axios.get).mockResolvedValueOnce({ data: [sampleTemplate] }); vi.mocked(axios.get).mockResolvedValueOnce({ data: [sampleTemplate] });
const client = await getClient(); const client = await getClient();
const result = await client.list(); 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); expect(result).toHaveLength(1);
}); });
@ -45,68 +45,68 @@ describe("templatesClient", () => {
vi.mocked(axios.get).mockResolvedValueOnce({ data: [] }); vi.mocked(axios.get).mockResolvedValueOnce({ data: [] });
const client = await getClient(); const client = await getClient();
await client.list({ category: "meeting" }); await client.list({ category: "meeting" });
expect(axios.get).toHaveBeenCalledWith("/api/acadenice/templates", { expect(axios.get).toHaveBeenCalledWith("/api/v1/templates", {
params: { category: "meeting" }, 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 }); vi.mocked(axios.get).mockResolvedValueOnce({ data: sampleTemplate });
const client = await getClient(); const client = await getClient();
const result = await client.get("tmpl-1"); 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"); 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 }); vi.mocked(axios.post).mockResolvedValueOnce({ data: sampleTemplate });
const client = await getClient(); const client = await getClient();
const result = await client.create({ name: "Meeting Note", category: "meeting" }); 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", name: "Meeting Note",
category: "meeting", category: "meeting",
}); });
expect(result.name).toBe("Meeting Note"); 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({ vi.mocked(axios.patch).mockResolvedValueOnce({
data: { ...sampleTemplate, name: "Updated" }, data: { ...sampleTemplate, name: "Updated" },
}); });
const client = await getClient(); const client = await getClient();
const result = await client.update("tmpl-1", { name: "Updated" }); 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", name: "Updated",
}); });
expect(result.name).toBe("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({}); vi.mocked(axios.delete).mockResolvedValueOnce({});
const client = await getClient(); const client = await getClient();
await expect(client.delete("tmpl-1")).resolves.toBeUndefined(); 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" } }); vi.mocked(axios.post).mockResolvedValueOnce({ data: { pageId: "p1", slugId: "slug1" } });
const client = await getClient(); const client = await getClient();
const result = await client.instantiate("tmpl-1", { spaceId: "space-1" }); const result = await client.instantiate("tmpl-1", { spaceId: "space-1" });
expect(axios.post).toHaveBeenCalledWith( expect(axios.post).toHaveBeenCalledWith(
"/api/acadenice/templates/tmpl-1/instantiate", "/api/v1/templates/tmpl-1/instantiate",
{ spaceId: "space-1" }, { spaceId: "space-1" },
); );
expect(result.pageId).toBe("p1"); expect(result.pageId).toBe("p1");
expect(result.slugId).toBe("slug1"); 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({ vi.mocked(axios.patch).mockResolvedValueOnce({
data: { ...sampleTemplate, isWorkspaceDefault: true }, data: { ...sampleTemplate, isWorkspaceDefault: true },
}); });
const client = await getClient(); const client = await getClient();
const result = await client.setDefault("tmpl-1"); 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); expect(result.isWorkspaceDefault).toBe(true);
}); });
@ -122,7 +122,7 @@ describe("templatesClient", () => {
const client = await getClient(); const client = await getClient();
await client.instantiate("tmpl-1", { spaceId: "space-1", parentPageId: "parent-1" }); await client.instantiate("tmpl-1", { spaceId: "space-1", parentPageId: "parent-1" });
expect(axios.post).toHaveBeenCalledWith( expect(axios.post).toHaveBeenCalledWith(
"/api/acadenice/templates/tmpl-1/instantiate", "/api/v1/templates/tmpl-1/instantiate",
{ spaceId: "space-1", parentPageId: "parent-1" }, { spaceId: "space-1", parentPageId: "parent-1" },
); );
}); });

View file

@ -36,7 +36,7 @@ export interface InstantiatePayload {
name?: string; name?: string;
} }
const BASE = '/api/acadenice/templates'; const BASE = '/api/v1/templates';
export const templatesClient = { export const templatesClient = {
list(opts: { category?: string; search?: string } = {}): Promise<TemplateDto[]> { list(opts: { category?: string; search?: string } = {}): Promise<TemplateDto[]> {

View file

@ -42,7 +42,7 @@ export async function sendClip(
apiToken: string, apiToken: string,
payload: ClipPayload, payload: ClipPayload,
): Promise<ClipResult> { ): Promise<ClipResult> {
const url = `${apiUrl.replace(/\/$/, '')}/api/acadenice/clipper/import`; const url = `${apiUrl.replace(/\/$/, '')}/api/v1/clipper/import`;
let response: Response; let response: Response;
try { try {

View file

@ -8,7 +8,7 @@
* Flow: * Flow:
* 1. On open: load settings, query the active tab for page data. * 1. On open: load settings, query the active tab for page data.
* 2. User fills in form and clicks "Clip". * 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. * 4. On success: show result link. On error: show typed error message.
*/ */

View file

@ -43,14 +43,14 @@ describe('sendClip', () => {
mockFetch(201, { pageId: 'p-1', slugId: 's-1', url: '' }); mockFetch(201, { pageId: 'p-1', slugId: 's-1', url: '' });
await sendClip(BASE_URL, TOKEN, samplePayload); await sendClip(BASE_URL, TOKEN, samplePayload);
const [url] = (global.fetch as any).mock.calls[0]; 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 () => { it('strips trailing slash from apiUrl', async () => {
mockFetch(201, { pageId: 'p-1', slugId: 's-1', url: '' }); mockFetch(201, { pageId: 'p-1', slugId: 's-1', url: '' });
await sendClip(`${BASE_URL}/`, TOKEN, samplePayload); await sendClip(`${BASE_URL}/`, TOKEN, samplePayload);
const [url] = (global.fetch as any).mock.calls[0]; 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 () => { it('throws ApiError with statusCode on 401', async () => {

View file

@ -6,9 +6,9 @@ import { AcadeniceApiKeyService } from './services/api-key.service';
* AcadeniceApiKeysModule personal access tokens (R4.5). * AcadeniceApiKeysModule personal access tokens (R4.5).
* *
* Endpoints: * Endpoints:
* GET /api/acadenice/api-keys * GET /api/v1/api-keys
* POST /api/acadenice/api-keys * POST /api/v1/api-keys
* DELETE /api/acadenice/api-keys/:id * DELETE /api/v1/api-keys/:id
* *
* Token format: acdk_<64 hex chars> * Token format: acdk_<64 hex chars>
* Storage: bcrypt hash only plaintext returned once at creation. * Storage: bcrypt hash only plaintext returned once at creation.

View file

@ -25,12 +25,12 @@ import { CreateApiKeySchema } from '../dto/api-key.dto';
/** /**
* AcadeniceApiKeyController personal access tokens (R4.5). * AcadeniceApiKeyController personal access tokens (R4.5).
* *
* GET /api/acadenice/api-keys List caller's tokens (no hashes) * GET /api/v1/api-keys List caller's tokens (no hashes)
* POST /api/acadenice/api-keys Create token returns plain once * POST /api/v1/api-keys Create token returns plain once
* DELETE /api/acadenice/api-keys/:id Revoke * DELETE /api/v1/api-keys/:id Revoke
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/api-keys') @Controller('v1/api-keys')
export class AcadeniceApiKeyController { export class AcadeniceApiKeyController {
constructor(private readonly apiKeyService: AcadeniceApiKeyService) {} constructor(private readonly apiKeyService: AcadeniceApiKeyService) {}

View file

@ -5,7 +5,7 @@ import { AcadeniceAuditLogService } from './services/audit-log.service';
/** /**
* AcadeniceAuditLogModule R4.5. * 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. * Reads directly from the `audit` table via Kysely.
* No EE dependency. * No EE dependency.
*/ */

View file

@ -20,12 +20,12 @@ import { AuditLogQuerySchema } from '../dto/audit-log-query.dto';
/** /**
* AcadeniceAuditLogController R4.5 read-only audit log. * AcadeniceAuditLogController R4.5 read-only audit log.
* *
* GET /api/acadenice/audit-log * GET /api/v1/audit-log
* Auth : JWT (admin or owner only) * Auth : JWT (admin or owner only)
* Query : limit, offset, userId, action, since (ISO), until (ISO) * Query : limit, offset, userId, action, since (ISO), until (ISO)
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/audit-log') @Controller('v1/audit-log')
export class AcadeniceAuditLogController { export class AcadeniceAuditLogController {
constructor(private readonly auditLogService: AcadeniceAuditLogService) {} constructor(private readonly auditLogService: AcadeniceAuditLogService) {}

View file

@ -12,7 +12,7 @@ import { PageContentUpdatedListener } from './events/page-content-updated.listen
* - BacklinkParserService : walks Tiptap JSON, extracts links * - BacklinkParserService : walks Tiptap JSON, extracts links
* - BacklinkIndexerService : delete-then-insert reindex per page * - BacklinkIndexerService : delete-then-insert reindex per page
* - BacklinkService : permission-aware query API * - 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 * - PageContentUpdatedListener: reacts to collaboration saves
* *
* Dependencies: * Dependencies:

View file

@ -15,7 +15,7 @@ import { BacklinkService, BacklinksResult } from '../services/backlink.service';
/** /**
* REST controller for the backlinks feature. * 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 * Authentication: JWT (JwtAuthGuard). The user's read access to each source
* page is enforced inside BacklinkService (space_members / public space check). * 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. * workspace member can query backlinks for pages they can read.
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/pages') @Controller('v1/pages')
export class BacklinksController { export class BacklinksController {
constructor(private readonly backlinkService: BacklinkService) {} constructor(private readonly backlinkService: BacklinkService) {}

View file

@ -49,15 +49,15 @@ const TOKEN_HEADER = 'x-clipper-token';
/** /**
* ClipperController Web Clipper REST surface (R4.3). * 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). * Auth: X-Clipper-Token header (token validated against DB hash).
* Body: ImportClipDto (Zod). * Body: ImportClipDto (Zod).
* *
* POST /api/acadenice/clipper/tokens (JWT auth) * POST /api/v1/clipper/tokens (JWT auth)
* GET /api/acadenice/clipper/tokens (JWT auth) * GET /api/v1/clipper/tokens (JWT auth)
* DELETE /api/acadenice/clipper/tokens/:id (JWT auth) * DELETE /api/v1/clipper/tokens/:id (JWT auth)
*/ */
@Controller('acadenice/clipper') @Controller('v1/clipper')
export class ClipperController { export class ClipperController {
constructor( constructor(
private readonly clipperService: ClipperService, private readonly clipperService: ClipperService,

View file

@ -1,7 +1,7 @@
import { z } from 'zod'; 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 * html_selection is expected to arrive pre-sanitized (DOMPurify on the
* extension side). The server sanitizes again server-side via the existing * 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<typeof ImportClipDto>; export type ImportClipDtoType = z.infer<typeof ImportClipDto>;
/** /**
* 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. * duration: days until expiry, or null for no expiry.
*/ */
export const CreateClipperTokenDto = z.object({ export const CreateClipperTokenDto = z.object({

View file

@ -23,10 +23,10 @@ import { ResolvePageCommentDto } from '../dto/comment.dto';
* requiring an open collab websocket. * requiring an open collab websocket.
* *
* Endpoints: * Endpoints:
* POST /api/acadenice/page-comments/resolve * POST /api/v1/page-comments/resolve
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/page-comments') @Controller('v1/page-comments')
export class PageCommentsController { export class PageCommentsController {
constructor( constructor(
private readonly pageCommentResolveService: PageCommentResolveService, private readonly pageCommentResolveService: PageCommentResolveService,

View file

@ -29,15 +29,15 @@ import {
* user's `acadenice_permissions` JWT claim. * user's `acadenice_permissions` JWT claim.
* *
* Endpoints: * Endpoints:
* POST /api/acadenice/row-comments/list list thread for (tableId, rowId) * POST /api/v1/row-comments/list list thread for (tableId, rowId)
* POST /api/acadenice/row-comments/create create root or reply * POST /api/v1/row-comments/create create root or reply
* POST /api/acadenice/row-comments/update edit own comment * POST /api/v1/row-comments/update edit own comment
* POST /api/acadenice/row-comments/resolve resolve/unresolve root thread * POST /api/v1/row-comments/resolve resolve/unresolve root thread
* POST /api/acadenice/row-comments/delete delete own (or moderate) * POST /api/v1/row-comments/delete delete own (or moderate)
* POST /api/acadenice/row-comments/count count comments for a row * POST /api/v1/row-comments/count count comments for a row
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/row-comments') @Controller('v1/row-comments')
export class RowCommentsController { export class RowCommentsController {
constructor(private readonly rowCommentService: RowCommentService) {} constructor(private readonly rowCommentService: RowCommentService) {}

View file

@ -15,7 +15,7 @@ import { GraphQuerySchema, GraphResponse } from '../dto/graph.dto';
/** /**
* REST controller for the knowledge-graph endpoint (R3.5.1). * 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 * Authentication: JWT (JwtAuthGuard). Permission filtering is applied inside
* GraphService using the same space_members / public visibility model as * 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. * the query param is accepted but ignored to prevent cross-workspace leaks.
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/graph') @Controller('v1/graph')
export class GraphController { export class GraphController {
constructor(private readonly graphService: GraphService) {} constructor(private readonly graphService: GraphService) {}

View file

@ -8,7 +8,7 @@ import { GraphService } from './services/graph.service';
* Provides: * Provides:
* - GraphService : builds { nodes, edges } from acadenice_backlink (R3.2) * - GraphService : builds { nodes, edges } from acadenice_backlink (R3.2)
* with BFS traversal, permission filtering, Redis cache * with BFS traversal, permission filtering, Redis cache
* - GraphController : REST GET /api/acadenice/graph * - GraphController : REST GET /api/v1/graph
* *
* Dependencies: * Dependencies:
* - KyselyDB is global (AppModule). * - KyselyDB is global (AppModule).

View file

@ -28,7 +28,7 @@ import { ZodError } from 'zod';
* the native notification email pipeline. * the native notification email pipeline.
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/notification-preferences') @Controller('v1/notification-preferences')
export class NotificationPreferencesController { export class NotificationPreferencesController {
constructor( constructor(
private readonly prefsService: NotificationPreferencesService, private readonly prefsService: NotificationPreferencesService,

View file

@ -45,19 +45,19 @@ function parseQuery<T>(schema: { parse: (v: unknown) => T }, raw: unknown): T {
* REST controller for Acadenice notification endpoints (R3.7). * REST controller for Acadenice notification endpoints (R3.7).
* *
* This controller is a thin facade over the native Docmost NotificationService. * 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 * frontend to discover and poll them without conflicting with the native
* `/notifications` endpoints used by the upstream Docmost UI. * `/notifications` endpoints used by the upstream Docmost UI.
* *
* All mutation endpoints use the same guards and service as the native path. * All mutation endpoints use the same guards and service as the native path.
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/notifications') @Controller('v1/notifications')
export class AcadeniceNotificationsController { export class AcadeniceNotificationsController {
constructor(private readonly notificationService: NotificationService) {} constructor(private readonly notificationService: NotificationService) {}
/** /**
* GET /api/acadenice/notifications * GET /api/v1/notifications
* Paginated list of notifications for the authenticated user. * Paginated list of notifications for the authenticated user.
*/ */
@Get() @Get()
@ -82,7 +82,7 @@ export class AcadeniceNotificationsController {
} }
/** /**
* GET /api/acadenice/notifications/unread-count * GET /api/v1/notifications/unread-count
*/ */
@Get('unread-count') @Get('unread-count')
async unreadCount(@AuthUser() user: User) { 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') @Post('read-all')
@HttpCode(HttpStatus.NO_CONTENT) @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') @Post('mark-read')
@HttpCode(HttpStatus.NO_CONTENT) @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') @Post(':id/read')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)

View file

@ -27,15 +27,15 @@ import { NotificationModule } from '../../notification/notification.module';
* Shared with native NotificationPref UI (same keys). * Shared with native NotificationPref UI (same keys).
* *
* 4. AcadeniceNotificationsController * 4. AcadeniceNotificationsController
* GET /api/acadenice/notifications (paginated) * GET /api/v1/notifications (paginated)
* GET /api/acadenice/notifications/unread-count * GET /api/v1/notifications/unread-count
* POST /api/acadenice/notifications/read-all * POST /api/v1/notifications/read-all
* POST /api/acadenice/notifications/mark-read * POST /api/v1/notifications/mark-read
* POST /api/acadenice/notifications/:id/read * POST /api/v1/notifications/:id/read
* *
* 5. NotificationPreferencesController * 5. NotificationPreferencesController
* GET /api/acadenice/notification-preferences * GET /api/v1/notification-preferences
* PUT /api/acadenice/notification-preferences * PUT /api/v1/notification-preferences
* *
* Depends on: * Depends on:
* - NotificationModule (exports NotificationService also imports it) * - NotificationModule (exports NotificationService also imports it)

View file

@ -15,7 +15,7 @@ import { AcadeniceRoleService } from '../services/role.service';
const ADMIN_WILDCARD_KEY = 'admin:*'; const ADMIN_WILDCARD_KEY = 'admin:*';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/permissions') @Controller('v1/permissions')
export class AcadenicePermissionsController { export class AcadenicePermissionsController {
constructor(private readonly roleService: AcadeniceRoleService) {} constructor(private readonly roleService: AcadeniceRoleService) {}

View file

@ -26,7 +26,7 @@ import { AcadenicePermissionsGuard } from '../guards/permissions.guard';
import { RequirePermission } from '../guards/require-permission.decorator'; import { RequirePermission } from '../guards/require-permission.decorator';
@UseGuards(JwtAuthGuard, AcadenicePermissionsGuard) @UseGuards(JwtAuthGuard, AcadenicePermissionsGuard)
@Controller('acadenice/roles') @Controller('v1/roles')
export class AcadeniceRolesController { export class AcadeniceRolesController {
constructor(private readonly roleService: AcadeniceRoleService) {} constructor(private readonly roleService: AcadeniceRoleService) {}

View file

@ -34,7 +34,7 @@ import { permissionMatches } from '../permissions-catalog';
* because the access logic depends on `userId` path param vs the actor. * because the access logic depends on `userId` path param vs the actor.
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/users/:userId/roles') @Controller('v1/users/:userId/roles')
export class AcadeniceUserRolesController { export class AcadeniceUserRolesController {
constructor(private readonly roleService: AcadeniceRoleService) {} constructor(private readonly roleService: AcadeniceRoleService) {}

View file

@ -22,12 +22,12 @@ export interface OidcStatusResponse {
/** /**
* AcadeniceOidcStatusController R4.5 read-only OIDC status for admins. * 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) * Auth : JWT (admin or owner only)
* Returns OIDC configuration derived from env vars no secrets exposed. * Returns OIDC configuration derived from env vars no secrets exposed.
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/security') @Controller('v1/security')
export class AcadeniceOidcStatusController { export class AcadeniceOidcStatusController {
constructor(private readonly env: EnvironmentService) {} constructor(private readonly env: EnvironmentService) {}

View file

@ -4,7 +4,7 @@ import { AcadeniceOidcStatusController } from './controllers/oidc-status.control
/** /**
* AcadeniceSecurityModule R4.5. * 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. * Returns OIDC configuration from environment never exposes client_secret.
*/ */
@Module({ @Module({

View file

@ -54,7 +54,7 @@ function parseBody<T>(schema: { parse: (v: unknown) => T }, body: unknown): T {
* default via the seed see permissions-catalog.ts). * default via the seed see permissions-catalog.ts).
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/slash-commands') @Controller('v1/slash-commands')
export class SlashCommandsController { export class SlashCommandsController {
constructor(private readonly slashCommandService: SlashCommandService) {} constructor(private readonly slashCommandService: SlashCommandService) {}

View file

@ -31,14 +31,14 @@ import {
* automatically scoped to the authenticated workspace. * automatically scoped to the authenticated workspace.
* *
* Routes: * Routes:
* POST /api/acadenice/sync-blocks create master block * POST /api/v1/sync-blocks create master block
* GET /api/acadenice/sync-blocks/:id read content * GET /api/v1/sync-blocks/:id read content
* PATCH /api/acadenice/sync-blocks/:id update content * PATCH /api/v1/sync-blocks/:id update content
* DELETE /api/acadenice/sync-blocks/:id delete master * DELETE /api/v1/sync-blocks/:id delete master
* GET /api/acadenice/sync-blocks/:id/usages list referencing pages * GET /api/v1/sync-blocks/:id/usages list referencing pages
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/sync-blocks') @Controller('v1/sync-blocks')
export class SyncBlocksController { export class SyncBlocksController {
constructor(private readonly syncBlocksService: SyncBlocksService) {} constructor(private readonly syncBlocksService: SyncBlocksService) {}

View file

@ -56,16 +56,16 @@ function parseBody<T>(schema: { parse: (v: unknown) => T }, body: unknown): T {
* REST controller for workspace page templates (R3.6). * REST controller for workspace page templates (R3.6).
* *
* Permission matrix: * Permission matrix:
* GET /acadenice/templates templates:read * GET /v1/templates templates:read
* GET /acadenice/templates/:id templates:read * GET /v1/templates/:id templates:read
* POST /acadenice/templates templates:create * POST /v1/templates templates:create
* PATCH /acadenice/templates/:id owner-or-manage (service enforces) * PATCH /v1/templates/:id owner-or-manage (service enforces)
* DELETE /acadenice/templates/:id owner-or-manage (service enforces) * DELETE /v1/templates/:id owner-or-manage (service enforces)
* POST /acadenice/templates/:id/instantiate templates:read * POST /v1/templates/:id/instantiate templates:read
* PATCH /acadenice/templates/:id/default templates:manage * PATCH /v1/templates/:id/default templates:manage
*/ */
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('acadenice/templates') @Controller('v1/templates')
export class TemplatesController { export class TemplatesController {
constructor( constructor(
private readonly templateService: TemplateService, private readonly templateService: TemplateService,

View file

@ -15,7 +15,7 @@ import { Kysely, sql } from 'kysely';
* *
* UNIQUE(workspace_id, keyword) prevents collision within the same workspace. * UNIQUE(workspace_id, keyword) prevents collision within the same workspace.
* Both systems and custom commands share the slash menu the runtime fetches * 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. * Idempotent: ifNotExists on every CREATE so migration re-runs never fail.
*/ */