summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-07-17 23:42:55 +0200
committerJoel Klinghed <the_jk@spawned.biz>2025-07-17 23:44:11 +0200
commitbef3da2a567e3804e12355d9c3d5c09439dbe2ea (patch)
treeab7974c941bd31994da46150234976b33c2f61b5
parent145be2b3c92e254904d4040850e3c1e9b6a66f32 (diff)
Humble beginnings
Redirect to login if not logged in, on login session cookie is set and projects or reviews are listed.
-rw-r--r--client/.dir-locals.el5
-rw-r--r--client/.prettierrc26
-rw-r--r--client/api/schema.d.ts1422
-rw-r--r--client/eslint.config.js42
-rw-r--r--client/package-lock.json28
-rw-r--r--client/package.json82
-rw-r--r--client/src/app.d.ts14
-rw-r--r--client/src/app.html18
-rw-r--r--client/src/hooks.server.ts29
-rw-r--r--client/src/lib/ListPrevNext.svelte10
-rw-r--r--client/src/lib/api/schema.d.ts2608
-rw-r--r--client/src/lib/config.ts54
-rw-r--r--client/src/lib/fetch-client.ts10
-rw-r--r--client/src/routes/(app)/+error.svelte5
-rw-r--r--client/src/routes/(app)/+layout.svelte12
-rw-r--r--client/src/routes/(app)/+layout.ts6
-rw-r--r--client/src/routes/(app)/+page.svelte25
-rw-r--r--client/src/routes/(app)/+page.ts38
-rw-r--r--client/src/routes/+page.svelte2
-rw-r--r--client/src/routes/login/+page.server.ts36
-rw-r--r--client/src/routes/login/+page.svelte22
-rw-r--r--client/src/routes/login/+page.ts7
-rw-r--r--client/svelte.config.js20
-rw-r--r--client/tsconfig.json37
-rw-r--r--client/vite.config.ts7
-rw-r--r--server/Cargo.lock28
-rw-r--r--server/Cargo.toml1
-rw-r--r--server/src/auth.rs2
-rw-r--r--server/src/main.rs11
29 files changed, 1765 insertions, 2842 deletions
diff --git a/client/.dir-locals.el b/client/.dir-locals.el
new file mode 100644
index 0000000..43de649
--- /dev/null
+++ b/client/.dir-locals.el
@@ -0,0 +1,5 @@
+;;; Directory Local Variables -*- no-byte-compile: t; -*-
+;;; For more information see (info "(emacs) Directory Variables")
+
+((javascript-mode . ((js-indent-level . 2)))
+ (typescript-mode . ((typescript-indent-level . 2))))
diff --git a/client/.prettierrc b/client/.prettierrc
index 3f7802c..c84b786 100644
--- a/client/.prettierrc
+++ b/client/.prettierrc
@@ -1,15 +1,15 @@
{
- "useTabs": true,
- "singleQuote": true,
- "trailingComma": "none",
- "printWidth": 100,
- "plugins": ["prettier-plugin-svelte"],
- "overrides": [
- {
- "files": "*.svelte",
- "options": {
- "parser": "svelte"
- }
- }
- ]
+ "useTabs": false,
+ "singleQuote": true,
+ "trailingComma": "none",
+ "printWidth": 100,
+ "plugins": ["prettier-plugin-svelte"],
+ "overrides": [
+ {
+ "files": "*.svelte",
+ "options": {
+ "parser": "svelte"
+ }
+ }
+ ]
}
diff --git a/client/api/schema.d.ts b/client/api/schema.d.ts
deleted file mode 100644
index f69f227..0000000
--- a/client/api/schema.d.ts
+++ /dev/null
@@ -1,1422 +0,0 @@
-/**
- * This file was auto-generated by openapi-typescript.
- * Do not make direct changes to the file.
- */
-
-export interface paths {
- "/healthcheck": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["healthcheck"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/login": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["login"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/logout": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["logout"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/project/{projectid}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["project"];
- put?: never;
- post: operations["project_update"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/project/{projectid}/new": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["project_new"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/project/{projectid}/reviews": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["reviews"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/project/{projectid}/translations": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["translation_reviews"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/project/{projectid}/user/{userid}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["project_user_update"];
- delete: operations["project_user_del"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/project/{projectid}/user/{userid}/new": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["project_user_add"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/projects": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["projects"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/review/{projectid}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["review_id"];
- put?: never;
- post?: never;
- delete: operations["review_id_del"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/review/{projectid}/{branch}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["review"];
- put?: never;
- post?: never;
- delete: operations["review_del"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/status": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["status"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/translation/{projectid}/new": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["translation_review_new"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/translation/{translation_reviewid}/strings": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["translation_review_strings"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/user/keys": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["user_keys"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/user/keys/add": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["user_key_add"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/user/keys/{id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["user_key_get"];
- put?: never;
- post?: never;
- delete: operations["user_key_del"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["users"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
-}
-export type webhooks = Record<string, never>;
-export interface components {
- schemas: {
- LocalizationPlaceholder: {
- /** @example %1$d */
- content: string;
- /** @example 42 */
- example: string;
- /** @example NAME */
- id: string;
- };
- LocalizationString: {
- /** @example Generic greating */
- description: string;
- /** @example strings/strings.grd */
- file: string;
- /** @example IDS_GENERIC_WELCOME */
- id: string;
- /** @example This should be a positive greating */
- meaning: string;
- placeholder_offset: number[];
- placeholders: components["schemas"]["LocalizationPlaceholder"][];
- /** @example Hello! */
- source: string;
- translations: components["schemas"]["TranslationString"][];
- };
- LocalizationStrings: {
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example true */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- strings: components["schemas"]["LocalizationString"][];
- /**
- * Format: int32
- * @example 42
- */
- total_count: number;
- };
- Login: {
- password: string;
- username: string;
- };
- Project: {
- /** @example Example project */
- description: string;
- /** @example fake */
- id: string;
- /** @example main */
- main_branch: string;
- /** @example ssh://git.example.org/srv/git/ */
- remote: string;
- /** @example b3BlbNNz...AQIDBA== */
- remote_key_abbrev: string;
- /** @example FAKE: Features All Kids Erase */
- title: string;
- users: components["schemas"]["ProjectUserEntry"][];
- };
- ProjectData: {
- /** @example Example project */
- description?: string | null;
- /** @example main */
- main_branch?: string | null;
- /** @example ssh://git.example.org/srv/git/ */
- remote?: string | null;
- /** @example b3BlbNNz...AQIDBA== */
- remote_key?: string | null;
- /** @example FAKE: Features All Kids Erase */
- title?: string | null;
- };
- ProjectEntry: {
- /** @example fake */
- id: string;
- /** @example FAKE: Features All Kids Erase */
- title: string;
- };
- ProjectUserEntry: {
- default_role: components["schemas"]["UserReviewRole"];
- /** @example false */
- maintainer: boolean;
- user: components["schemas"]["User"];
- };
- ProjectUserEntryData: {
- default_role?: null | components["schemas"]["UserReviewRole"];
- /** @example false */
- maintainer?: boolean | null;
- };
- Projects: {
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example false */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- projects: components["schemas"]["ProjectEntry"][];
- /**
- * Format: int32
- * @example 1
- */
- total_count: number;
- };
- Review: {
- /** @example false */
- archived: boolean;
- /** @example r/user/TASK-123456 */
- branch: string;
- /** @example We're adding more features because features are what we want. */
- description: string;
- /**
- * Format: int64
- * @example 1000
- */
- id: number;
- owner: components["schemas"]["User"];
- /**
- * Format: float
- * @example 37.5
- */
- progress: number;
- state: components["schemas"]["ReviewState"];
- /** @example FAKE-512: Add more features */
- title: string;
- users: components["schemas"]["ReviewUserEntry"][];
- };
- ReviewEntry: {
- /** @example r/user/TASK-123456 */
- branch: string;
- /**
- * Format: int64
- * @example 1000
- */
- id: number;
- owner: components["schemas"]["User"];
- /**
- * Format: float
- * @example 37.5
- */
- progress: number;
- state: components["schemas"]["ReviewState"];
- /** @example FAKE-512: Add more features */
- title: string;
- };
- /** @enum {string} */
- ReviewState: "Draft" | "Open" | "Dropped" | "Closed";
- ReviewUserEntry: {
- role: components["schemas"]["UserReviewRole"];
- user: components["schemas"]["User"];
- };
- Reviews: {
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example true */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- reviews: components["schemas"]["ReviewEntry"][];
- /**
- * Format: int32
- * @example 42
- */
- total_count: number;
- };
- StatusResponse: {
- ok: boolean;
- };
- TranslationReview: {
- /** @example false */
- archived: boolean;
- /** @example d7c502b9c6b833060576a0c4da0287933d603011 */
- base: string;
- /** @example New translations */
- description: string;
- /** @example 2cecdec660a30bf3964cee645d9cee03640ef8dc */
- head: string;
- /**
- * Format: int64
- * @example 1
- */
- id: number;
- owner: components["schemas"]["User"];
- /**
- * Format: float
- * @example 37.5
- */
- progress: number;
- state: components["schemas"]["ReviewState"];
- /** @example FAKE-512: Update translations */
- title: string;
- users: components["schemas"]["ReviewUserEntry"][];
- };
- TranslationReviewData: {
- /** @example d7c502b9c6b833060576a0c4da0287933d603011 */
- base?: string | null;
- /** @example New translations */
- description: string;
- /** @example FAKE-512: Update translations */
- title: string;
- };
- TranslationReviewEntry: {
- /** @example d7c502b9c6b833060576a0c4da0287933d603011 */
- base: string;
- /** @example 2cecdec660a30bf3964cee645d9cee03640ef8dc */
- head: string;
- /**
- * Format: int64
- * @example 1
- */
- id: number;
- owner: components["schemas"]["User"];
- /**
- * Format: float
- * @example 37.5
- */
- progress: number;
- state: components["schemas"]["ReviewState"];
- /** @example FAKE-512: Update translations */
- title: string;
- };
- TranslationReviews: {
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example true */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- reviews: components["schemas"]["TranslationReviewEntry"][];
- /**
- * Format: int32
- * @example 42
- */
- total_count: number;
- };
- /** @enum {string} */
- TranslationState: "Unreviewed" | "Unchanged" | "Approved" | "Revert" | "Fix";
- TranslationString: {
- comment: string;
- /** @example sv */
- language: string;
- placeholder_offset: number[];
- reviewer?: null | components["schemas"]["User"];
- state: components["schemas"]["TranslationState"];
- /** @example Hej! */
- translation: string;
- };
- User: {
- /** @example true */
- active: boolean;
- /** @example jsmith */
- id: string;
- /** @example John Smith */
- name: string;
- };
- UserKey: {
- /** @example user@host 1970-01-01 */
- comment: string;
- /** @example AAAAfoobar== */
- data: string;
- /**
- * Format: int64
- * @example 1
- */
- id: number;
- /** @example ssh-rsa */
- kind: string;
- };
- UserKeyData: {
- /** @example user@host 1970-01-01 */
- comment?: string | null;
- /** @example AAAAfoobar== */
- data: string;
- /** @example ssh-rsa */
- kind: string;
- };
- UserKeys: {
- keys: components["schemas"]["UserKey"][];
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example false */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- /**
- * Format: int32
- * @example 2
- */
- total_count: number;
- };
- /** @enum {string} */
- UserReviewRole: "Reviewer" | "Watcher" | "None";
- Users: {
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example true */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- /**
- * Format: int32
- * @example 42
- */
- total_count: number;
- users: components["schemas"]["User"][];
- };
- };
- responses: never;
- parameters: never;
- requestBodies: never;
- headers: never;
- pathItems: never;
-}
-export type $defs = Record<string, never>;
-export interface operations {
- healthcheck: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description All good */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- login: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/x-www-form-urlencoded": components["schemas"]["Login"];
- };
- };
- responses: {
- /** @description Login successful */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- /** @example {
- * "ok": true
- * } */
- "application/json": components["schemas"]["StatusResponse"];
- };
- };
- /** @description Login failed */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- /** @example {
- * "error": "Unauthorized",
- * "ok": false
- * } */
- "application/json": components["schemas"]["StatusResponse"];
- };
- };
- };
- };
- logout: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Logout successful */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- /** @example {
- * "ok": true
- * } */
- "application/json": components["schemas"]["StatusResponse"];
- };
- };
- };
- };
- project: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Project"];
- };
- };
- /** @description No such project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- project_update: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["ProjectData"];
- };
- };
- responses: {
- /** @description Project updated */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- project_new: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["ProjectData"];
- };
- };
- responses: {
- /** @description Project created */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Project"];
- };
- };
- /** @description Project with id already exists */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- reviews: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get all reviews for project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Reviews"];
- };
- };
- /** @description No such project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- translation_reviews: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get all translation reviews for project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["TranslationReviews"];
- };
- };
- /** @description No such project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- project_user_update: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- userid: string;
- };
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["ProjectUserEntryData"];
- };
- };
- responses: {
- /** @description User updated in project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such project, no such user or user not in project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- project_user_del: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- userid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description User removed from project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such project, no such user or user not in project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- project_user_add: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- userid: string;
- };
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["ProjectUserEntryData"];
- };
- };
- responses: {
- /** @description User added to project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description User already in project */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- projects: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get all projects */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Projects"];
- };
- };
- };
- };
- review_id: {
- parameters: {
- query: {
- reviewid: number;
- };
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get review */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Review"];
- };
- };
- /** @description No such review */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- review_id_del: {
- parameters: {
- query: {
- reviewid: number;
- };
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Remove deleted */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Review is open or closed */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not owner of review or maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such review */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- review: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- branch: string;
- projectid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get review */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Review"];
- };
- };
- /** @description No such review */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- review_del: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- branch: string;
- projectid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Review deleted */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Review is open or closed */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not owner of review or maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such review */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- status: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Current status */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- /** @example {
- * "ok": true
- * } */
- "application/json": components["schemas"]["StatusResponse"];
- };
- };
- /** @description Not authorized */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- /** @example {
- * "error": "Unauthorized",
- * "ok": false
- * } */
- "application/json": components["schemas"]["StatusResponse"];
- };
- };
- };
- };
- translation_review_new: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["TranslationReviewData"];
- };
- };
- responses: {
- /** @description Translation review created */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["TranslationReview"];
- };
- };
- };
- };
- translation_review_strings: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path: {
- translation_reviewid: number;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get all strings for a translation review */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["LocalizationStrings"];
- };
- };
- /** @description No such translation review */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- user_keys: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get all keys for user */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["UserKeys"];
- };
- };
- };
- };
- user_key_add: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["UserKeyData"];
- };
- };
- responses: {
- /** @description Key added to current user */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["UserKey"];
- };
- };
- /** @description Key too large or invalid */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- user_key_get: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- id: number;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description User key */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["UserKey"];
- };
- };
- /** @description No such key */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- user_key_del: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- id: number;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Key removed from current user */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such key for current user */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
- users: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get all users */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Users"];
- };
- };
- };
- };
-}
diff --git a/client/eslint.config.js b/client/eslint.config.js
index 3a30bfc..157fdce 100644
--- a/client/eslint.config.js
+++ b/client/eslint.config.js
@@ -10,27 +10,27 @@ import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
- includeIgnoreFile(gitignorePath),
- js.configs.recommended,
- ...ts.configs.recommended,
- ...svelte.configs.recommended,
- prettier,
- ...svelte.configs.prettier,
- {
- languageOptions: {
- globals: { ...globals.browser, ...globals.node }
- },
- rules: { 'no-undef': 'off' }
+ includeIgnoreFile(gitignorePath),
+ js.configs.recommended,
+ ...ts.configs.recommended,
+ ...svelte.configs.recommended,
+ prettier,
+ ...svelte.configs.prettier,
+ {
+ languageOptions: {
+ globals: { ...globals.browser, ...globals.node }
},
- {
- files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
- languageOptions: {
- parserOptions: {
- projectService: true,
- extraFileExtensions: ['.svelte'],
- parser: ts.parser,
- svelteConfig
- }
- }
+ rules: { 'no-undef': 'off' }
+ },
+ {
+ files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
+ languageOptions: {
+ parserOptions: {
+ projectService: true,
+ extraFileExtensions: ['.svelte'],
+ parser: ts.parser,
+ svelteConfig
+ }
}
+ }
);
diff --git a/client/package-lock.json b/client/package-lock.json
index 513ceb9..2c79eb4 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -7,6 +7,10 @@
"": {
"name": "client",
"version": "0.0.1",
+ "dependencies": {
+ "openapi-fetch": "^0.14.0",
+ "superstruct": "^2.0.2"
+ },
"devDependencies": {
"@eslint/compat": "^1.3.1",
"@eslint/js": "^9.30.1",
@@ -2709,6 +2713,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/openapi-fetch": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.14.0.tgz",
+ "integrity": "sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==",
+ "license": "MIT",
+ "dependencies": {
+ "openapi-typescript-helpers": "^0.0.15"
+ }
+ },
"node_modules/openapi-typescript": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.8.0.tgz",
@@ -2730,6 +2743,12 @@
"typescript": "^5.x"
}
},
+ "node_modules/openapi-typescript-helpers": {
+ "version": "0.0.15",
+ "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz",
+ "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==",
+ "license": "MIT"
+ },
"node_modules/openapi-typescript/node_modules/supports-color": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.0.0.tgz",
@@ -3282,6 +3301,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/superstruct": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz",
+ "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
diff --git a/client/package.json b/client/package.json
index b9f228b..4b3110a 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,42 +1,46 @@
{
- "name": "client",
- "private": true,
- "version": "0.0.1",
- "type": "module",
- "scripts": {
- "dev": "vite dev",
- "build": "vite build",
- "preview": "vite preview",
- "prepare": "svelte-kit sync || echo ''",
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
- "format": "prettier --write .",
- "lint": "prettier --check . && eslint .",
- "openapi": "openapi-typescript http://127.0.0.1:8000/openapi/openapi.json -o src/lib/api/schema.d.ts"
- },
- "devDependencies": {
- "@eslint/compat": "^1.3.1",
- "@eslint/js": "^9.30.1",
- "@sveltejs/adapter-static": "^3.0.8",
- "@sveltejs/kit": "^2.22.2",
- "@sveltejs/vite-plugin-svelte": "^5.0.0",
- "eslint": "^9.30.1",
- "eslint-config-prettier": "^10.0.1",
- "eslint-plugin-svelte": "^3.10.1",
- "globals": "^16.3.0",
- "openapi-typescript": "^7.8.0",
- "prettier": "^3.6.2",
- "prettier-plugin-svelte": "^3.3.3",
- "svelte": "^5.35.0",
- "svelte-check": "^4.0.0",
- "typescript": "^5.8.3",
- "typescript-eslint": "^8.35.1",
- "vite": "^6.2.6",
- "vite-plugin-devtools-json": "^0.2.1"
- },
- "overrides": {
- "@sveltejs/kit": {
- "cookie": "^0.7.0"
- }
+ "name": "client",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "prepare": "svelte-kit sync || echo ''",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "format": "prettier --write .",
+ "lint": "prettier --check . && eslint .",
+ "openapi": "openapi-typescript http://127.0.0.1:8000/openapi/openapi.json -o src/lib/api/schema.d.ts"
+ },
+ "devDependencies": {
+ "@eslint/compat": "^1.3.1",
+ "@eslint/js": "^9.30.1",
+ "@sveltejs/adapter-static": "^3.0.8",
+ "@sveltejs/kit": "^2.22.2",
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
+ "eslint": "^9.30.1",
+ "eslint-config-prettier": "^10.0.1",
+ "eslint-plugin-svelte": "^3.10.1",
+ "globals": "^16.3.0",
+ "openapi-typescript": "^7.8.0",
+ "prettier": "^3.6.2",
+ "prettier-plugin-svelte": "^3.3.3",
+ "svelte": "^5.35.0",
+ "svelte-check": "^4.0.0",
+ "typescript": "^5.8.3",
+ "typescript-eslint": "^8.35.1",
+ "vite": "^6.2.6",
+ "vite-plugin-devtools-json": "^0.2.1"
+ },
+ "overrides": {
+ "@sveltejs/kit": {
+ "cookie": "^0.7.0"
}
+ },
+ "dependencies": {
+ "openapi-fetch": "^0.14.0",
+ "superstruct": "^2.0.2"
+ }
}
diff --git a/client/src/app.d.ts b/client/src/app.d.ts
index d76242a..520c421 100644
--- a/client/src/app.d.ts
+++ b/client/src/app.d.ts
@@ -1,13 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
- namespace App {
- // interface Error {}
- // interface Locals {}
- // interface PageData {}
- // interface PageState {}
- // interface Platform {}
- }
+ namespace App {
+ // interface Error {}
+ // interface Locals {}
+ // interface PageData {}
+ // interface PageState {}
+ // interface Platform {}
+ }
}
export {};
diff --git a/client/src/app.html b/client/src/app.html
index ecd5efc..84ffad1 100644
--- a/client/src/app.html
+++ b/client/src/app.html
@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en">
- <head>
- <meta charset="utf-8" />
- <link rel="icon" href="%sveltekit.assets%/favicon.png" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- %sveltekit.head%
- </head>
- <body data-sveltekit-preload-data="hover">
- <div style="display: contents">%sveltekit.body%</div>
- </body>
+ <head>
+ <meta charset="utf-8" />
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ %sveltekit.head%
+ </head>
+ <body data-sveltekit-preload-data="hover">
+ <div style="display: contents">%sveltekit.body%</div>
+ </body>
</html>
diff --git a/client/src/hooks.server.ts b/client/src/hooks.server.ts
new file mode 100644
index 0000000..4a7a5cb
--- /dev/null
+++ b/client/src/hooks.server.ts
@@ -0,0 +1,29 @@
+import { base } from '$app/paths';
+import { redirect, type Handle, type HandleFetch } from '@sveltejs/kit';
+
+export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
+ return fetch(request).then((resp) => {
+ if (resp.status == 401) {
+ const url = new URL(event.request.url);
+ const ret = url.pathname + url.search;
+ redirect(307, base + '/login?return=' + encodeURIComponent(ret));
+ }
+ return resp;
+ });
+};
+
+export const handle: Handle = async ({ event, resolve }) => {
+ const response = await resolve(event, {
+ filterSerializedResponseHeaders: (name) => {
+ switch (name) {
+ // used by openapi-fetch
+ case 'content-length':
+ return true;
+ default:
+ return false;
+ }
+ }
+ });
+
+ return response;
+};
diff --git a/client/src/lib/ListPrevNext.svelte b/client/src/lib/ListPrevNext.svelte
new file mode 100644
index 0000000..89a5c14
--- /dev/null
+++ b/client/src/lib/ListPrevNext.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+ let { list, query_offset = 'offset' } = $props();
+</script>
+
+{#if list.offset > 0}
+ <a href="?{query_offset}={Math.max(0, list.offset - list.limit)}">Previous</a>
+{/if}
+{#if list.more}
+ <a href="?{query_offset}={list.offset + list.limit}">Next</a>
+{/if}
diff --git a/client/src/lib/api/schema.d.ts b/client/src/lib/api/schema.d.ts
index f69f227..ef99389 100644
--- a/client/src/lib/api/schema.d.ts
+++ b/client/src/lib/api/schema.d.ts
@@ -4,1419 +4,1419 @@
*/
export interface paths {
- "/healthcheck": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["healthcheck"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ '/healthcheck': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/login": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["login"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['healthcheck'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/login': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/logout": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["logout"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get?: never;
+ put?: never;
+ post: operations['login'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/logout': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/project/{projectid}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["project"];
- put?: never;
- post: operations["project_update"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['logout'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/project/{projectid}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/project/{projectid}/new": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["project_new"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['project'];
+ put?: never;
+ post: operations['project_update'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/project/{projectid}/new': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/project/{projectid}/reviews": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["reviews"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get?: never;
+ put?: never;
+ post: operations['project_new'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/project/{projectid}/reviews': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/project/{projectid}/translations": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["translation_reviews"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['reviews'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/project/{projectid}/translations': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/project/{projectid}/user/{userid}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["project_user_update"];
- delete: operations["project_user_del"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['translation_reviews'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/project/{projectid}/user/{userid}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/project/{projectid}/user/{userid}/new": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["project_user_add"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get?: never;
+ put?: never;
+ post: operations['project_user_update'];
+ delete: operations['project_user_del'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/project/{projectid}/user/{userid}/new': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/projects": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["projects"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get?: never;
+ put?: never;
+ post: operations['project_user_add'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/projects': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/review/{projectid}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["review_id"];
- put?: never;
- post?: never;
- delete: operations["review_id_del"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['projects'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/review/{projectid}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/review/{projectid}/{branch}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["review"];
- put?: never;
- post?: never;
- delete: operations["review_del"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['review_id'];
+ put?: never;
+ post?: never;
+ delete: operations['review_id_del'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/review/{projectid}/{branch}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/status": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["status"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['review'];
+ put?: never;
+ post?: never;
+ delete: operations['review_del'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/status': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/translation/{projectid}/new": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["translation_review_new"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['status'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/translation/{projectid}/new': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/translation/{translation_reviewid}/strings": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["translation_review_strings"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get?: never;
+ put?: never;
+ post: operations['translation_review_new'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/translation/{translation_reviewid}/strings': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/user/keys": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["user_keys"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['translation_review_strings'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/user/keys': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/user/keys/add": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["user_key_add"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['user_keys'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/user/keys/add': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/user/keys/{id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["user_key_get"];
- put?: never;
- post?: never;
- delete: operations["user_key_del"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get?: never;
+ put?: never;
+ post: operations['user_key_add'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/user/keys/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- "/users": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["users"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
+ get: operations['user_key_get'];
+ put?: never;
+ post?: never;
+ delete: operations['user_key_del'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/users': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
+ get: operations['users'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
}
export type webhooks = Record<string, never>;
export interface components {
- schemas: {
- LocalizationPlaceholder: {
- /** @example %1$d */
- content: string;
- /** @example 42 */
- example: string;
- /** @example NAME */
- id: string;
+ schemas: {
+ LocalizationPlaceholder: {
+ /** @example %1$d */
+ content: string;
+ /** @example 42 */
+ example: string;
+ /** @example NAME */
+ id: string;
+ };
+ LocalizationString: {
+ /** @example Generic greating */
+ description: string;
+ /** @example strings/strings.grd */
+ file: string;
+ /** @example IDS_GENERIC_WELCOME */
+ id: string;
+ /** @example This should be a positive greating */
+ meaning: string;
+ placeholder_offset: number[];
+ placeholders: components['schemas']['LocalizationPlaceholder'][];
+ /** @example Hello! */
+ source: string;
+ translations: components['schemas']['TranslationString'][];
+ };
+ LocalizationStrings: {
+ /**
+ * Format: int32
+ * @example 10
+ */
+ limit: number;
+ /** @example true */
+ more: boolean;
+ /**
+ * Format: int32
+ * @example 0
+ */
+ offset: number;
+ strings: components['schemas']['LocalizationString'][];
+ /**
+ * Format: int32
+ * @example 42
+ */
+ total_count: number;
+ };
+ Login: {
+ password: string;
+ username: string;
+ };
+ Project: {
+ /** @example Example project */
+ description: string;
+ /** @example fake */
+ id: string;
+ /** @example main */
+ main_branch: string;
+ /** @example ssh://git.example.org/srv/git/ */
+ remote: string;
+ /** @example b3BlbNNz...AQIDBA== */
+ remote_key_abbrev: string;
+ /** @example FAKE: Features All Kids Erase */
+ title: string;
+ users: components['schemas']['ProjectUserEntry'][];
+ };
+ ProjectData: {
+ /** @example Example project */
+ description?: string | null;
+ /** @example main */
+ main_branch?: string | null;
+ /** @example ssh://git.example.org/srv/git/ */
+ remote?: string | null;
+ /** @example b3BlbNNz...AQIDBA== */
+ remote_key?: string | null;
+ /** @example FAKE: Features All Kids Erase */
+ title?: string | null;
+ };
+ ProjectEntry: {
+ /** @example fake */
+ id: string;
+ /** @example FAKE: Features All Kids Erase */
+ title: string;
+ };
+ ProjectUserEntry: {
+ default_role: components['schemas']['UserReviewRole'];
+ /** @example false */
+ maintainer: boolean;
+ user: components['schemas']['User'];
+ };
+ ProjectUserEntryData: {
+ default_role?: null | components['schemas']['UserReviewRole'];
+ /** @example false */
+ maintainer?: boolean | null;
+ };
+ Projects: {
+ /**
+ * Format: int32
+ * @example 10
+ */
+ limit: number;
+ /** @example false */
+ more: boolean;
+ /**
+ * Format: int32
+ * @example 0
+ */
+ offset: number;
+ projects: components['schemas']['ProjectEntry'][];
+ /**
+ * Format: int32
+ * @example 1
+ */
+ total_count: number;
+ };
+ Review: {
+ /** @example false */
+ archived: boolean;
+ /** @example r/user/TASK-123456 */
+ branch: string;
+ /** @example We're adding more features because features are what we want. */
+ description: string;
+ /**
+ * Format: int64
+ * @example 1000
+ */
+ id: number;
+ owner: components['schemas']['User'];
+ /**
+ * Format: float
+ * @example 37.5
+ */
+ progress: number;
+ state: components['schemas']['ReviewState'];
+ /** @example FAKE-512: Add more features */
+ title: string;
+ users: components['schemas']['ReviewUserEntry'][];
+ };
+ ReviewEntry: {
+ /** @example r/user/TASK-123456 */
+ branch: string;
+ /**
+ * Format: int64
+ * @example 1000
+ */
+ id: number;
+ owner: components['schemas']['User'];
+ /**
+ * Format: float
+ * @example 37.5
+ */
+ progress: number;
+ state: components['schemas']['ReviewState'];
+ /** @example FAKE-512: Add more features */
+ title: string;
+ };
+ /** @enum {string} */
+ ReviewState: 'Draft' | 'Open' | 'Dropped' | 'Closed';
+ ReviewUserEntry: {
+ role: components['schemas']['UserReviewRole'];
+ user: components['schemas']['User'];
+ };
+ Reviews: {
+ /**
+ * Format: int32
+ * @example 10
+ */
+ limit: number;
+ /** @example true */
+ more: boolean;
+ /**
+ * Format: int32
+ * @example 0
+ */
+ offset: number;
+ reviews: components['schemas']['ReviewEntry'][];
+ /**
+ * Format: int32
+ * @example 42
+ */
+ total_count: number;
+ };
+ StatusResponse: {
+ ok: boolean;
+ };
+ TranslationReview: {
+ /** @example false */
+ archived: boolean;
+ /** @example d7c502b9c6b833060576a0c4da0287933d603011 */
+ base: string;
+ /** @example New translations */
+ description: string;
+ /** @example 2cecdec660a30bf3964cee645d9cee03640ef8dc */
+ head: string;
+ /**
+ * Format: int64
+ * @example 1
+ */
+ id: number;
+ owner: components['schemas']['User'];
+ /**
+ * Format: float
+ * @example 37.5
+ */
+ progress: number;
+ state: components['schemas']['ReviewState'];
+ /** @example FAKE-512: Update translations */
+ title: string;
+ users: components['schemas']['ReviewUserEntry'][];
+ };
+ TranslationReviewData: {
+ /** @example d7c502b9c6b833060576a0c4da0287933d603011 */
+ base?: string | null;
+ /** @example New translations */
+ description: string;
+ /** @example FAKE-512: Update translations */
+ title: string;
+ };
+ TranslationReviewEntry: {
+ /** @example d7c502b9c6b833060576a0c4da0287933d603011 */
+ base: string;
+ /** @example 2cecdec660a30bf3964cee645d9cee03640ef8dc */
+ head: string;
+ /**
+ * Format: int64
+ * @example 1
+ */
+ id: number;
+ owner: components['schemas']['User'];
+ /**
+ * Format: float
+ * @example 37.5
+ */
+ progress: number;
+ state: components['schemas']['ReviewState'];
+ /** @example FAKE-512: Update translations */
+ title: string;
+ };
+ TranslationReviews: {
+ /**
+ * Format: int32
+ * @example 10
+ */
+ limit: number;
+ /** @example true */
+ more: boolean;
+ /**
+ * Format: int32
+ * @example 0
+ */
+ offset: number;
+ reviews: components['schemas']['TranslationReviewEntry'][];
+ /**
+ * Format: int32
+ * @example 42
+ */
+ total_count: number;
+ };
+ /** @enum {string} */
+ TranslationState: 'Unreviewed' | 'Unchanged' | 'Approved' | 'Revert' | 'Fix';
+ TranslationString: {
+ comment: string;
+ /** @example sv */
+ language: string;
+ placeholder_offset: number[];
+ reviewer?: null | components['schemas']['User'];
+ state: components['schemas']['TranslationState'];
+ /** @example Hej! */
+ translation: string;
+ };
+ User: {
+ /** @example true */
+ active: boolean;
+ /** @example jsmith */
+ id: string;
+ /** @example John Smith */
+ name: string;
+ };
+ UserKey: {
+ /** @example user@host 1970-01-01 */
+ comment: string;
+ /** @example AAAAfoobar== */
+ data: string;
+ /**
+ * Format: int64
+ * @example 1
+ */
+ id: number;
+ /** @example ssh-rsa */
+ kind: string;
+ };
+ UserKeyData: {
+ /** @example user@host 1970-01-01 */
+ comment?: string | null;
+ /** @example AAAAfoobar== */
+ data: string;
+ /** @example ssh-rsa */
+ kind: string;
+ };
+ UserKeys: {
+ keys: components['schemas']['UserKey'][];
+ /**
+ * Format: int32
+ * @example 10
+ */
+ limit: number;
+ /** @example false */
+ more: boolean;
+ /**
+ * Format: int32
+ * @example 0
+ */
+ offset: number;
+ /**
+ * Format: int32
+ * @example 2
+ */
+ total_count: number;
+ };
+ /** @enum {string} */
+ UserReviewRole: 'Reviewer' | 'Watcher' | 'None';
+ Users: {
+ /**
+ * Format: int32
+ * @example 10
+ */
+ limit: number;
+ /** @example true */
+ more: boolean;
+ /**
+ * Format: int32
+ * @example 0
+ */
+ offset: number;
+ /**
+ * Format: int32
+ * @example 42
+ */
+ total_count: number;
+ users: components['schemas']['User'][];
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record<string, never>;
+export interface operations {
+ healthcheck: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description All good */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- LocalizationString: {
- /** @example Generic greating */
- description: string;
- /** @example strings/strings.grd */
- file: string;
- /** @example IDS_GENERIC_WELCOME */
- id: string;
- /** @example This should be a positive greating */
- meaning: string;
- placeholder_offset: number[];
- placeholders: components["schemas"]["LocalizationPlaceholder"][];
- /** @example Hello! */
- source: string;
- translations: components["schemas"]["TranslationString"][];
+ content?: never;
+ };
+ };
+ };
+ login: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/x-www-form-urlencoded': components['schemas']['Login'];
+ };
+ };
+ responses: {
+ /** @description Login successful */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- LocalizationStrings: {
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example true */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- strings: components["schemas"]["LocalizationString"][];
- /**
- * Format: int32
- * @example 42
- */
- total_count: number;
+ content: {
+ /** @example {
+ * "ok": true
+ * } */
+ 'application/json': components['schemas']['StatusResponse'];
};
- Login: {
- password: string;
- username: string;
+ };
+ /** @description Login failed */
+ 401: {
+ headers: {
+ [name: string]: unknown;
};
- Project: {
- /** @example Example project */
- description: string;
- /** @example fake */
- id: string;
- /** @example main */
- main_branch: string;
- /** @example ssh://git.example.org/srv/git/ */
- remote: string;
- /** @example b3BlbNNz...AQIDBA== */
- remote_key_abbrev: string;
- /** @example FAKE: Features All Kids Erase */
- title: string;
- users: components["schemas"]["ProjectUserEntry"][];
+ content: {
+ /** @example {
+ * "error": "Unauthorized",
+ * "ok": false
+ * } */
+ 'application/json': components['schemas']['StatusResponse'];
};
- ProjectData: {
- /** @example Example project */
- description?: string | null;
- /** @example main */
- main_branch?: string | null;
- /** @example ssh://git.example.org/srv/git/ */
- remote?: string | null;
- /** @example b3BlbNNz...AQIDBA== */
- remote_key?: string | null;
- /** @example FAKE: Features All Kids Erase */
- title?: string | null;
+ };
+ };
+ };
+ logout: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Logout successful */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- ProjectEntry: {
- /** @example fake */
- id: string;
- /** @example FAKE: Features All Kids Erase */
- title: string;
+ content: {
+ /** @example {
+ * "ok": true
+ * } */
+ 'application/json': components['schemas']['StatusResponse'];
};
- ProjectUserEntry: {
- default_role: components["schemas"]["UserReviewRole"];
- /** @example false */
- maintainer: boolean;
- user: components["schemas"]["User"];
+ };
+ };
+ };
+ project: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ projectid: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Get project */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- ProjectUserEntryData: {
- default_role?: null | components["schemas"]["UserReviewRole"];
- /** @example false */
- maintainer?: boolean | null;
+ content: {
+ 'application/json': components['schemas']['Project'];
};
- Projects: {
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example false */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- projects: components["schemas"]["ProjectEntry"][];
- /**
- * Format: int32
- * @example 1
- */
- total_count: number;
+ };
+ /** @description No such project */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
- Review: {
- /** @example false */
- archived: boolean;
- /** @example r/user/TASK-123456 */
- branch: string;
- /** @example We're adding more features because features are what we want. */
- description: string;
- /**
- * Format: int64
- * @example 1000
- */
- id: number;
- owner: components["schemas"]["User"];
- /**
- * Format: float
- * @example 37.5
- */
- progress: number;
- state: components["schemas"]["ReviewState"];
- /** @example FAKE-512: Add more features */
- title: string;
- users: components["schemas"]["ReviewUserEntry"][];
+ content?: never;
+ };
+ };
+ };
+ project_update: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ projectid: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ProjectData'];
+ };
+ };
+ responses: {
+ /** @description Project updated */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- ReviewEntry: {
- /** @example r/user/TASK-123456 */
- branch: string;
- /**
- * Format: int64
- * @example 1000
- */
- id: number;
- owner: components["schemas"]["User"];
- /**
- * Format: float
- * @example 37.5
- */
- progress: number;
- state: components["schemas"]["ReviewState"];
- /** @example FAKE-512: Add more features */
- title: string;
+ content?: never;
+ };
+ /** @description Not maintainer of project */
+ 401: {
+ headers: {
+ [name: string]: unknown;
};
- /** @enum {string} */
- ReviewState: "Draft" | "Open" | "Dropped" | "Closed";
- ReviewUserEntry: {
- role: components["schemas"]["UserReviewRole"];
- user: components["schemas"]["User"];
+ content?: never;
+ };
+ /** @description No such project */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
- Reviews: {
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example true */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- reviews: components["schemas"]["ReviewEntry"][];
- /**
- * Format: int32
- * @example 42
- */
- total_count: number;
+ content?: never;
+ };
+ };
+ };
+ project_new: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ projectid: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ProjectData'];
+ };
+ };
+ responses: {
+ /** @description Project created */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- StatusResponse: {
- ok: boolean;
+ content: {
+ 'application/json': components['schemas']['Project'];
};
- TranslationReview: {
- /** @example false */
- archived: boolean;
- /** @example d7c502b9c6b833060576a0c4da0287933d603011 */
- base: string;
- /** @example New translations */
- description: string;
- /** @example 2cecdec660a30bf3964cee645d9cee03640ef8dc */
- head: string;
- /**
- * Format: int64
- * @example 1
- */
- id: number;
- owner: components["schemas"]["User"];
- /**
- * Format: float
- * @example 37.5
- */
- progress: number;
- state: components["schemas"]["ReviewState"];
- /** @example FAKE-512: Update translations */
- title: string;
- users: components["schemas"]["ReviewUserEntry"][];
+ };
+ /** @description Project with id already exists */
+ 409: {
+ headers: {
+ [name: string]: unknown;
};
- TranslationReviewData: {
- /** @example d7c502b9c6b833060576a0c4da0287933d603011 */
- base?: string | null;
- /** @example New translations */
- description: string;
- /** @example FAKE-512: Update translations */
- title: string;
+ content?: never;
+ };
+ };
+ };
+ reviews: {
+ parameters: {
+ query?: {
+ limit?: number;
+ offset?: number;
+ };
+ header?: never;
+ path: {
+ projectid: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Get all reviews for project */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- TranslationReviewEntry: {
- /** @example d7c502b9c6b833060576a0c4da0287933d603011 */
- base: string;
- /** @example 2cecdec660a30bf3964cee645d9cee03640ef8dc */
- head: string;
- /**
- * Format: int64
- * @example 1
- */
- id: number;
- owner: components["schemas"]["User"];
- /**
- * Format: float
- * @example 37.5
- */
- progress: number;
- state: components["schemas"]["ReviewState"];
- /** @example FAKE-512: Update translations */
- title: string;
+ content: {
+ 'application/json': components['schemas']['Reviews'];
};
- TranslationReviews: {
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example true */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- reviews: components["schemas"]["TranslationReviewEntry"][];
- /**
- * Format: int32
- * @example 42
- */
- total_count: number;
+ };
+ /** @description No such project */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
- /** @enum {string} */
- TranslationState: "Unreviewed" | "Unchanged" | "Approved" | "Revert" | "Fix";
- TranslationString: {
- comment: string;
- /** @example sv */
- language: string;
- placeholder_offset: number[];
- reviewer?: null | components["schemas"]["User"];
- state: components["schemas"]["TranslationState"];
- /** @example Hej! */
- translation: string;
+ content?: never;
+ };
+ };
+ };
+ translation_reviews: {
+ parameters: {
+ query?: {
+ limit?: number;
+ offset?: number;
+ };
+ header?: never;
+ path: {
+ projectid: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Get all translation reviews for project */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- User: {
- /** @example true */
- active: boolean;
- /** @example jsmith */
- id: string;
- /** @example John Smith */
- name: string;
+ content: {
+ 'application/json': components['schemas']['TranslationReviews'];
};
- UserKey: {
- /** @example user@host 1970-01-01 */
- comment: string;
- /** @example AAAAfoobar== */
- data: string;
- /**
- * Format: int64
- * @example 1
- */
- id: number;
- /** @example ssh-rsa */
- kind: string;
+ };
+ /** @description No such project */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
- UserKeyData: {
- /** @example user@host 1970-01-01 */
- comment?: string | null;
- /** @example AAAAfoobar== */
- data: string;
- /** @example ssh-rsa */
- kind: string;
+ content?: never;
+ };
+ };
+ };
+ project_user_update: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ projectid: string;
+ userid: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ProjectUserEntryData'];
+ };
+ };
+ responses: {
+ /** @description User updated in project */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- UserKeys: {
- keys: components["schemas"]["UserKey"][];
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example false */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- /**
- * Format: int32
- * @example 2
- */
- total_count: number;
+ content?: never;
+ };
+ /** @description Not maintainer of project */
+ 401: {
+ headers: {
+ [name: string]: unknown;
};
- /** @enum {string} */
- UserReviewRole: "Reviewer" | "Watcher" | "None";
- Users: {
- /**
- * Format: int32
- * @example 10
- */
- limit: number;
- /** @example true */
- more: boolean;
- /**
- * Format: int32
- * @example 0
- */
- offset: number;
- /**
- * Format: int32
- * @example 42
- */
- total_count: number;
- users: components["schemas"]["User"][];
+ content?: never;
+ };
+ /** @description No such project, no such user or user not in project */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
+ };
};
- responses: never;
- parameters: never;
- requestBodies: never;
- headers: never;
- pathItems: never;
-}
-export type $defs = Record<string, never>;
-export interface operations {
- healthcheck: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description All good */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
+ };
+ project_user_del: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ projectid: string;
+ userid: string;
+ };
+ cookie?: never;
};
- login: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
+ requestBody?: never;
+ responses: {
+ /** @description User removed from project */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody: {
- content: {
- "application/x-www-form-urlencoded": components["schemas"]["Login"];
- };
+ content?: never;
+ };
+ /** @description Not maintainer of project */
+ 401: {
+ headers: {
+ [name: string]: unknown;
};
- responses: {
- /** @description Login successful */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- /** @example {
- * "ok": true
- * } */
- "application/json": components["schemas"]["StatusResponse"];
- };
- };
- /** @description Login failed */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- /** @example {
- * "error": "Unauthorized",
- * "ok": false
- * } */
- "application/json": components["schemas"]["StatusResponse"];
- };
- };
+ content?: never;
+ };
+ /** @description No such project, no such user or user not in project */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
+ };
};
- logout: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
+ };
+ project_user_add: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ projectid: string;
+ userid: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ProjectUserEntryData'];
+ };
+ };
+ responses: {
+ /** @description User added to project */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description Logout successful */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- /** @example {
- * "ok": true
- * } */
- "application/json": components["schemas"]["StatusResponse"];
- };
- };
+ content?: never;
+ };
+ /** @description Not maintainer of project */
+ 401: {
+ headers: {
+ [name: string]: unknown;
};
- };
- project: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
+ content?: never;
+ };
+ /** @description No such project */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description Get project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Project"];
- };
- };
- /** @description No such project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ content?: never;
+ };
+ /** @description User already in project */
+ 409: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
+ };
};
- project_update: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["ProjectData"];
- };
+ };
+ projects: {
+ parameters: {
+ query?: {
+ limit?: number;
+ offset?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Get all projects */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- responses: {
- /** @description Project updated */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ content: {
+ 'application/json': components['schemas']['Projects'];
};
+ };
};
- project_new: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
+ };
+ review_id: {
+ parameters: {
+ query: {
+ reviewid: number;
+ };
+ header?: never;
+ path: {
+ projectid: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Get review */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["ProjectData"];
- };
+ content: {
+ 'application/json': components['schemas']['Review'];
};
- responses: {
- /** @description Project created */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Project"];
- };
- };
- /** @description Project with id already exists */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ };
+ /** @description No such review */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
+ };
};
- reviews: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get all reviews for project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Reviews"];
- };
- };
- /** @description No such project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
+ };
+ review_id_del: {
+ parameters: {
+ query: {
+ reviewid: number;
+ };
+ header?: never;
+ path: {
+ projectid: string;
+ };
+ cookie?: never;
};
- translation_reviews: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get all translation reviews for project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["TranslationReviews"];
- };
- };
- /** @description No such project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ requestBody?: never;
+ responses: {
+ /** @description Remove deleted */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- };
- project_user_update: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- userid: string;
- };
- cookie?: never;
+ content?: never;
+ };
+ /** @description Review is open or closed */
+ 400: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["ProjectUserEntryData"];
- };
+ content?: never;
+ };
+ /** @description Not owner of review or maintainer of project */
+ 401: {
+ headers: {
+ [name: string]: unknown;
};
- responses: {
- /** @description User updated in project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such project, no such user or user not in project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ content?: never;
+ };
+ /** @description No such review */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
+ };
};
- project_user_del: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- userid: string;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description User removed from project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such project, no such user or user not in project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
+ };
+ review: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ branch: string;
+ projectid: string;
+ };
+ cookie?: never;
};
- project_user_add: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- userid: string;
- };
- cookie?: never;
+ requestBody?: never;
+ responses: {
+ /** @description Get review */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["ProjectUserEntryData"];
- };
+ content: {
+ 'application/json': components['schemas']['Review'];
};
- responses: {
- /** @description User added to project */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such project */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description User already in project */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ };
+ /** @description No such review */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
+ };
};
- projects: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get all projects */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Projects"];
- };
- };
- };
+ };
+ review_del: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ branch: string;
+ projectid: string;
+ };
+ cookie?: never;
};
- review_id: {
- parameters: {
- query: {
- reviewid: number;
- };
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
+ requestBody?: never;
+ responses: {
+ /** @description Review deleted */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description Get review */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Review"];
- };
- };
- /** @description No such review */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ content?: never;
+ };
+ /** @description Review is open or closed */
+ 400: {
+ headers: {
+ [name: string]: unknown;
};
- };
- review_id_del: {
- parameters: {
- query: {
- reviewid: number;
- };
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
+ content?: never;
+ };
+ /** @description Not owner of review or maintainer of project */
+ 401: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description Remove deleted */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Review is open or closed */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not owner of review or maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such review */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ content?: never;
+ };
+ /** @description No such review */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
+ };
+ };
+ };
+ status: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- review: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- branch: string;
- projectid: string;
- };
- cookie?: never;
+ requestBody?: never;
+ responses: {
+ /** @description Current status */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description Get review */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Review"];
- };
- };
- /** @description No such review */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ content: {
+ /** @example {
+ * "ok": true
+ * } */
+ 'application/json': components['schemas']['StatusResponse'];
};
- };
- review_del: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- branch: string;
- projectid: string;
- };
- cookie?: never;
+ };
+ /** @description Not authorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description Review deleted */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Review is open or closed */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Not owner of review or maintainer of project */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such review */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ content: {
+ /** @example {
+ * "error": "Unauthorized",
+ * "ok": false
+ * } */
+ 'application/json': components['schemas']['StatusResponse'];
};
+ };
+ };
+ };
+ translation_review_new: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ projectid: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['TranslationReviewData'];
+ };
};
- status: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
+ responses: {
+ /** @description Translation review created */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description Current status */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- /** @example {
- * "ok": true
- * } */
- "application/json": components["schemas"]["StatusResponse"];
- };
- };
- /** @description Not authorized */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- /** @example {
- * "error": "Unauthorized",
- * "ok": false
- * } */
- "application/json": components["schemas"]["StatusResponse"];
- };
- };
+ content: {
+ 'application/json': components['schemas']['TranslationReview'];
};
+ };
};
- translation_review_new: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- projectid: string;
- };
- cookie?: never;
+ };
+ translation_review_strings: {
+ parameters: {
+ query?: {
+ limit?: number;
+ offset?: number;
+ };
+ header?: never;
+ path: {
+ translation_reviewid: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Get all strings for a translation review */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["TranslationReviewData"];
- };
+ content: {
+ 'application/json': components['schemas']['LocalizationStrings'];
};
- responses: {
- /** @description Translation review created */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["TranslationReview"];
- };
- };
+ };
+ /** @description No such translation review */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
+ };
};
- translation_review_strings: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path: {
- translation_reviewid: number;
- };
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Get all strings for a translation review */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["LocalizationStrings"];
- };
- };
- /** @description No such translation review */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
+ };
+ user_keys: {
+ parameters: {
+ query?: {
+ limit?: number;
+ offset?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- user_keys: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path?: never;
- cookie?: never;
+ requestBody?: never;
+ responses: {
+ /** @description Get all keys for user */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description Get all keys for user */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["UserKeys"];
- };
- };
+ content: {
+ 'application/json': components['schemas']['UserKeys'];
};
+ };
};
- user_key_add: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
+ };
+ user_key_add: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['UserKeyData'];
+ };
+ };
+ responses: {
+ /** @description Key added to current user */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["UserKeyData"];
- };
+ content: {
+ 'application/json': components['schemas']['UserKey'];
};
- responses: {
- /** @description Key added to current user */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["UserKey"];
- };
- };
- /** @description Key too large or invalid */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ };
+ /** @description Key too large or invalid */
+ 400: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
+ };
+ };
+ };
+ user_key_get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- user_key_get: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- id: number;
- };
- cookie?: never;
+ requestBody?: never;
+ responses: {
+ /** @description User key */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description User key */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["UserKey"];
- };
- };
- /** @description No such key */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ content: {
+ 'application/json': components['schemas']['UserKey'];
};
+ };
+ /** @description No such key */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ user_key_del: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- user_key_del: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- id: number;
- };
- cookie?: never;
+ requestBody?: never;
+ responses: {
+ /** @description Key removed from current user */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description Key removed from current user */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description No such key for current user */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
+ content?: never;
+ };
+ /** @description No such key for current user */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
+ };
+ };
+ };
+ users: {
+ parameters: {
+ query?: {
+ limit?: number;
+ offset?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- users: {
- parameters: {
- query?: {
- limit?: number;
- offset?: number;
- };
- header?: never;
- path?: never;
- cookie?: never;
+ requestBody?: never;
+ responses: {
+ /** @description Get all users */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- requestBody?: never;
- responses: {
- /** @description Get all users */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Users"];
- };
- };
+ content: {
+ 'application/json': components['schemas']['Users'];
};
+ };
};
+ };
}
diff --git a/client/src/lib/config.ts b/client/src/lib/config.ts
new file mode 100644
index 0000000..fccb09d
--- /dev/null
+++ b/client/src/lib/config.ts
@@ -0,0 +1,54 @@
+import { browser } from '$app/environment';
+import type { Infer } from 'superstruct';
+import { assert, object, optional, number, string } from 'superstruct';
+
+const Config = object({
+ active_project: optional(string())
+});
+
+type Config = Infer<typeof Config>;
+
+const CachedConfig = object({
+ config: Config,
+ expires: number()
+});
+
+type CachedConfig = Infer<typeof CachedConfig>;
+
+const CACHE_TTL = 10 * 60 * 60 * 1000;
+
+async function get_config(): Promise<Config> {
+ // Cache, might be outdated but saves on call to server
+ if (browser) {
+ try {
+ // TODO: Use async localStorage
+ const stored = localStorage.getItem('config');
+ if (stored !== null) {
+ const cached: CachedConfig = JSON.parse(stored);
+ assert(cached, CachedConfig);
+ if (cached.expires < Date.now()) {
+ // TODO: Should we update expires here?
+ // If page is in use we probably don't need to sync for a while.
+ return cached.config;
+ }
+ }
+ } catch {
+ // ignore errors
+ }
+ }
+
+ // Default config
+ // eslint-disable-next-line prefer-const
+ let config: Config = { active_project: undefined };
+
+ // TODO: Fetch config
+
+ if (browser) {
+ const cached: CachedConfig = { config: config, expires: Date.now() + CACHE_TTL };
+ // TODO: Use async localStorage
+ localStorage.setItem('config', JSON.stringify(cached));
+ }
+ return config;
+}
+
+export { type Config, get_config };
diff --git a/client/src/lib/fetch-client.ts b/client/src/lib/fetch-client.ts
new file mode 100644
index 0000000..b0b60bd
--- /dev/null
+++ b/client/src/lib/fetch-client.ts
@@ -0,0 +1,10 @@
+import { PUBLIC_BASE_URL } from '$env/static/public';
+import createClient from 'openapi-fetch';
+import type { paths } from './api/schema.d.ts';
+
+const client = createClient<paths>({
+ // TODO: Can we make this relative?
+ baseUrl: PUBLIC_BASE_URL + '/api/v1'
+});
+
+export { client };
diff --git a/client/src/routes/(app)/+error.svelte b/client/src/routes/(app)/+error.svelte
new file mode 100644
index 0000000..63f3d66
--- /dev/null
+++ b/client/src/routes/(app)/+error.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+ import { page } from '$app/state';
+</script>
+
+<h1>{page.status} {page.error?.message}</h1>
diff --git a/client/src/routes/(app)/+layout.svelte b/client/src/routes/(app)/+layout.svelte
new file mode 100644
index 0000000..47dc736
--- /dev/null
+++ b/client/src/routes/(app)/+layout.svelte
@@ -0,0 +1,12 @@
+<script lang="ts">
+ let { children } = $props();
+</script>
+
+<h1>eyeballs</h1>
+
+<nav>
+ <a href="/">Dashboard</a>
+ <a href="/settings">Settings</a>
+</nav>
+
+{@render children()}
diff --git a/client/src/routes/(app)/+layout.ts b/client/src/routes/(app)/+layout.ts
new file mode 100644
index 0000000..08366b0
--- /dev/null
+++ b/client/src/routes/(app)/+layout.ts
@@ -0,0 +1,6 @@
+import type { LayoutLoad } from './$types';
+
+export const load: LayoutLoad = () => {
+ // TODO: Decrypt sessioncookie if set, if not set, redirect to /login
+ return {};
+};
diff --git a/client/src/routes/(app)/+page.svelte b/client/src/routes/(app)/+page.svelte
new file mode 100644
index 0000000..945945e
--- /dev/null
+++ b/client/src/routes/(app)/+page.svelte
@@ -0,0 +1,25 @@
+<script lang="ts">
+ import type { PageProps } from './$types';
+ import ListPrevNext from '$lib/ListPrevNext.svelte';
+ let { data }: PageProps = $props();
+</script>
+
+<h1>Reviews</h1>
+
+{#if data.reviews === undefined}
+ Select active project
+
+ <ul>
+ {#each data.projects!!.projects as { id, title } (id)}
+ <li>{title}</li>
+ {/each}
+ </ul>
+ <ListPrevNext list={data.projects} query_offset="project_offset" />
+{:else}
+ <ul>
+ {#each data.reviews.reviews as { id, title } (id)}
+ <li>{title}</li>
+ {/each}
+ </ul>
+ <ListPrevNext list={data.reviews} />
+{/if}
diff --git a/client/src/routes/(app)/+page.ts b/client/src/routes/(app)/+page.ts
new file mode 100644
index 0000000..b6b923e
--- /dev/null
+++ b/client/src/routes/(app)/+page.ts
@@ -0,0 +1,38 @@
+import { client } from '$lib/fetch-client';
+import { get_config } from '$lib/config';
+import type { PageLoad } from './$types';
+
+function maybeInt(input: string | null, fallback: number): number {
+ if (input === null) return fallback;
+ try {
+ return parseInt(input);
+ } catch {
+ return fallback;
+ }
+}
+
+export const load: PageLoad = async ({ fetch, url }) => {
+ const config = await get_config();
+ if (config.active_project === undefined) {
+ const projects = await client.GET('/projects', {
+ params: { query: { offset: maybeInt(url.searchParams.get('projects_offset'), 0) } },
+ fetch
+ });
+ return {
+ projects: projects.data!!,
+ reviews: undefined
+ };
+ } else {
+ const reviews = await client.GET('/project/{projectid}/reviews', {
+ params: {
+ path: { projectid: config.active_project },
+ query: { offset: maybeInt(url.searchParams.get('reviews_offset'), 0) }
+ },
+ fetch
+ });
+ return {
+ projects: undefined,
+ reviews: reviews.data
+ };
+ }
+};
diff --git a/client/src/routes/+page.svelte b/client/src/routes/+page.svelte
deleted file mode 100644
index cc88df0..0000000
--- a/client/src/routes/+page.svelte
+++ /dev/null
@@ -1,2 +0,0 @@
-<h1>Welcome to SvelteKit</h1>
-<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
diff --git a/client/src/routes/login/+page.server.ts b/client/src/routes/login/+page.server.ts
new file mode 100644
index 0000000..738b8ad
--- /dev/null
+++ b/client/src/routes/login/+page.server.ts
@@ -0,0 +1,36 @@
+import { redirect } from '@sveltejs/kit';
+import { base } from '$app/paths';
+import type { Actions } from './$types';
+import { client } from '$lib/fetch-client';
+
+export const actions = {
+ default: async ({ request, fetch }) => {
+ const data = await request.formData();
+ const username = data.get('username');
+ const password = data.get('password');
+ const ret = data.get('return');
+
+ const login = await client.POST('/login', {
+ body: {
+ username: username?.toString() || '',
+ password: password?.toString() || ''
+ },
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ fetch
+ });
+ if (login.data?.ok === true) {
+ if (ret) {
+ redirect(303, ret.toString());
+ } else {
+ redirect(303, base);
+ }
+ } else {
+ return {
+ error: true,
+ username: username
+ };
+ }
+ }
+} satisfies Actions;
diff --git a/client/src/routes/login/+page.svelte b/client/src/routes/login/+page.svelte
new file mode 100644
index 0000000..8a91125
--- /dev/null
+++ b/client/src/routes/login/+page.svelte
@@ -0,0 +1,22 @@
+<script lang="ts">
+ import type { PageProps } from './$types';
+
+ let { data, form }: PageProps = $props();
+</script>
+
+{#if form?.error}
+ <p>Unknown username or password</p>
+{/if}
+
+<form method="POST">
+ <label>
+ Username
+ <input name="username" type="text" value={form?.username} />
+ </label>
+ <label>
+ Password
+ <input name="password" type="password" />
+ </label>
+ <button>Log in</button>
+ <input type="hidden" name="return" value={data.return} />
+</form>
diff --git a/client/src/routes/login/+page.ts b/client/src/routes/login/+page.ts
new file mode 100644
index 0000000..70d306a
--- /dev/null
+++ b/client/src/routes/login/+page.ts
@@ -0,0 +1,7 @@
+import type { PageLoad } from './$types';
+
+export const load: PageLoad = async ({ url }) => {
+ return {
+ return: url.searchParams.get('return') || ''
+ };
+};
diff --git a/client/svelte.config.js b/client/svelte.config.js
index c138f77..cedc080 100644
--- a/client/svelte.config.js
+++ b/client/svelte.config.js
@@ -3,13 +3,23 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
- // Consult https://svelte.dev/docs/kit/integrations
- // for more information about preprocessors
- preprocess: vitePreprocess(),
+ // Consult https://svelte.dev/docs/kit/integrations
+ // for more information about preprocessors
+ preprocess: vitePreprocess(),
- kit: {
- adapter: adapter()
+ kit: {
+ adapter: adapter()
+ },
+ csp: {
+ directives: {
+ 'script-src': ['self']
+ },
+ // must be specified with either the `report-uri` or `report-to` directives, or both
+ reportOnly: {
+ 'script-src': ['self'],
+ 'report-uri': ['/']
}
+ }
};
export default config;
diff --git a/client/tsconfig.json b/client/tsconfig.json
index e99bb7b..373e32e 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -1,20 +1,21 @@
{
- "extends": "./.svelte-kit/tsconfig.json",
- "compilerOptions": {
- "allowJs": true,
- "checkJs": true,
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "resolveJsonModule": true,
- "skipLibCheck": true,
- "sourceMap": true,
- "strict": true,
- "moduleResolution": "bundler",
- "noUncheckedIndexedAccess": true
- }
- // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
- // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
- //
- // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
- // from the referenced tsconfig.json - TypeScript does not merge them in
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler",
+ "noUncheckedIndexedAccess": true,
+ "strictNullChecks": true
+ }
+ // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
+ // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
+ //
+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
+ // from the referenced tsconfig.json - TypeScript does not merge them in
}
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 95eb0be..66d0f3f 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -3,5 +3,10 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
- plugins: [sveltekit(), devtoolsJson()]
+ plugins: [sveltekit(), devtoolsJson()],
+ server: {
+ proxy: {
+ '/api': 'http://127.0.0.1:8000'
+ }
+ }
});
diff --git a/server/Cargo.lock b/server/Cargo.lock
index cf2a209..52168cd 100644
--- a/server/Cargo.lock
+++ b/server/Cargo.lock
@@ -593,6 +593,7 @@ dependencies = [
"reqwest",
"rmp-serde",
"rocket",
+ "rocket_cors",
"rocket_db_pools",
"serde",
"serial_test",
@@ -2133,6 +2134,23 @@ dependencies = [
]
[[package]]
+name = "rocket_cors"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfac3a1df83f8d4fc96aa41dba3b86c786417b7fc0f52ec76295df2ba781aa69"
+dependencies = [
+ "http 0.2.12",
+ "log",
+ "regex",
+ "rocket",
+ "serde",
+ "serde_derive",
+ "unicase",
+ "unicase_serde",
+ "url",
+]
+
+[[package]]
name = "rocket_db_pools"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3262,6 +3280,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
+name = "unicase_serde"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ef53697679d874d69f3160af80bc28de12730a985d57bdf2b47456ccb8b11f1"
+dependencies = [
+ "serde",
+ "unicase",
+]
+
+[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 9cf031f..a634083 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -30,6 +30,7 @@ log.workspace = true
pretty_assertions.workspace = true
rmp-serde.workspace = true
rocket = { version = "0.5.1", features = ["json", "secrets"] }
+rocket_cors = "0.6.0"
rocket_db_pools = { version = "0.2.0", features = ["sqlx_mysql"] }
serde.workspace = true
serial_test = "3.2.0"
diff --git a/server/src/auth.rs b/server/src/auth.rs
index edd794c..530b2ef 100644
--- a/server/src/auth.rs
+++ b/server/src/auth.rs
@@ -225,7 +225,7 @@ async fn login(
);
let cookie = Cookie::build((SESSION_COOKIE, json::to_string(&session).unwrap()))
- .path("/api")
+ .path("/")
.max_age(max_age)
.http_only(true)
.build();
diff --git a/server/src/main.rs b/server/src/main.rs
index 7a6b1b7..9a4f781 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -8,6 +8,7 @@ use rocket::http::Status;
use rocket::response::status::{Custom, NotFound};
use rocket::serde::json::Json;
use rocket::{futures, Build, Rocket, State};
+use rocket_cors::AllowedOrigins;
use rocket_db_pools::{sqlx, Connection, Database};
use sqlx::Acquire;
use std::path::PathBuf;
@@ -1465,6 +1466,15 @@ async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
fn rocket_from_config(figment: Figment) -> Rocket<Build> {
let basepath = "/api/v1";
+
+ let cors = rocket_cors::CorsOptions {
+ allowed_origins: AllowedOrigins::all(),
+ allow_credentials: false,
+ ..Default::default()
+ }
+ .to_cors()
+ .unwrap();
+
rocket::custom(figment)
.attach(Db::init())
.attach(AdHoc::try_on_ignite("Database Migrations", run_migrations))
@@ -1497,6 +1507,7 @@ fn rocket_from_config(figment: Figment) -> Rocket<Build> {
translation_reviews,
],
)
+ .attach(cors)
.attach(auth::stage(basepath))
.attach(git_root::stage())
.attach(authorized_keys::stage())