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 /client | |
| parent | 145be2b3c92e254904d4040850e3c1e9b6a66f32 (diff) | |
Humble beginnings
Redirect to login if not logged in, on login session cookie is set
and projects or reviews are listed.
Diffstat (limited to 'client')
25 files changed, 1724 insertions, 2841 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' + } + } }); |
