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:
parent
3af579498b
commit
9dd283ced6
48 changed files with 164 additions and 164 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,17 +6,17 @@ import {
|
|||
} from "../types/api-key.types";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export async function createAcadeniceApiKey(
|
||||
data: CreateAcadeniceApiKeyRequest,
|
||||
): Promise<CreateAcadeniceApiKeyResponse> {
|
||||
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<void> {
|
||||
await api.delete(`/acadenice/api-keys/${id}`);
|
||||
await api.delete(`/v1/api-keys/${id}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@ import {
|
|||
export async function getAcadeniceAuditLogs(
|
||||
params: AcadeniceAuditLogQuery = {},
|
||||
): Promise<AcadeniceAuditLogPage> {
|
||||
const resp = await api.get("/acadenice/audit-log", { params });
|
||||
const resp = await api.get("/v1/audit-log", { params });
|
||||
return resp.data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import axios from 'axios';
|
||||
|
||||
const BASE = '/api/acadenice/clipper';
|
||||
const BASE = '/api/v1/clipper';
|
||||
|
||||
export interface ClipperTokenInfo {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export async function listRowComments(
|
|||
params: ListRowCommentsParams,
|
||||
): Promise<RowComment[]> {
|
||||
const res = await api.post<RowComment[]>(
|
||||
"/acadenice/row-comments/list",
|
||||
"/v1/row-comments/list",
|
||||
params,
|
||||
);
|
||||
return res.data;
|
||||
|
|
@ -44,7 +44,7 @@ export async function createRowComment(
|
|||
params: CreateRowCommentParams,
|
||||
): Promise<RowComment> {
|
||||
const res = await api.post<RowComment>(
|
||||
"/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<RowComment> {
|
||||
const res = await api.post<RowComment>("/acadenice/row-comments/update", {
|
||||
const res = await api.post<RowComment>("/v1/row-comments/update", {
|
||||
commentId,
|
||||
content,
|
||||
});
|
||||
|
|
@ -65,7 +65,7 @@ export async function resolveRowComment(
|
|||
commentId: string,
|
||||
resolved: boolean,
|
||||
): Promise<RowComment> {
|
||||
const res = await api.post<RowComment>("/acadenice/row-comments/resolve", {
|
||||
const res = await api.post<RowComment>("/v1/row-comments/resolve", {
|
||||
commentId,
|
||||
resolved,
|
||||
});
|
||||
|
|
@ -73,7 +73,7 @@ export async function resolveRowComment(
|
|||
}
|
||||
|
||||
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(
|
||||
|
|
@ -81,7 +81,7 @@ export async function countRowComments(
|
|||
rowId: string,
|
||||
): Promise<number> {
|
||||
const res = await api.post<{ count: number }>(
|
||||
"/acadenice/row-comments/count",
|
||||
"/v1/row-comments/count",
|
||||
{ tableId, rowId },
|
||||
);
|
||||
return res.data.count;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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<GraphResponse>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ export interface NotificationPreferences {
|
|||
|
||||
export type UpdatePreferencesPayload = Partial<NotificationPreferences>;
|
||||
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ import api from "@/lib/api-client";
|
|||
import { OidcStatusResponse } from "../types/oidc-status.types";
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<IPermissionDescriptor[]> {
|
||||
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<IPermissionDescriptor[]>
|
|||
*/
|
||||
export async function getMyPermissions(): Promise<IMyPermissionsResponse> {
|
||||
return api.get(
|
||||
"/acadenice/permissions/me",
|
||||
"/v1/permissions/me",
|
||||
) as unknown as Promise<IMyPermissionsResponse>;
|
||||
}
|
||||
|
||||
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> {
|
||||
return api.get(
|
||||
`/acadenice/roles/${roleId}`,
|
||||
`/v1/roles/${roleId}`,
|
||||
) as unknown as Promise<IRoleWithPermissions>;
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ export async function createRole(
|
|||
payload: ICreateRolePayload,
|
||||
): Promise<IRoleWithPermissions> {
|
||||
return api.post(
|
||||
"/acadenice/roles",
|
||||
"/v1/roles",
|
||||
payload,
|
||||
) as unknown as Promise<IRoleWithPermissions>;
|
||||
}
|
||||
|
|
@ -57,20 +57,20 @@ export async function updateRole(
|
|||
payload: IUpdateRolePayload,
|
||||
): Promise<IRole> {
|
||||
return api.patch(
|
||||
`/acadenice/roles/${roleId}`,
|
||||
`/v1/roles/${roleId}`,
|
||||
payload,
|
||||
) as unknown as Promise<IRole>;
|
||||
}
|
||||
|
||||
export async function deleteRole(roleId: string): Promise<void> {
|
||||
await api.delete(`/acadenice/roles/${roleId}`);
|
||||
await api.delete(`/v1/roles/${roleId}`);
|
||||
}
|
||||
|
||||
export async function setRolePermissions(
|
||||
roleId: string,
|
||||
permissions: string[],
|
||||
): Promise<IRoleWithPermissions> {
|
||||
return api.put(`/acadenice/roles/${roleId}/permissions`, {
|
||||
return api.put(`/v1/roles/${roleId}/permissions`, {
|
||||
permissions,
|
||||
}) as unknown as Promise<IRoleWithPermissions>;
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ export async function setRolePermissions(
|
|||
export async function listUserRoles(
|
||||
userId: string,
|
||||
): 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[]
|
||||
>;
|
||||
}
|
||||
|
|
@ -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<void> {
|
||||
await api.delete(`/acadenice/users/${userId}/roles/${roleId}`);
|
||||
await api.delete(`/v1/users/${userId}/roles/${roleId}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export interface CreateSlashCommandPayload {
|
|||
|
||||
export type UpdateSlashCommandPayload = Partial<CreateSlashCommandPayload>;
|
||||
|
||||
const BASE = '/api/acadenice/slash-commands';
|
||||
const BASE = '/api/v1/slash-commands';
|
||||
|
||||
export const slashCommandsClient = {
|
||||
list(): Promise<SlashCommandDto[]> {
|
||||
|
|
|
|||
|
|
@ -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<typeof setTimeout> | null = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> = {}): Promise<SyncBlockDto> {
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<TemplateDto[]> {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export async function sendClip(
|
|||
apiToken: string,
|
||||
payload: ClipPayload,
|
||||
): Promise<ClipResult> {
|
||||
const url = `${apiUrl.replace(/\/$/, '')}/api/acadenice/clipper/import`;
|
||||
const url = `${apiUrl.replace(/\/$/, '')}/api/v1/clipper/import`;
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<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.
|
||||
*/
|
||||
export const CreateClipperTokenDto = z.object({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -45,19 +45,19 @@ function parseQuery<T>(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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ function parseBody<T>(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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,16 +56,16 @@ function parseBody<T>(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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue