Add a small, tested base for all three clients, and Gitea CI/release workflows
internal/client (new): a typed Go HTTP client for the API - login, register, logout, me, and full CRUD for games/platforms - shared by the TUI and the Fyne GUI so request-building, auth and error handling (an APIError with the HTTP status) live in one tested place instead of being duplicated. 8 tests against an httptest server covering success, 401, 403 and that the token is stored/cleared at the right times. cmd/tui (rewrite): drops the old unauthenticated read-only viewer. Now a login screen (bubbles/textinput) -> a scrollable games table (bubbles/table) with 'r' to refresh, built as an explicit state machine (screenLogin/screenLoading/screenGames) so Update's transitions are testable without a real terminal. 9 tests driving Update() directly. cmd/gui (rewrite): drops raylib for Fyne. Same shape as the TUI - a login form, then a games table, with networking on a goroutine and fyne.Do for the UI updates. The table-cell and login-validation logic are plain functions with no Fyne/display dependency, so they're tested directly (7 tests) rather than needing Fyne's headless test driver. internal/api: tagged every operation (Auth/Platforms/Games/Misc) so the generated OpenAPI spec groups into named services instead of one bucket; api/openapi.json regenerated. Added CORS middleware (wildcard origin, no credentials needed since clients use a bearer token, not the cookie) - needed for the new web client's browser requests to the API's a different origin. web/ (new): an Angular 22 + Material app, API client generated from api/openapi.json via ng-openapi-gen (services: true for per-resource classes - AuthService/GamesService/PlatformsService - instead of one generic Api facade). A Session service holds the token/user as signals, persists the token to localStorage, and is the only thing that calls the generated AuthService; an HttpInterceptorFn attaches the bearer token to every request; a CanActivateFn guard protects /games. Login and Games are standalone, lazy-loaded route components. 21 tests (Vitest, Angular's new default - no browser/Karma needed) covering the service, interceptor, guard and both components, run via `npm test` exactly as CI will call it. Justfile: fixed run-tui/run-gui, which used `go run .../main.go` - broken now that both packages span multiple files, since that only compiles the one named file. Added web-install/web-dev/web-build/web-test. .gitea/workflows/ci.yml (moved from .github, which Gitea doesn't read): on push to main/develop - gofmt, go vet, go build, go test, go mod tidy diff (with the Fyne build's system deps installed), sqlc diff, and the web app's build+test. .gitea/workflows/release.yml (new): on a vX.Y.Z tag, cross-compiles api+tui for linux/darwin/windows (amd64+arm64, pure Go, no cgo), builds the Fyne GUI for linux/amd64 only (cgo + its OpenGL/X11 link means it can't cross-compile from one Linux runner the way api/tui do, and this repo has no registered macOS/Windows runner to build it on natively - documented in the workflow's comments), builds the web app, and creates a Gitea release with all of it attached via the Gitea API. Written to Gitea's documented GitHub-Actions-compatible syntax but not executable from here - there's no Gitea server in this environment to run it against, unlike everything else in this commit, which was. Verified: every Go package builds/vets/tests clean; sqlc diff and go mod tidy -diff are clean; the Angular app builds for production and its test suite passes via `npm test`/`npm run build` (the exact commands CI runs), including after a clean `npm ci` from the committed lockfile; the new CORS config was checked with real preflight and GET requests against a running server, not just read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
384c4a7217
commit
9972537b63
97 files changed
+11810
-336
No files matched your search
@@ -0,0 +1,17 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,44 @@
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/mcp.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
__screenshots__/
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ng serve",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/"
|
||||
},
|
||||
{
|
||||
"name": "ng test",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: test",
|
||||
"url": "http://localhost:9876/debug.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "Changes detected"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation (complete|failed)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "Changes detected"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation (complete|failed)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# Web
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 22.2.1.
|
||||
|
||||
## Development server
|
||||
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"packageManager": "npm"
|
||||
},
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"web": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"browser": "src/main.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": ["src/material-theme.scss", "src/styles.css"]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "web:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "web:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:unit-test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "node_modules/ng-openapi-gen/ng-openapi-gen-schema.json",
|
||||
"input": "../api/openapi.json",
|
||||
"output": "src/app/api",
|
||||
"ignoreUnusedModels": false,
|
||||
"services": true
|
||||
}
|
||||
Generated
+7131
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"packageManager": "npm@11.19.1",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^22.2.1",
|
||||
"@angular/cdk": "^22.2.1",
|
||||
"@angular/common": "^22.2.0",
|
||||
"@angular/compiler": "^22.2.0",
|
||||
"@angular/core": "^22.2.0",
|
||||
"@angular/forms": "^22.2.0",
|
||||
"@angular/material": "^22.2.1",
|
||||
"@angular/platform-browser": "^22.2.0",
|
||||
"@angular/router": "^22.2.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^22.2.1",
|
||||
"@angular/cli": "^22.2.1",
|
||||
"@angular/compiler-cli": "^22.2.0",
|
||||
"jsdom": "^30.0.0",
|
||||
"ng-openapi-gen": "^1.1.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~6.0.2",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,27 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Provides the ApiConfiguration configuration object with a given root URL.
|
||||
*/
|
||||
export function provideApiConfiguration(rootUrl: string) {
|
||||
var config = new ApiConfiguration();
|
||||
config.rootUrl = rootUrl;
|
||||
return {
|
||||
provide: ApiConfiguration,
|
||||
useValue: config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Global configuration
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ApiConfiguration {
|
||||
rootUrl: string = '';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable, firstValueFrom } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { ApiConfiguration } from './api-configuration';
|
||||
import { StrictHttpResponse } from './strict-http-response';
|
||||
|
||||
export type ApiFnOptional<P, R> = (http: HttpClient, rootUrl: string, params?: P, context?: HttpContext) => Observable<StrictHttpResponse<R>>;
|
||||
export type ApiFnRequired<P, R> = (http: HttpClient, rootUrl: string, params: P, context?: HttpContext) => Observable<StrictHttpResponse<R>>;
|
||||
|
||||
/**
|
||||
* Helper service to call API functions directly
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class Api {
|
||||
constructor(
|
||||
private config: ApiConfiguration,
|
||||
private http: HttpClient
|
||||
) {
|
||||
}
|
||||
|
||||
private _rootUrl?: string;
|
||||
|
||||
/**
|
||||
* Returns the root url for API operations. If not set directly here,
|
||||
* will fallback to `ApiConfiguration.rootUrl`.
|
||||
*/
|
||||
get rootUrl(): string {
|
||||
return this._rootUrl || this.config.rootUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the root URL for API operations
|
||||
*/
|
||||
set rootUrl(rootUrl: string) {
|
||||
this._rootUrl = rootUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an API call, returning the response body only
|
||||
*/
|
||||
invoke<P, R>(fn: ApiFnRequired<P, R>, params: P, context?: HttpContext): Promise<R>;
|
||||
invoke<P, R>(fn: ApiFnOptional<P, R>, params?: P, context?: HttpContext): Promise<R>;
|
||||
async invoke<P, R>(fn: ApiFnRequired<P, R> | ApiFnOptional<P, R>, params: P, context?: HttpContext): Promise<R> {
|
||||
const resp = this.invoke$Response(fn, params, context);
|
||||
return (await resp).body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an API call, returning the entire response
|
||||
*/
|
||||
invoke$Response<P, R>(fn: ApiFnRequired<P, R>, params: P, context?: HttpContext): Promise<StrictHttpResponse<R>>;
|
||||
invoke$Response<P, R>(fn: ApiFnOptional<P, R>, params?: P, context?: HttpContext): Promise<StrictHttpResponse<R>>;
|
||||
invoke$Response<P, R>(fn: ApiFnRequired<P, R> | ApiFnOptional<P, R>, params: P, context?: HttpContext): Promise<StrictHttpResponse<R>> {
|
||||
const obs = fn(this.http, this.rootUrl, params, context)
|
||||
.pipe(
|
||||
filter(r => r instanceof HttpResponse),
|
||||
map(r => r as StrictHttpResponse<R>));
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ApiConfiguration } from './api-configuration';
|
||||
|
||||
/**
|
||||
* Base class for services
|
||||
*/
|
||||
@Injectable()
|
||||
export class BaseService {
|
||||
constructor(
|
||||
protected config: ApiConfiguration,
|
||||
protected http: HttpClient
|
||||
) {
|
||||
}
|
||||
|
||||
private _rootUrl?: string;
|
||||
|
||||
/**
|
||||
* Returns the root url for all operations in this service. If not set directly in this
|
||||
* service, will fallback to `ApiConfiguration.rootUrl`.
|
||||
*/
|
||||
get rootUrl(): string {
|
||||
return this._rootUrl || this.config.rootUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the root URL for API operations in this service.
|
||||
*/
|
||||
set rootUrl(rootUrl: string) {
|
||||
this._rootUrl = rootUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { User } from '../../models/user';
|
||||
|
||||
export interface GetMe$Params {
|
||||
}
|
||||
|
||||
export function getMe(http: HttpClient, rootUrl: string, params?: GetMe$Params, context?: HttpContext): Observable<StrictHttpResponse<User>> {
|
||||
const rb = new RequestBuilder(rootUrl, getMe.PATH, 'get');
|
||||
if (params) {
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<User>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getMe.PATH = '/api/auth/me';
|
||||
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { AuthResponse } from '../../models/auth-response';
|
||||
import { LoginInput } from '../../models/login-input';
|
||||
|
||||
export interface Login$Params {
|
||||
body: LoginInput
|
||||
}
|
||||
|
||||
export function login(http: HttpClient, rootUrl: string, params: Login$Params, context?: HttpContext): Observable<StrictHttpResponse<AuthResponse>> {
|
||||
const rb = new RequestBuilder(rootUrl, login.PATH, 'post');
|
||||
if (params) {
|
||||
rb.body(params.body, 'application/json');
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<AuthResponse>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
login.PATH = '/api/auth/login';
|
||||
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
|
||||
export interface Logout$Params {
|
||||
}
|
||||
|
||||
export function logout(http: HttpClient, rootUrl: string, params?: Logout$Params, context?: HttpContext): Observable<StrictHttpResponse<void>> {
|
||||
const rb = new RequestBuilder(rootUrl, logout.PATH, 'post');
|
||||
if (params) {
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'text', accept: '*/*', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return (r as HttpResponse<any>).clone({ body: undefined }) as StrictHttpResponse<void>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
logout.PATH = '/api/auth/logout';
|
||||
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { AuthResponse } from '../../models/auth-response';
|
||||
import { RegisterInput } from '../../models/register-input';
|
||||
|
||||
export interface Register$Params {
|
||||
body: RegisterInput
|
||||
}
|
||||
|
||||
export function register(http: HttpClient, rootUrl: string, params: Register$Params, context?: HttpContext): Observable<StrictHttpResponse<AuthResponse>> {
|
||||
const rb = new RequestBuilder(rootUrl, register.PATH, 'post');
|
||||
if (params) {
|
||||
rb.body(params.body, 'application/json');
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<AuthResponse>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
register.PATH = '/api/auth/register';
|
||||
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { Game } from '../../models/game';
|
||||
import { GameInput } from '../../models/game-input';
|
||||
|
||||
export interface CreateGame$Params {
|
||||
body: GameInput
|
||||
}
|
||||
|
||||
export function createGame(http: HttpClient, rootUrl: string, params: CreateGame$Params, context?: HttpContext): Observable<StrictHttpResponse<Game>> {
|
||||
const rb = new RequestBuilder(rootUrl, createGame.PATH, 'post');
|
||||
if (params) {
|
||||
rb.body(params.body, 'application/json');
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<Game>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
createGame.PATH = '/api/games';
|
||||
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
|
||||
export interface DeleteGame$Params {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function deleteGame(http: HttpClient, rootUrl: string, params: DeleteGame$Params, context?: HttpContext): Observable<StrictHttpResponse<void>> {
|
||||
const rb = new RequestBuilder(rootUrl, deleteGame.PATH, 'delete');
|
||||
if (params) {
|
||||
rb.path('id', params.id, {});
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'text', accept: '*/*', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return (r as HttpResponse<any>).clone({ body: undefined }) as StrictHttpResponse<void>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteGame.PATH = '/api/games/{id}';
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { Game } from '../../models/game';
|
||||
|
||||
export interface GetGame$Params {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function getGame(http: HttpClient, rootUrl: string, params: GetGame$Params, context?: HttpContext): Observable<StrictHttpResponse<Game>> {
|
||||
const rb = new RequestBuilder(rootUrl, getGame.PATH, 'get');
|
||||
if (params) {
|
||||
rb.path('id', params.id, {});
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<Game>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getGame.PATH = '/api/games/{id}';
|
||||
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { Game } from '../../models/game';
|
||||
|
||||
export interface ListGames$Params {
|
||||
}
|
||||
|
||||
export function listGames(http: HttpClient, rootUrl: string, params?: ListGames$Params, context?: HttpContext): Observable<StrictHttpResponse<(Array<Game> | null)>> {
|
||||
const rb = new RequestBuilder(rootUrl, listGames.PATH, 'get');
|
||||
if (params) {
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<(Array<Game> | null)>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
listGames.PATH = '/api/games';
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { Game } from '../../models/game';
|
||||
import { GameInput } from '../../models/game-input';
|
||||
|
||||
export interface UpdateGame$Params {
|
||||
id: string;
|
||||
body: GameInput
|
||||
}
|
||||
|
||||
export function updateGame(http: HttpClient, rootUrl: string, params: UpdateGame$Params, context?: HttpContext): Observable<StrictHttpResponse<Game>> {
|
||||
const rb = new RequestBuilder(rootUrl, updateGame.PATH, 'put');
|
||||
if (params) {
|
||||
rb.path('id', params.id, {});
|
||||
rb.body(params.body, 'application/json');
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<Game>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateGame.PATH = '/api/games/{id}';
|
||||
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { GetHelloResponse } from '../../models/get-hello-response';
|
||||
|
||||
export interface GetHello$Params {
|
||||
}
|
||||
|
||||
export function getHello(http: HttpClient, rootUrl: string, params?: GetHello$Params, context?: HttpContext): Observable<StrictHttpResponse<GetHelloResponse>> {
|
||||
const rb = new RequestBuilder(rootUrl, getHello.PATH, 'get');
|
||||
if (params) {
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<GetHelloResponse>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getHello.PATH = '/hello';
|
||||
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { Platform } from '../../models/platform';
|
||||
import { PlatformInput } from '../../models/platform-input';
|
||||
|
||||
export interface CreatePlatform$Params {
|
||||
body: PlatformInput
|
||||
}
|
||||
|
||||
export function createPlatform(http: HttpClient, rootUrl: string, params: CreatePlatform$Params, context?: HttpContext): Observable<StrictHttpResponse<Platform>> {
|
||||
const rb = new RequestBuilder(rootUrl, createPlatform.PATH, 'post');
|
||||
if (params) {
|
||||
rb.body(params.body, 'application/json');
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<Platform>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
createPlatform.PATH = '/api/platforms';
|
||||
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
|
||||
export interface DeletePlatform$Params {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function deletePlatform(http: HttpClient, rootUrl: string, params: DeletePlatform$Params, context?: HttpContext): Observable<StrictHttpResponse<void>> {
|
||||
const rb = new RequestBuilder(rootUrl, deletePlatform.PATH, 'delete');
|
||||
if (params) {
|
||||
rb.path('id', params.id, {});
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'text', accept: '*/*', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return (r as HttpResponse<any>).clone({ body: undefined }) as StrictHttpResponse<void>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deletePlatform.PATH = '/api/platforms/{id}';
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { Platform } from '../../models/platform';
|
||||
|
||||
export interface GetPlatform$Params {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function getPlatform(http: HttpClient, rootUrl: string, params: GetPlatform$Params, context?: HttpContext): Observable<StrictHttpResponse<Platform>> {
|
||||
const rb = new RequestBuilder(rootUrl, getPlatform.PATH, 'get');
|
||||
if (params) {
|
||||
rb.path('id', params.id, {});
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<Platform>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getPlatform.PATH = '/api/platforms/{id}';
|
||||
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { Platform } from '../../models/platform';
|
||||
|
||||
export interface ListPlatforms$Params {
|
||||
}
|
||||
|
||||
export function listPlatforms(http: HttpClient, rootUrl: string, params?: ListPlatforms$Params, context?: HttpContext): Observable<StrictHttpResponse<(Array<Platform> | null)>> {
|
||||
const rb = new RequestBuilder(rootUrl, listPlatforms.PATH, 'get');
|
||||
if (params) {
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<(Array<Platform> | null)>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
listPlatforms.PATH = '/api/platforms';
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StrictHttpResponse } from '../../strict-http-response';
|
||||
import { RequestBuilder } from '../../request-builder';
|
||||
|
||||
import { Platform } from '../../models/platform';
|
||||
import { PlatformInput } from '../../models/platform-input';
|
||||
|
||||
export interface UpdatePlatform$Params {
|
||||
id: string;
|
||||
body: PlatformInput
|
||||
}
|
||||
|
||||
export function updatePlatform(http: HttpClient, rootUrl: string, params: UpdatePlatform$Params, context?: HttpContext): Observable<StrictHttpResponse<Platform>> {
|
||||
const rb = new RequestBuilder(rootUrl, updatePlatform.PATH, 'put');
|
||||
if (params) {
|
||||
rb.path('id', params.id, {});
|
||||
rb.body(params.body, 'application/json');
|
||||
}
|
||||
|
||||
return http.request(
|
||||
rb.build({ responseType: 'json', accept: 'application/json', context })
|
||||
).pipe(
|
||||
filter((r: any): r is HttpResponse<any> => r instanceof HttpResponse),
|
||||
map((r: HttpResponse<any>) => {
|
||||
return r as StrictHttpResponse<Platform>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updatePlatform.PATH = '/api/platforms/{id}';
|
||||
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export type { Login$Params as Login$Params } from './fn/auth/login';
|
||||
export { login as login } from './fn/auth/login';
|
||||
export type { Logout$Params as Logout$Params } from './fn/auth/logout';
|
||||
export { logout as logout } from './fn/auth/logout';
|
||||
export type { GetMe$Params as GetMe$Params } from './fn/auth/get-me';
|
||||
export { getMe as getMe } from './fn/auth/get-me';
|
||||
export type { Register$Params as Register$Params } from './fn/auth/register';
|
||||
export { register as register } from './fn/auth/register';
|
||||
export type { ListGames$Params as ListGames$Params } from './fn/games/list-games';
|
||||
export { listGames as listGames } from './fn/games/list-games';
|
||||
export type { CreateGame$Params as CreateGame$Params } from './fn/games/create-game';
|
||||
export { createGame as createGame } from './fn/games/create-game';
|
||||
export type { GetGame$Params as GetGame$Params } from './fn/games/get-game';
|
||||
export { getGame as getGame } from './fn/games/get-game';
|
||||
export type { UpdateGame$Params as UpdateGame$Params } from './fn/games/update-game';
|
||||
export { updateGame as updateGame } from './fn/games/update-game';
|
||||
export type { DeleteGame$Params as DeleteGame$Params } from './fn/games/delete-game';
|
||||
export { deleteGame as deleteGame } from './fn/games/delete-game';
|
||||
export type { ListPlatforms$Params as ListPlatforms$Params } from './fn/platforms/list-platforms';
|
||||
export { listPlatforms as listPlatforms } from './fn/platforms/list-platforms';
|
||||
export type { CreatePlatform$Params as CreatePlatform$Params } from './fn/platforms/create-platform';
|
||||
export { createPlatform as createPlatform } from './fn/platforms/create-platform';
|
||||
export type { GetPlatform$Params as GetPlatform$Params } from './fn/platforms/get-platform';
|
||||
export { getPlatform as getPlatform } from './fn/platforms/get-platform';
|
||||
export type { UpdatePlatform$Params as UpdatePlatform$Params } from './fn/platforms/update-platform';
|
||||
export { updatePlatform as updatePlatform } from './fn/platforms/update-platform';
|
||||
export type { DeletePlatform$Params as DeletePlatform$Params } from './fn/platforms/delete-platform';
|
||||
export { deletePlatform as deletePlatform } from './fn/platforms/delete-platform';
|
||||
export type { GetHello$Params as GetHello$Params } from './fn/misc/get-hello';
|
||||
export { getHello as getHello } from './fn/misc/get-hello';
|
||||
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export type { AuthResponse } from './models/auth-response';
|
||||
export type { ErrorDetail } from './models/error-detail';
|
||||
export type { ErrorModel } from './models/error-model';
|
||||
export type { Game } from './models/game';
|
||||
export type { GameInput } from './models/game-input';
|
||||
export type { GetHelloResponse } from './models/get-hello-response';
|
||||
export type { LoginInput } from './models/login-input';
|
||||
export type { Platform } from './models/platform';
|
||||
export type { PlatformInput } from './models/platform-input';
|
||||
export type { RegisterInput } from './models/register-input';
|
||||
export type { User } from './models/user';
|
||||
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { User } from '../models/user';
|
||||
export interface AuthResponse {
|
||||
|
||||
/**
|
||||
* A URL to the JSON Schema for this object.
|
||||
*/
|
||||
'$schema'?: string;
|
||||
token: string;
|
||||
user: User;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export interface ErrorDetail {
|
||||
|
||||
/**
|
||||
* Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'
|
||||
*/
|
||||
location?: string;
|
||||
|
||||
/**
|
||||
* Error message text
|
||||
*/
|
||||
message?: string;
|
||||
|
||||
/**
|
||||
* The value at the given location
|
||||
*/
|
||||
value?: any;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { ErrorDetail } from '../models/error-detail';
|
||||
export interface ErrorModel {
|
||||
|
||||
/**
|
||||
* A URL to the JSON Schema for this object.
|
||||
*/
|
||||
'$schema'?: string;
|
||||
|
||||
/**
|
||||
* A human-readable explanation specific to this occurrence of the problem.
|
||||
*/
|
||||
detail?: string;
|
||||
|
||||
/**
|
||||
* Optional list of individual error details
|
||||
*/
|
||||
errors?: (Array<ErrorDetail> | null);
|
||||
|
||||
/**
|
||||
* A URI reference that identifies the specific occurrence of the problem.
|
||||
*/
|
||||
instance?: string;
|
||||
|
||||
/**
|
||||
* HTTP status code
|
||||
*/
|
||||
status?: number;
|
||||
|
||||
/**
|
||||
* A short, human-readable summary of the problem type. This value should not change between occurrences of the error.
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* A URI reference to human-readable documentation for the error.
|
||||
*/
|
||||
type?: string;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export interface GameInput {
|
||||
|
||||
/**
|
||||
* A URL to the JSON Schema for this object.
|
||||
*/
|
||||
'$schema'?: string;
|
||||
|
||||
/**
|
||||
* YYYY-MM-DD (optional)
|
||||
*/
|
||||
finished?: string;
|
||||
name: string;
|
||||
platform_id: string;
|
||||
|
||||
/**
|
||||
* YYYY-MM-DD
|
||||
*/
|
||||
release_date: string;
|
||||
score: number;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export interface Game {
|
||||
|
||||
/**
|
||||
* A URL to the JSON Schema for this object.
|
||||
*/
|
||||
'$schema'?: string;
|
||||
|
||||
/**
|
||||
* YYYY-MM-DD (optional)
|
||||
*/
|
||||
finished?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
platform_id: string;
|
||||
platform_name: string;
|
||||
|
||||
/**
|
||||
* YYYY-MM-DD
|
||||
*/
|
||||
release_date: string;
|
||||
score: number;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export interface GetHelloResponse {
|
||||
|
||||
/**
|
||||
* A URL to the JSON Schema for this object.
|
||||
*/
|
||||
'$schema'?: string;
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export interface LoginInput {
|
||||
|
||||
/**
|
||||
* A URL to the JSON Schema for this object.
|
||||
*/
|
||||
'$schema'?: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export interface PlatformInput {
|
||||
|
||||
/**
|
||||
* A URL to the JSON Schema for this object.
|
||||
*/
|
||||
'$schema'?: string;
|
||||
|
||||
/**
|
||||
* Name of the platform
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export interface Platform {
|
||||
|
||||
/**
|
||||
* A URL to the JSON Schema for this object.
|
||||
*/
|
||||
'$schema'?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export interface RegisterInput {
|
||||
|
||||
/**
|
||||
* A URL to the JSON Schema for this object.
|
||||
*/
|
||||
'$schema'?: string;
|
||||
email: string;
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export interface User {
|
||||
|
||||
/**
|
||||
* A URL to the JSON Schema for this object.
|
||||
*/
|
||||
'$schema'?: string;
|
||||
bio?: string;
|
||||
email: string;
|
||||
id: string;
|
||||
is_admin: boolean;
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpRequest, HttpParameterCodec, HttpParams, HttpHeaders, HttpContext } from '@angular/common/http';
|
||||
|
||||
/**
|
||||
* Custom parameter codec to correctly handle the plus sign in parameter
|
||||
* values. See https://github.com/angular/angular/issues/18261
|
||||
*/
|
||||
class ParameterCodec implements HttpParameterCodec {
|
||||
encodeKey(key: string): string {
|
||||
return encodeURIComponent(key);
|
||||
}
|
||||
|
||||
encodeValue(value: string): string {
|
||||
return encodeURIComponent(value);
|
||||
}
|
||||
|
||||
decodeKey(key: string): string {
|
||||
return decodeURIComponent(key);
|
||||
}
|
||||
|
||||
decodeValue(value: string): string {
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
const ParameterCodecInstance = new ParameterCodec();
|
||||
|
||||
/**
|
||||
* Defines the options for appending a parameter
|
||||
*/
|
||||
interface ParameterOptions {
|
||||
style?: string;
|
||||
explode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for a parameter
|
||||
*/
|
||||
abstract class Parameter {
|
||||
constructor(public name: string, public value: any, public options: ParameterOptions, defaultStyle: string, defaultExplode: boolean) {
|
||||
this.options = options || {};
|
||||
if (this.options.style === null || this.options.style === undefined) {
|
||||
this.options.style = defaultStyle;
|
||||
}
|
||||
if (this.options.explode === null || this.options.explode === undefined) {
|
||||
this.options.explode = defaultExplode;
|
||||
}
|
||||
}
|
||||
|
||||
serializeValue(value: any, separator = ','): string {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
} else if (value instanceof Array) {
|
||||
return value.map(v => this.serializeValue(v).split(separator).join(encodeURIComponent(separator))).join(separator);
|
||||
} else if (typeof value === 'object') {
|
||||
const array: string[] = [];
|
||||
for (const key of Object.keys(value)) {
|
||||
let propVal = value[key];
|
||||
if (propVal !== null && propVal !== undefined) {
|
||||
propVal = this.serializeValue(propVal).split(separator).join(encodeURIComponent(separator));
|
||||
if (this.options.explode) {
|
||||
array.push(`${key}=${propVal}`);
|
||||
} else {
|
||||
array.push(key);
|
||||
array.push(propVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
return array.join(separator);
|
||||
} else {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parameter in the operation path
|
||||
*/
|
||||
class PathParameter extends Parameter {
|
||||
constructor(name: string, value: any, options: ParameterOptions) {
|
||||
super(name, value, options, 'simple', false);
|
||||
}
|
||||
|
||||
append(path: string): string {
|
||||
let value = this.value;
|
||||
if (value === null || value === undefined) {
|
||||
value = '';
|
||||
}
|
||||
let prefix = this.options.style === 'label' ? '.' : '';
|
||||
let separator = this.options.explode ? prefix === '' ? ',' : prefix : ',';
|
||||
let alreadySerialized = false;
|
||||
if (this.options.style === 'matrix') {
|
||||
// The parameter name is just used as prefix, except in some cases...
|
||||
prefix = `;${this.name}=`;
|
||||
if (this.options.explode && typeof value === 'object') {
|
||||
prefix = ';';
|
||||
if (value instanceof Array) {
|
||||
// For arrays we have to repeat the name for each element
|
||||
value = value.map(v => `${this.name}=${this.serializeValue(v, ';')}`);
|
||||
value = value.join(';');
|
||||
alreadySerialized = true;
|
||||
} else {
|
||||
// For objects we have to put each the key / value pairs
|
||||
value = this.serializeValue(value, ';');
|
||||
alreadySerialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
value = prefix + (alreadySerialized ? value : this.serializeValue(value, separator));
|
||||
// Replace both the plain variable and the corresponding variant taking in the prefix and explode into account
|
||||
path = path.replace(`{${this.name}}`, value);
|
||||
path = path.replace(`{${prefix}${this.name}${this.options.explode ? '*' : ''}}`, value);
|
||||
return path;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
serializeValue(value: any, separator = ','): string {
|
||||
var result = typeof value === 'string' ? encodeURIComponent(value) : super.serializeValue(value, separator);
|
||||
result = result.replace(/%3D/g, '=');
|
||||
result = result.replace(/%3B/g, ';');
|
||||
result = result.replace(/%2C/g, ',');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parameter in the query
|
||||
*/
|
||||
class QueryParameter extends Parameter {
|
||||
constructor(name: string, value: any, options: ParameterOptions) {
|
||||
super(name, value, options, 'form', true);
|
||||
}
|
||||
|
||||
append(params: HttpParams): HttpParams {
|
||||
if (this.value instanceof Array) {
|
||||
// Array serialization
|
||||
if (this.options.explode) {
|
||||
for (const v of this.value) {
|
||||
params = params.append(this.name, this.serializeValue(v));
|
||||
}
|
||||
} else {
|
||||
const separator = this.options.style === 'spaceDelimited'
|
||||
? ' ' : this.options.style === 'pipeDelimited'
|
||||
? '|' : ',';
|
||||
return params.append(this.name, this.serializeValue(this.value, separator));
|
||||
}
|
||||
} else if (this.value !== null && typeof this.value === 'object') {
|
||||
// Object serialization
|
||||
if (this.options.style === 'deepObject') {
|
||||
// Append a parameter for each key, in the form `name[key]`
|
||||
for (const key of Object.keys(this.value)) {
|
||||
const propVal = this.value[key];
|
||||
if (propVal !== null && propVal !== undefined) {
|
||||
params = params.append(`${this.name}[${key}]`, this.serializeValue(propVal));
|
||||
}
|
||||
}
|
||||
} else if (this.options.explode) {
|
||||
// Append a parameter for each key without using the parameter name
|
||||
for (const key of Object.keys(this.value)) {
|
||||
const propVal = this.value[key];
|
||||
if (propVal !== null && propVal !== undefined) {
|
||||
params = params.append(key, this.serializeValue(propVal));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Append a single parameter whose values are a comma-separated list of key,value,key,value...
|
||||
const array: any[] = [];
|
||||
for (const key of Object.keys(this.value)) {
|
||||
const propVal = this.value[key];
|
||||
if (propVal !== null && propVal !== undefined) {
|
||||
array.push(key);
|
||||
array.push(propVal);
|
||||
}
|
||||
}
|
||||
params = params.append(this.name, this.serializeValue(array));
|
||||
}
|
||||
} else if (this.value !== null && this.value !== undefined) {
|
||||
// Plain value
|
||||
params = params.append(this.name, this.serializeValue(this.value));
|
||||
}
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parameter in the HTTP request header
|
||||
*/
|
||||
class HeaderParameter extends Parameter {
|
||||
constructor(name: string, value: any, options: ParameterOptions) {
|
||||
super(name, value, options, 'simple', false);
|
||||
}
|
||||
|
||||
append(headers: HttpHeaders): HttpHeaders {
|
||||
if (this.value !== null && this.value !== undefined) {
|
||||
if (this.value instanceof Array) {
|
||||
for (const v of this.value) {
|
||||
headers = headers.append(this.name, this.serializeValue(v));
|
||||
}
|
||||
} else {
|
||||
headers = headers.append(this.name, this.serializeValue(this.value));
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to build http requests from parameters
|
||||
*/
|
||||
export class RequestBuilder {
|
||||
|
||||
private _path = new Map<string, PathParameter>();
|
||||
private _query = new Map<string, QueryParameter>();
|
||||
private _header = new Map<string, HeaderParameter>();
|
||||
_bodyContent: any | null;
|
||||
_bodyContentType?: string;
|
||||
|
||||
constructor(
|
||||
public rootUrl: string,
|
||||
public operationPath: string,
|
||||
public method: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a path parameter
|
||||
*/
|
||||
path(name: string, value: any, options?: ParameterOptions): void {
|
||||
this._path.set(name, new PathParameter(name, value, options || {}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a query parameter
|
||||
*/
|
||||
query(name: string, value: any, options?: ParameterOptions): void {
|
||||
this._query.set(name, new QueryParameter(name, value, options || {}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a header parameter
|
||||
*/
|
||||
header(name: string, value: any, options?: ParameterOptions): void {
|
||||
this._header.set(name, new HeaderParameter(name, value, options || {}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the body content, along with the content type
|
||||
*/
|
||||
body(value: any, contentType = 'application/json'): void {
|
||||
if (value instanceof Blob) {
|
||||
this._bodyContentType = value.type;
|
||||
} else {
|
||||
this._bodyContentType = contentType;
|
||||
}
|
||||
if (this._bodyContentType === 'application/x-www-form-urlencoded' && value !== null && typeof value === 'object') {
|
||||
// Handle URL-encoded data
|
||||
const pairs: Array<[string, string]> = [];
|
||||
for (const key of Object.keys(value)) {
|
||||
let val = value[key];
|
||||
if (!(val instanceof Array)) {
|
||||
val = [val];
|
||||
}
|
||||
for (const v of val) {
|
||||
const formValue = this.formDataValue(v);
|
||||
if (formValue !== null) {
|
||||
pairs.push([key, formValue]);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._bodyContent = pairs.map(p => `${encodeURIComponent(p[0])}=${encodeURIComponent(p[1])}`).join('&');
|
||||
} else if (this._bodyContentType === 'multipart/form-data') {
|
||||
// Handle multipart form data
|
||||
const formData = new FormData();
|
||||
if (value !== null && value !== undefined) {
|
||||
for (const key of Object.keys(value)) {
|
||||
const val = value[key];
|
||||
if (val instanceof Array) {
|
||||
for (const v of val) {
|
||||
const toAppend = this.formDataValue(v);
|
||||
if (toAppend !== null) {
|
||||
formData.append(key, toAppend);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const toAppend = this.formDataValue(val);
|
||||
if (toAppend !== null) {
|
||||
formData.set(key, toAppend);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._bodyContent = formData;
|
||||
} else {
|
||||
// The body is the plain content
|
||||
this._bodyContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
private formDataValue(value: any): any {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Blob) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return new Blob([JSON.stringify(value)], {type: 'application/json'})
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the request with the current set parameters
|
||||
*/
|
||||
build<T = any>(options?: {
|
||||
/** Which content types to accept */
|
||||
accept?: string;
|
||||
|
||||
/** The expected response type */
|
||||
responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
|
||||
|
||||
/** Whether to report progress on uploads / downloads */
|
||||
reportProgress?: boolean;
|
||||
|
||||
/** Allow passing HttpContext for HttpClient */
|
||||
context?: HttpContext;
|
||||
}): HttpRequest<T> {
|
||||
|
||||
options = options || {};
|
||||
|
||||
// Path parameters
|
||||
let path = this.operationPath;
|
||||
for (const pathParam of this._path.values()) {
|
||||
path = pathParam.append(path);
|
||||
}
|
||||
const url = this.rootUrl + path;
|
||||
|
||||
// Query parameters
|
||||
let httpParams = new HttpParams({
|
||||
encoder: ParameterCodecInstance
|
||||
});
|
||||
for (const queryParam of this._query.values()) {
|
||||
httpParams = queryParam.append(httpParams);
|
||||
}
|
||||
|
||||
// Header parameters
|
||||
let httpHeaders = new HttpHeaders();
|
||||
if (options.accept) {
|
||||
httpHeaders = httpHeaders.append('Accept', options.accept);
|
||||
}
|
||||
for (const headerParam of this._header.values()) {
|
||||
httpHeaders = headerParam.append(httpHeaders);
|
||||
}
|
||||
|
||||
// Request content headers
|
||||
if (this._bodyContentType && !(this._bodyContent instanceof FormData)) {
|
||||
httpHeaders = httpHeaders.set('Content-Type', this._bodyContentType);
|
||||
}
|
||||
|
||||
// Perform the request
|
||||
return new HttpRequest<T>(this.method.toUpperCase(), url, this._bodyContent, {
|
||||
params: httpParams,
|
||||
headers: httpHeaders,
|
||||
responseType: options.responseType,
|
||||
reportProgress: options.reportProgress,
|
||||
context: options.context
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
export { AuthService } from './services/auth.service';
|
||||
export { GamesService } from './services/games.service';
|
||||
export { PlatformsService } from './services/platforms.service';
|
||||
export { MiscService } from './services/misc.service';
|
||||
@@ -0,0 +1,161 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { BaseService } from '../base-service';
|
||||
import { ApiConfiguration } from '../api-configuration';
|
||||
import { StrictHttpResponse } from '../strict-http-response';
|
||||
|
||||
import { AuthResponse } from '../models/auth-response';
|
||||
import { getMe } from '../fn/auth/get-me';
|
||||
import { GetMe$Params } from '../fn/auth/get-me';
|
||||
import { login } from '../fn/auth/login';
|
||||
import { Login$Params } from '../fn/auth/login';
|
||||
import { logout } from '../fn/auth/logout';
|
||||
import { Logout$Params } from '../fn/auth/logout';
|
||||
import { register } from '../fn/auth/register';
|
||||
import { Register$Params } from '../fn/auth/register';
|
||||
import { User } from '../models/user';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService extends BaseService {
|
||||
constructor(config: ApiConfiguration, http: HttpClient) {
|
||||
super(config, http);
|
||||
}
|
||||
|
||||
/** Path part for operation `login()` */
|
||||
static readonly LoginPath = '/api/auth/login';
|
||||
|
||||
/**
|
||||
* Log in to an existing account.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `login()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
login$Response(params: Login$Params, context?: HttpContext): Promise<StrictHttpResponse<AuthResponse>> {
|
||||
const obs = login(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in to an existing account.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `login$Response()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
login(params: Login$Params, context?: HttpContext): Promise<AuthResponse> {
|
||||
const resp = this.login$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<AuthResponse>): AuthResponse => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `logout()` */
|
||||
static readonly LogoutPath = '/api/auth/logout';
|
||||
|
||||
/**
|
||||
* Log out of the current session.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `logout()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
logout$Response(params?: Logout$Params, context?: HttpContext): Promise<StrictHttpResponse<void>> {
|
||||
const obs = logout(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log out of the current session.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `logout$Response()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
logout(params?: Logout$Params, context?: HttpContext): Promise<void> {
|
||||
const resp = this.logout$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<void>): void => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `getMe()` */
|
||||
static readonly GetMePath = '/api/auth/me';
|
||||
|
||||
/**
|
||||
* Get the current user.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `getMe()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
getMe$Response(params?: GetMe$Params, context?: HttpContext): Promise<StrictHttpResponse<User>> {
|
||||
const obs = getMe(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `getMe$Response()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
getMe(params?: GetMe$Params, context?: HttpContext): Promise<User> {
|
||||
const resp = this.getMe$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<User>): User => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `register()` */
|
||||
static readonly RegisterPath = '/api/auth/register';
|
||||
|
||||
/**
|
||||
* Register a new account.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `register()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
register$Response(params: Register$Params, context?: HttpContext): Promise<StrictHttpResponse<AuthResponse>> {
|
||||
const obs = register(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new account.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `register$Response()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
register(params: Register$Params, context?: HttpContext): Promise<AuthResponse> {
|
||||
const resp = this.register$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<AuthResponse>): AuthResponse => r.body);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { BaseService } from '../base-service';
|
||||
import { ApiConfiguration } from '../api-configuration';
|
||||
import { StrictHttpResponse } from '../strict-http-response';
|
||||
|
||||
import { createGame } from '../fn/games/create-game';
|
||||
import { CreateGame$Params } from '../fn/games/create-game';
|
||||
import { deleteGame } from '../fn/games/delete-game';
|
||||
import { DeleteGame$Params } from '../fn/games/delete-game';
|
||||
import { Game } from '../models/game';
|
||||
import { getGame } from '../fn/games/get-game';
|
||||
import { GetGame$Params } from '../fn/games/get-game';
|
||||
import { listGames } from '../fn/games/list-games';
|
||||
import { ListGames$Params } from '../fn/games/list-games';
|
||||
import { updateGame } from '../fn/games/update-game';
|
||||
import { UpdateGame$Params } from '../fn/games/update-game';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class GamesService extends BaseService {
|
||||
constructor(config: ApiConfiguration, http: HttpClient) {
|
||||
super(config, http);
|
||||
}
|
||||
|
||||
/** Path part for operation `listGames()` */
|
||||
static readonly ListGamesPath = '/api/games';
|
||||
|
||||
/**
|
||||
* List Games.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `listGames()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
listGames$Response(params?: ListGames$Params, context?: HttpContext): Promise<StrictHttpResponse<(Array<Game> | null)>> {
|
||||
const obs = listGames(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* List Games.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `listGames$Response()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
listGames(params?: ListGames$Params, context?: HttpContext): Promise<(Array<Game> | null)> {
|
||||
const resp = this.listGames$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<(Array<Game> | null)>): (Array<Game> | null) => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `createGame()` */
|
||||
static readonly CreateGamePath = '/api/games';
|
||||
|
||||
/**
|
||||
* Create Game.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `createGame()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
createGame$Response(params: CreateGame$Params, context?: HttpContext): Promise<StrictHttpResponse<Game>> {
|
||||
const obs = createGame(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Game.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `createGame$Response()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
createGame(params: CreateGame$Params, context?: HttpContext): Promise<Game> {
|
||||
const resp = this.createGame$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<Game>): Game => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `getGame()` */
|
||||
static readonly GetGamePath = '/api/games/{id}';
|
||||
|
||||
/**
|
||||
* Get Game.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `getGame()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
getGame$Response(params: GetGame$Params, context?: HttpContext): Promise<StrictHttpResponse<Game>> {
|
||||
const obs = getGame(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Game.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `getGame$Response()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
getGame(params: GetGame$Params, context?: HttpContext): Promise<Game> {
|
||||
const resp = this.getGame$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<Game>): Game => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `updateGame()` */
|
||||
static readonly UpdateGamePath = '/api/games/{id}';
|
||||
|
||||
/**
|
||||
* Update Game.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `updateGame()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
updateGame$Response(params: UpdateGame$Params, context?: HttpContext): Promise<StrictHttpResponse<Game>> {
|
||||
const obs = updateGame(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Game.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `updateGame$Response()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
updateGame(params: UpdateGame$Params, context?: HttpContext): Promise<Game> {
|
||||
const resp = this.updateGame$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<Game>): Game => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `deleteGame()` */
|
||||
static readonly DeleteGamePath = '/api/games/{id}';
|
||||
|
||||
/**
|
||||
* Delete Game.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `deleteGame()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
deleteGame$Response(params: DeleteGame$Params, context?: HttpContext): Promise<StrictHttpResponse<void>> {
|
||||
const obs = deleteGame(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Game.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `deleteGame$Response()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
deleteGame(params: DeleteGame$Params, context?: HttpContext): Promise<void> {
|
||||
const resp = this.deleteGame$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<void>): void => r.body);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { BaseService } from '../base-service';
|
||||
import { ApiConfiguration } from '../api-configuration';
|
||||
import { StrictHttpResponse } from '../strict-http-response';
|
||||
|
||||
import { GetHelloResponse } from '../models/get-hello-response';
|
||||
import { getHello } from '../fn/misc/get-hello';
|
||||
import { GetHello$Params } from '../fn/misc/get-hello';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MiscService extends BaseService {
|
||||
constructor(config: ApiConfiguration, http: HttpClient) {
|
||||
super(config, http);
|
||||
}
|
||||
|
||||
/** Path part for operation `getHello()` */
|
||||
static readonly GetHelloPath = '/hello';
|
||||
|
||||
/**
|
||||
* Say Hello.
|
||||
*
|
||||
* Returns a friendly hello message.
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `getHello()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
getHello$Response(params?: GetHello$Params, context?: HttpContext): Promise<StrictHttpResponse<GetHelloResponse>> {
|
||||
const obs = getHello(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Say Hello.
|
||||
*
|
||||
* Returns a friendly hello message.
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `getHello$Response()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
getHello(params?: GetHello$Params, context?: HttpContext): Promise<GetHelloResponse> {
|
||||
const resp = this.getHello$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<GetHelloResponse>): GetHelloResponse => r.body);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpClient, HttpContext } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { BaseService } from '../base-service';
|
||||
import { ApiConfiguration } from '../api-configuration';
|
||||
import { StrictHttpResponse } from '../strict-http-response';
|
||||
|
||||
import { createPlatform } from '../fn/platforms/create-platform';
|
||||
import { CreatePlatform$Params } from '../fn/platforms/create-platform';
|
||||
import { deletePlatform } from '../fn/platforms/delete-platform';
|
||||
import { DeletePlatform$Params } from '../fn/platforms/delete-platform';
|
||||
import { getPlatform } from '../fn/platforms/get-platform';
|
||||
import { GetPlatform$Params } from '../fn/platforms/get-platform';
|
||||
import { listPlatforms } from '../fn/platforms/list-platforms';
|
||||
import { ListPlatforms$Params } from '../fn/platforms/list-platforms';
|
||||
import { Platform } from '../models/platform';
|
||||
import { updatePlatform } from '../fn/platforms/update-platform';
|
||||
import { UpdatePlatform$Params } from '../fn/platforms/update-platform';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PlatformsService extends BaseService {
|
||||
constructor(config: ApiConfiguration, http: HttpClient) {
|
||||
super(config, http);
|
||||
}
|
||||
|
||||
/** Path part for operation `listPlatforms()` */
|
||||
static readonly ListPlatformsPath = '/api/platforms';
|
||||
|
||||
/**
|
||||
* List Platforms.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `listPlatforms()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
listPlatforms$Response(params?: ListPlatforms$Params, context?: HttpContext): Promise<StrictHttpResponse<(Array<Platform> | null)>> {
|
||||
const obs = listPlatforms(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* List Platforms.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `listPlatforms$Response()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
listPlatforms(params?: ListPlatforms$Params, context?: HttpContext): Promise<(Array<Platform> | null)> {
|
||||
const resp = this.listPlatforms$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<(Array<Platform> | null)>): (Array<Platform> | null) => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `createPlatform()` */
|
||||
static readonly CreatePlatformPath = '/api/platforms';
|
||||
|
||||
/**
|
||||
* Create Platform.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `createPlatform()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
createPlatform$Response(params: CreatePlatform$Params, context?: HttpContext): Promise<StrictHttpResponse<Platform>> {
|
||||
const obs = createPlatform(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Platform.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `createPlatform$Response()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
createPlatform(params: CreatePlatform$Params, context?: HttpContext): Promise<Platform> {
|
||||
const resp = this.createPlatform$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<Platform>): Platform => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `getPlatform()` */
|
||||
static readonly GetPlatformPath = '/api/platforms/{id}';
|
||||
|
||||
/**
|
||||
* Get Platform.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `getPlatform()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
getPlatform$Response(params: GetPlatform$Params, context?: HttpContext): Promise<StrictHttpResponse<Platform>> {
|
||||
const obs = getPlatform(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Platform.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `getPlatform$Response()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
getPlatform(params: GetPlatform$Params, context?: HttpContext): Promise<Platform> {
|
||||
const resp = this.getPlatform$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<Platform>): Platform => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `updatePlatform()` */
|
||||
static readonly UpdatePlatformPath = '/api/platforms/{id}';
|
||||
|
||||
/**
|
||||
* Update Platform.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `updatePlatform()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
updatePlatform$Response(params: UpdatePlatform$Params, context?: HttpContext): Promise<StrictHttpResponse<Platform>> {
|
||||
const obs = updatePlatform(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Platform.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `updatePlatform$Response()` instead.
|
||||
*
|
||||
* This method sends `application/json` and handles request body of type `application/json`.
|
||||
*/
|
||||
updatePlatform(params: UpdatePlatform$Params, context?: HttpContext): Promise<Platform> {
|
||||
const resp = this.updatePlatform$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<Platform>): Platform => r.body);
|
||||
}
|
||||
|
||||
/** Path part for operation `deletePlatform()` */
|
||||
static readonly DeletePlatformPath = '/api/platforms/{id}';
|
||||
|
||||
/**
|
||||
* Delete Platform.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access to the full `HttpResponse`, allowing access to response headers.
|
||||
* To access only the response body, use `deletePlatform()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
deletePlatform$Response(params: DeletePlatform$Params, context?: HttpContext): Promise<StrictHttpResponse<void>> {
|
||||
const obs = deletePlatform(this.http, this.rootUrl, params, context);
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Platform.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method provides access only to the response body.
|
||||
* To access the full response (for headers, for example), `deletePlatform$Response()` instead.
|
||||
*
|
||||
* This method doesn't expect any request body.
|
||||
*/
|
||||
deletePlatform(params: DeletePlatform$Params, context?: HttpContext): Promise<void> {
|
||||
const resp = this.deletePlatform$Response(params, context);
|
||||
return resp.then((r: StrictHttpResponse<void>): void => r.body);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable */
|
||||
/* Code generated by ng-openapi-gen DO NOT EDIT. */
|
||||
|
||||
import { HttpResponse } from '@angular/common/http';
|
||||
|
||||
/**
|
||||
* Constrains the http response to not have the body defined as `T | null`, but `T` only.
|
||||
*/
|
||||
export type StrictHttpResponse<T> = HttpResponse<T> & {
|
||||
readonly body: T;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { authInterceptor } from './core/auth-interceptor';
|
||||
import { provideApiConfiguration } from './api/api-configuration';
|
||||
|
||||
// The API's base URL. There's no server-side config to read it from at
|
||||
// runtime in this small base (a plain SPA build), so it's a build-time
|
||||
// constant; swap to Angular's fileReplacements if a later build needs a
|
||||
// different URL per environment.
|
||||
const API_BASE_URL = 'http://localhost:8080';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideAnimationsAsync(),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(withInterceptors([authInterceptor])),
|
||||
provideApiConfiguration(API_BASE_URL),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from './core/auth-guard';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'games' },
|
||||
{
|
||||
path: 'login',
|
||||
loadComponent: () => import('./features/login/login').then((m) => m.Login),
|
||||
},
|
||||
{
|
||||
path: 'games',
|
||||
loadComponent: () => import('./features/games/games').then((m) => m.Games),
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,18 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { App } from './app';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
providers: [provideRouter([])],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
expect(fixture.componentInstance).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
imports: [RouterOutlet],
|
||||
selector: 'app-root',
|
||||
template: '<router-outlet />',
|
||||
})
|
||||
export class App {}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { Router, UrlTree } from '@angular/router';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { authGuard } from './auth-guard';
|
||||
import { Session } from './session';
|
||||
import { provideApiConfiguration } from '../api/api-configuration';
|
||||
|
||||
describe('authGuard', () => {
|
||||
let session: Session;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
provideApiConfiguration('http://api.test'),
|
||||
provideRouter([]),
|
||||
],
|
||||
});
|
||||
session = TestBed.inject(Session);
|
||||
router = TestBed.inject(Router);
|
||||
});
|
||||
|
||||
function runGuard() {
|
||||
return TestBed.runInInjectionContext(() => authGuard({} as any, {} as any));
|
||||
}
|
||||
|
||||
it('allows navigation when authenticated', () => {
|
||||
session.token.set('tok-abc');
|
||||
expect(runGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it('redirects to /login when not authenticated', () => {
|
||||
const result = runGuard();
|
||||
expect(result).not.toBe(true);
|
||||
expect((result as UrlTree).toString()).toBe(router.createUrlTree(['/login']).toString());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
|
||||
import { Session } from './session';
|
||||
|
||||
/** Redirects to /login unless a session token is present. */
|
||||
export const authGuard: CanActivateFn = () => {
|
||||
if (inject(Session).isAuthenticated()) {
|
||||
return true;
|
||||
}
|
||||
return inject(Router).createUrlTree(['/login']);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
|
||||
import { authInterceptor } from './auth-interceptor';
|
||||
import { Session } from './session';
|
||||
import { provideApiConfiguration } from '../api/api-configuration';
|
||||
|
||||
describe('authInterceptor', () => {
|
||||
let http: HttpClient;
|
||||
let httpMock: HttpTestingController;
|
||||
let session: Session;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(withInterceptors([authInterceptor])),
|
||||
provideHttpClientTesting(),
|
||||
provideApiConfiguration('http://api.test'),
|
||||
],
|
||||
});
|
||||
http = TestBed.inject(HttpClient);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
session = TestBed.inject(Session);
|
||||
});
|
||||
|
||||
afterEach(() => httpMock.verify());
|
||||
|
||||
it('attaches the bearer token when a session is present', () => {
|
||||
session.token.set('tok-abc');
|
||||
|
||||
http.get('/whatever').subscribe();
|
||||
|
||||
const req = httpMock.expectOne('/whatever');
|
||||
expect(req.request.headers.get('Authorization')).toBe('Bearer tok-abc');
|
||||
});
|
||||
|
||||
it('sends no Authorization header when logged out', () => {
|
||||
http.get('/whatever').subscribe();
|
||||
|
||||
const req = httpMock.expectOne('/whatever');
|
||||
expect(req.request.headers.has('Authorization')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
|
||||
import { Session } from './session';
|
||||
|
||||
/** Attaches the stored session token to every outgoing API request. */
|
||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const token = inject(Session).token();
|
||||
if (!token) {
|
||||
return next(req);
|
||||
}
|
||||
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
|
||||
import { Session } from './session';
|
||||
import { provideApiConfiguration } from '../api/api-configuration';
|
||||
|
||||
describe('Session', () => {
|
||||
let service: Session;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideHttpClientTesting(), provideApiConfiguration('http://api.test')],
|
||||
});
|
||||
service = TestBed.inject(Session);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('starts logged out with no stored token', () => {
|
||||
expect(service.isAuthenticated()).toBe(false);
|
||||
expect(service.user()).toBeNull();
|
||||
});
|
||||
|
||||
it('restores the token from a previous session', () => {
|
||||
localStorage.setItem('completed.token', 'tok-123');
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideHttpClientTesting(), provideApiConfiguration('http://api.test')],
|
||||
});
|
||||
const restored = TestBed.inject(Session);
|
||||
expect(restored.isAuthenticated()).toBe(true);
|
||||
});
|
||||
|
||||
it('login stores the token and user, and persists to localStorage', async () => {
|
||||
const promise = service.login('alice@example.com', 'secret');
|
||||
|
||||
const req = httpMock.expectOne('http://api.test/api/auth/login');
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual({ email: 'alice@example.com', password: 'secret' });
|
||||
req.flush({ token: 'tok-abc', user: { id: 'u1', email: 'alice@example.com', name: 'Alice', is_admin: false } });
|
||||
|
||||
await promise;
|
||||
|
||||
expect(service.token()).toBe('tok-abc');
|
||||
expect(service.user()?.name).toBe('Alice');
|
||||
expect(service.isAdmin()).toBe(false);
|
||||
expect(localStorage.getItem('completed.token')).toBe('tok-abc');
|
||||
});
|
||||
|
||||
it('login failure leaves the session logged out', async () => {
|
||||
const promise = service.login('alice@example.com', 'wrong');
|
||||
|
||||
const req = httpMock.expectOne('http://api.test/api/auth/login');
|
||||
req.flush({ title: 'Unauthorized', status: 401, detail: 'invalid email or password' }, { status: 401, statusText: 'Unauthorized' });
|
||||
|
||||
await expect(promise).rejects.toBeTruthy();
|
||||
expect(service.isAuthenticated()).toBe(false);
|
||||
});
|
||||
|
||||
it('logout clears the session even if the request fails', async () => {
|
||||
service.token.set('tok-abc');
|
||||
service.user.set({ id: 'u1', email: 'a@example.com', name: 'Alice', is_admin: false });
|
||||
|
||||
const promise = service.logout();
|
||||
const req = httpMock.expectOne('http://api.test/api/auth/logout');
|
||||
req.flush(null, { status: 500, statusText: 'Internal Server Error' });
|
||||
|
||||
await expect(promise).rejects.toBeTruthy();
|
||||
expect(service.isAuthenticated()).toBe(false);
|
||||
expect(service.user()).toBeNull();
|
||||
expect(localStorage.getItem('completed.token')).toBeNull();
|
||||
});
|
||||
|
||||
it('restore() clears an invalid stored token instead of leaving a broken session', async () => {
|
||||
service.token.set('tok-stale');
|
||||
|
||||
const promise = service.restore();
|
||||
const req = httpMock.expectOne('http://api.test/api/auth/me');
|
||||
req.flush({ title: 'Unauthorized', status: 401, detail: 'session expired' }, { status: 401, statusText: 'Unauthorized' });
|
||||
|
||||
await promise;
|
||||
expect(service.isAuthenticated()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Service, computed, inject, signal } from '@angular/core';
|
||||
|
||||
import { AuthService } from '../api/services/auth.service';
|
||||
import { User } from '../api/models/user';
|
||||
|
||||
/** Where the token is kept so a page refresh doesn't log the user out. */
|
||||
const STORAGE_KEY = 'completed.token';
|
||||
|
||||
/**
|
||||
* Holds the current login state (token + user) and is the one place that
|
||||
* talks to the generated AuthService. Components read `user()` /
|
||||
* `isAdmin()` and call `login`/`register`/`logout`; nothing else touches
|
||||
* localStorage or the auth endpoints directly.
|
||||
*/
|
||||
@Service()
|
||||
export class Session {
|
||||
private readonly auth = inject(AuthService);
|
||||
|
||||
readonly user = signal<User | null>(null);
|
||||
readonly token = signal<string | null>(this.readStoredToken());
|
||||
readonly isAuthenticated = computed(() => this.token() !== null);
|
||||
readonly isAdmin = computed(() => this.user()?.is_admin ?? false);
|
||||
|
||||
// A stored token with no user yet (e.g. after a page refresh) is
|
||||
// resolved lazily by whoever needs `user()` first - in this small base,
|
||||
// the games page calls `restore()` on init.
|
||||
|
||||
private readStoredToken(): string | null {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY);
|
||||
} catch {
|
||||
// localStorage can throw in some privacy modes; fall back to
|
||||
// "logged out" rather than crashing the app.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private storeToken(token: string | null): void {
|
||||
try {
|
||||
if (token) {
|
||||
localStorage.setItem(STORAGE_KEY, token);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
} catch {
|
||||
// Same as above: a storage failure shouldn't break login/logout.
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-fetches the current user for a token restored from storage. */
|
||||
async restore(): Promise<void> {
|
||||
if (!this.token() || this.user()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const user = await this.auth.getMe();
|
||||
this.user.set(user);
|
||||
} catch {
|
||||
// The stored token is no longer valid (expired/revoked); drop it so
|
||||
// the app shows the login screen instead of a broken "logged in".
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async login(email: string, password: string): Promise<void> {
|
||||
const resp = await this.auth.login({ body: { email, password } });
|
||||
this.token.set(resp.token);
|
||||
this.user.set(resp.user);
|
||||
this.storeToken(resp.token);
|
||||
}
|
||||
|
||||
async register(email: string, password: string, name: string): Promise<void> {
|
||||
const resp = await this.auth.register({ body: { email, password, name } });
|
||||
this.token.set(resp.token);
|
||||
this.user.set(resp.user);
|
||||
this.storeToken(resp.token);
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
await this.auth.logout();
|
||||
} finally {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private clear(): void {
|
||||
this.token.set(null);
|
||||
this.user.set(null);
|
||||
this.storeToken(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin-right: 1rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.games-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 3rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--mat-sys-error, #b3261e);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<mat-toolbar color="primary">
|
||||
<span>Completed</span>
|
||||
<span class="spacer"></span>
|
||||
@if (session.user()) {
|
||||
<span class="user-name">{{ session.user()!.name }}</span>
|
||||
}
|
||||
<button mat-icon-button (click)="refresh()" aria-label="Refresh" [disabled]="loading()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
<button mat-button (click)="logout()">Log out</button>
|
||||
</mat-toolbar>
|
||||
|
||||
<div class="content">
|
||||
@if (loading()) {
|
||||
<div class="centered"><mat-spinner /></div>
|
||||
} @else if (error()) {
|
||||
<p class="error">{{ error() }}</p>
|
||||
} @else if (games().length === 0) {
|
||||
<p>No games yet.</p>
|
||||
} @else {
|
||||
<table mat-table [dataSource]="games()" class="games-table">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>Name</th>
|
||||
<td mat-cell *matCellDef="let g">{{ g.name }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="platform_name">
|
||||
<th mat-header-cell *matHeaderCellDef>Platform</th>
|
||||
<td mat-cell *matCellDef="let g">{{ g.platform_name }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="score">
|
||||
<th mat-header-cell *matHeaderCellDef>Score</th>
|
||||
<td mat-cell *matCellDef="let g">{{ g.score }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="release_date">
|
||||
<th mat-header-cell *matHeaderCellDef>Released</th>
|
||||
<td mat-cell *matCellDef="let g">{{ g.release_date }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="finished">
|
||||
<th mat-header-cell *matHeaderCellDef>Finished</th>
|
||||
<td mat-cell *matCellDef="let g">{{ g.finished ? '✓' : '' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="columns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: columns"></tr>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,109 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { Router, provideRouter } from '@angular/router';
|
||||
|
||||
import { Games } from './games';
|
||||
import { Session } from '../../core/session';
|
||||
import { provideApiConfiguration } from '../../api/api-configuration';
|
||||
|
||||
describe('Games', () => {
|
||||
let component: Games;
|
||||
let fixture: ComponentFixture<Games>;
|
||||
let httpMock: HttpTestingController;
|
||||
let session: Session;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Games],
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
provideApiConfiguration('http://api.test'),
|
||||
provideRouter([]),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Games);
|
||||
component = fixture.componentInstance;
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
session = TestBed.inject(Session);
|
||||
router = TestBed.inject(Router);
|
||||
});
|
||||
|
||||
afterEach(() => httpMock.verify());
|
||||
|
||||
// Setting both token and user means `session.restore()` (called by
|
||||
// ngOnInit) finds a user already present and skips its own HTTP call,
|
||||
// so each test only has to deal with the request it's actually testing.
|
||||
function logIn(): void {
|
||||
session.token.set('tok-abc');
|
||||
session.user.set({ id: 'u1', email: 'alice@example.com', name: 'Alice', is_admin: false });
|
||||
}
|
||||
|
||||
// ngOnInit awaits session.restore() before calling refresh(), and
|
||||
// restore() returns early (no HTTP call) once a user is already set -
|
||||
// but that early return is still through an async function, so it still
|
||||
// takes one microtask tick before ngOnInit resumes far enough to
|
||||
// actually dispatch the /api/games request.
|
||||
async function flushMicrotask(): Promise<void> {
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
it('loads games on init', async () => {
|
||||
logIn();
|
||||
const initPromise = component.ngOnInit();
|
||||
await flushMicrotask();
|
||||
|
||||
const req = httpMock.expectOne('http://api.test/api/games');
|
||||
req.flush([{ id: 'g1', name: 'Doom', platform_id: 'p1', platform_name: 'PC', score: 95, release_date: '1993-12-10' }]);
|
||||
await initPromise;
|
||||
|
||||
expect(component.games()).toHaveLength(1);
|
||||
expect(component.games()[0].name).toBe('Doom');
|
||||
expect(component.error()).toBeNull();
|
||||
expect(component.loading()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows an error when the request fails', async () => {
|
||||
logIn();
|
||||
const initPromise = component.ngOnInit();
|
||||
await flushMicrotask();
|
||||
|
||||
const req = httpMock.expectOne('http://api.test/api/games');
|
||||
req.flush({ title: 'Internal Server Error', status: 500 }, { status: 500, statusText: 'Internal Server Error' });
|
||||
await initPromise;
|
||||
|
||||
expect(component.error()).toContain('Could not load games');
|
||||
expect(component.games()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('refresh() re-fetches the list', async () => {
|
||||
logIn();
|
||||
const initPromise = component.ngOnInit();
|
||||
await flushMicrotask();
|
||||
httpMock.expectOne('http://api.test/api/games').flush([]);
|
||||
await initPromise;
|
||||
|
||||
const refreshPromise = component.refresh();
|
||||
const req = httpMock.expectOne('http://api.test/api/games');
|
||||
req.flush([{ id: 'g1', name: 'Half-Life', platform_id: 'p1', platform_name: 'PC', score: 96, release_date: '1998-11-19' }]);
|
||||
await refreshPromise;
|
||||
|
||||
expect(component.games()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('logout() clears the session and navigates to /login', async () => {
|
||||
logIn();
|
||||
const navigateSpy = vi.spyOn(router, 'navigateByUrl');
|
||||
|
||||
const logoutPromise = component.logout();
|
||||
httpMock.expectOne('http://api.test/api/auth/logout').flush(null);
|
||||
await logoutPromise;
|
||||
|
||||
expect(session.isAuthenticated()).toBe(false);
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/login');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
|
||||
import { GamesService } from '../../api/services/games.service';
|
||||
import { Game } from '../../api/models/game';
|
||||
import { Session } from '../../core/session';
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatTableModule,
|
||||
MatToolbarModule,
|
||||
],
|
||||
selector: 'app-games',
|
||||
styleUrl: './games.css',
|
||||
templateUrl: './games.html',
|
||||
})
|
||||
export class Games implements OnInit {
|
||||
readonly columns = ['name', 'platform_name', 'score', 'release_date', 'finished'];
|
||||
readonly games = signal<Game[]>([]);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
|
||||
constructor(
|
||||
readonly session: Session,
|
||||
private readonly gamesService: GamesService,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.session.restore();
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
try {
|
||||
this.games.set((await this.gamesService.listGames()) ?? []);
|
||||
} catch {
|
||||
this.error.set('Could not load games. Check your connection and try again.');
|
||||
} finally {
|
||||
this.loading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.session.logout();
|
||||
this.router.navigateByUrl('/login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.login-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 10vh;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--mat-sys-error, #b3261e);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<div class="login-page">
|
||||
<mat-card class="login-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Completed</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput type="email" formControlName="email" autocomplete="email" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput type="password" formControlName="password" autocomplete="current-password" />
|
||||
</mat-form-field>
|
||||
|
||||
@if (error()) {
|
||||
<p class="error">{{ error() }}</p>
|
||||
}
|
||||
|
||||
<button mat-flat-button color="primary" type="submit" [disabled]="form.invalid || submitting()">
|
||||
@if (submitting()) {
|
||||
<mat-spinner diameter="20" />
|
||||
} @else {
|
||||
Log in
|
||||
}
|
||||
</button>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { Router, provideRouter } from '@angular/router';
|
||||
|
||||
import { Login } from './login';
|
||||
import { provideApiConfiguration } from '../../api/api-configuration';
|
||||
|
||||
describe('Login', () => {
|
||||
let component: Login;
|
||||
let fixture: ComponentFixture<Login>;
|
||||
let httpMock: HttpTestingController;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Login],
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
provideApiConfiguration('http://api.test'),
|
||||
provideRouter([]),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Login);
|
||||
component = fixture.componentInstance;
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
router = TestBed.inject(Router);
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
afterEach(() => httpMock.verify());
|
||||
|
||||
it('is invalid with empty fields', () => {
|
||||
expect(component.form.invalid).toBe(true);
|
||||
});
|
||||
|
||||
it('is valid with a well-formed email and non-empty password', () => {
|
||||
component.form.setValue({ email: 'alice@example.com', password: 'secret' });
|
||||
expect(component.form.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a malformed email', () => {
|
||||
component.form.setValue({ email: 'not-an-email', password: 'secret' });
|
||||
expect(component.form.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('on success, logs in and navigates to /games', async () => {
|
||||
const navigateSpy = vi.spyOn(router, 'navigateByUrl');
|
||||
component.form.setValue({ email: 'alice@example.com', password: 'secret' });
|
||||
|
||||
const submitPromise = component.submit();
|
||||
const req = httpMock.expectOne('http://api.test/api/auth/login');
|
||||
req.flush({ token: 'tok-abc', user: { id: 'u1', email: 'alice@example.com', name: 'Alice', is_admin: false } });
|
||||
await submitPromise;
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/games');
|
||||
expect(component.error()).toBeNull();
|
||||
});
|
||||
|
||||
it('on failure, shows an error and does not navigate', async () => {
|
||||
const navigateSpy = vi.spyOn(router, 'navigateByUrl');
|
||||
component.form.setValue({ email: 'alice@example.com', password: 'wrong' });
|
||||
|
||||
const submitPromise = component.submit();
|
||||
const req = httpMock.expectOne('http://api.test/api/auth/login');
|
||||
req.flush(
|
||||
{ title: 'Unauthorized', status: 401, detail: 'invalid email or password' },
|
||||
{ status: 401, statusText: 'Unauthorized' },
|
||||
);
|
||||
await submitPromise;
|
||||
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
expect(component.error()).toBe('invalid email or password');
|
||||
});
|
||||
|
||||
it('does nothing when the form is invalid', async () => {
|
||||
component.form.setValue({ email: '', password: '' });
|
||||
await component.submit();
|
||||
httpMock.expectNone('http://api.test/api/auth/login');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
|
||||
import { Session } from '../../core/session';
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
],
|
||||
selector: 'app-login',
|
||||
styleUrl: './login.css',
|
||||
templateUrl: './login.html',
|
||||
})
|
||||
export class Login {
|
||||
readonly form = new FormBuilder().group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', Validators.required],
|
||||
});
|
||||
|
||||
readonly submitting = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
|
||||
constructor(
|
||||
private readonly session: Session,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
async submit(): Promise<void> {
|
||||
if (this.form.invalid || this.submitting()) {
|
||||
return;
|
||||
}
|
||||
const { email, password } = this.form.getRawValue();
|
||||
|
||||
this.submitting.set(true);
|
||||
this.error.set(null);
|
||||
try {
|
||||
await this.session.login(email!, password!);
|
||||
this.router.navigateByUrl('/games');
|
||||
} catch (err) {
|
||||
this.error.set(errorMessage(err));
|
||||
} finally {
|
||||
this.submitting.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Pulls a server-provided message out of the generated client's error
|
||||
* shape, falling back to a generic one for network failures etc. */
|
||||
function errorMessage(err: unknown): string {
|
||||
if (err && typeof err === 'object' && 'error' in err) {
|
||||
const body = (err as { error?: unknown }).error;
|
||||
if (body && typeof body === 'object' && 'detail' in body && typeof (body as any).detail === 'string') {
|
||||
return (body as any).detail;
|
||||
}
|
||||
}
|
||||
return 'Could not log in. Check your connection and try again.';
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Completed</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
@@ -0,0 +1,38 @@
|
||||
// Include theming for Angular Material with `mat.theme()`.
|
||||
// This Sass mixin will define CSS variables that are used for styling Angular Material
|
||||
// components according to the Material 3 design spec.
|
||||
// Learn more about theming and how to use it for your application's
|
||||
// custom components at https://material.angular.dev/guide/theming
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
@include mat.theme(
|
||||
(
|
||||
color: (
|
||||
primary: mat.$azure-palette,
|
||||
tertiary: mat.$blue-palette,
|
||||
),
|
||||
typography: Roboto,
|
||||
density: 0,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
body {
|
||||
// Default the application to a light color theme. This can be changed to
|
||||
// `dark` to enable the dark color theme, or to `light dark` to defer to the
|
||||
// user's system settings.
|
||||
color-scheme: light;
|
||||
|
||||
// Set a default background, font and text colors for the application using
|
||||
// Angular Material's system-level CSS variables. Learn more about these
|
||||
// variables at https://material.angular.dev/guide/system-variables
|
||||
background-color: var(--mat-sys-surface);
|
||||
color: var(--mat-sys-on-surface);
|
||||
font: var(--mat-sys-body-medium);
|
||||
|
||||
// Reset the user agent margin.
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@@ -0,0 +1,14 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"vitest/globals"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user