diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2025-07-17 23:42:55 +0200 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2025-07-17 23:44:11 +0200 |
| commit | bef3da2a567e3804e12355d9c3d5c09439dbe2ea (patch) | |
| tree | ab7974c941bd31994da46150234976b33c2f61b5 | |
| parent | 145be2b3c92e254904d4040850e3c1e9b6a66f32 (diff) | |
Humble beginnings
Redirect to login if not logged in, on login session cookie is set
and projects or reviews are listed.
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()) |
