Added new Routing System
This commit is contained in:
@@ -1,54 +0,0 @@
|
|||||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'QUERY';
|
|
||||||
|
|
||||||
export interface ApiOptions extends RequestInit {
|
|
||||||
method?: HttpMethod;
|
|
||||||
body?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zentraler API-Wrapper mit typisiertem Rückgabewert und Fehlerhandling.
|
|
||||||
* Beispiele:
|
|
||||||
* const tracks = await api<Track[]>('/api/tracks');
|
|
||||||
* await api<void>('/api/tracks/1', { method: 'DELETE' });
|
|
||||||
*/
|
|
||||||
export async function api<T = any>(
|
|
||||||
path: string,
|
|
||||||
options: ApiOptions = {}
|
|
||||||
): Promise<T> {
|
|
||||||
const config: RequestInit = {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(options.headers || {}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.body && typeof options.body !== 'string') {
|
|
||||||
config.body = JSON.stringify(options.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(path, config);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
let msg = `HTTP Error ${response.status}`;
|
|
||||||
try {
|
|
||||||
const text = await response.text();
|
|
||||||
msg += `: ${text}`;
|
|
||||||
} catch { }
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 204) return undefined as T;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return (await response.json()) as T;
|
|
||||||
} catch {
|
|
||||||
return undefined as T;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const apiGet = <T>(path: string) => api<T>(path, { method: 'GET' });
|
|
||||||
export const apiPost = <T>(path: string, body: any) => api<T>(path, { method: 'POST', body });
|
|
||||||
export const apiPut = <T>(path: string, body: any) => api<T>(path, { method: 'PUT', body });
|
|
||||||
export const apiDelete = <T>(path: string) => api<T>(path, { method: 'DELETE' });
|
|
||||||
+94
-55
@@ -1,91 +1,130 @@
|
|||||||
import { LitElement, html, css } from 'lit';
|
import { LitElement, html, css } from 'lit';
|
||||||
import { customElement, state } from 'lit/decorators.js';
|
import { customElement, state } from 'lit/decorators.js';
|
||||||
import { Router } from './router/router';
|
import { Router } from './router/router';
|
||||||
import './pages/not-found-page';
|
import { authService } from './services/auth.service';
|
||||||
|
import type { AuthLevel } from './router/router';
|
||||||
|
import './components/footer-bar.js';
|
||||||
import './components/nav-bar';
|
import './components/nav-bar';
|
||||||
import './components/cc/cc-nav-bar';
|
import './components/cc/cc-nav-bar';
|
||||||
import './components/footer-bar';
|
//import './components/tt/tt-nav-bar';
|
||||||
|
|
||||||
@customElement('app-root')
|
@customElement('app-root')
|
||||||
export class AppRoot extends LitElement {
|
export class AppRoot extends LitElement {
|
||||||
private router!: Router;
|
private router!: Router;
|
||||||
|
@state() private currentPath = '/';
|
||||||
@state() private isCompetitionCenter = false;
|
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host { display: flex; flex-direction: column; min-height: 100vh; }
|
||||||
display: flex;
|
main { flex: 1 0 auto; display: flex; flex-direction: column; }
|
||||||
flex-direction: column;
|
`;
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
nav-bar {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer-bar {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
const outlet = this.shadowRoot?.getElementById('outlet') as HTMLElement;
|
authService.init();
|
||||||
|
const outlet = this.shadowRoot!.getElementById('outlet') as HTMLElement;
|
||||||
|
|
||||||
this.router = new Router(outlet, [
|
this.router = new Router(outlet, [
|
||||||
|
// ── Public ──────────────────────────────────────────
|
||||||
{
|
{
|
||||||
path: '/', view: async () => {
|
path: '/',
|
||||||
|
auth: 'public',
|
||||||
|
exact: true,
|
||||||
|
view: async () => {
|
||||||
await import('./pages/home-page.js');
|
await import('./pages/home-page.js');
|
||||||
return document.createElement('home-page');
|
return document.createElement('home-page');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/competitions', view: async () => {
|
path: '/competitions',
|
||||||
|
auth: 'public',
|
||||||
|
view: async () => {
|
||||||
await import('./pages/competition/competition-page.js');
|
await import('./pages/competition/competition-page.js');
|
||||||
return document.createElement('competition-page');
|
return document.createElement('competition-page');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login', view: async () => {
|
path: '/dev',
|
||||||
await import('./pages/auth/login-page.js');
|
auth: 'public',
|
||||||
return document.createElement('login-page')
|
view: async () => {
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/register', view: async () => {
|
|
||||||
await import('./pages/auth/register-page.js');
|
|
||||||
return document.createElement('register-page');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/cc/', view: async () => {
|
|
||||||
await import('./pages/cc/cc-home-page.js');
|
|
||||||
return document.createElement('cc-home-page');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/dev', view: async () => {
|
|
||||||
await import('./pages/dev-page.js');
|
await import('./pages/dev-page.js');
|
||||||
return document.createElement('dev-page');
|
return document.createElement('dev-page');
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]);
|
{
|
||||||
|
path: '/login',
|
||||||
|
auth: 'public',
|
||||||
|
view: async () => {
|
||||||
|
await import('./pages/auth/login-page.js');
|
||||||
|
return document.createElement('login-page');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
this.router.onRouteChange = (path: string) => {
|
// ── Target Team (Code-geschützt) ─────────────────────
|
||||||
this.isCompetitionCenter = path.startsWith('/cc');
|
{
|
||||||
|
path: '/tt',
|
||||||
|
auth: 'code',
|
||||||
|
view: async () => {
|
||||||
|
await import('./pages/tt/tt-home-page.js');
|
||||||
|
return document.createElement('tt-home-page');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Competition Center (User-Login) ──────────────────
|
||||||
|
{
|
||||||
|
path: '/cc',
|
||||||
|
auth: 'user',
|
||||||
|
view: async () => {
|
||||||
|
await import('./pages/cc/cc-home-page.js');
|
||||||
|
return document.createElement('cc-home-page');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── System Admin ─────────────────────────────────────
|
||||||
|
{
|
||||||
|
path: '/admin',
|
||||||
|
auth: 'admin',
|
||||||
|
view: async () => {
|
||||||
|
await import('./pages/admin/admin-home-page.js');
|
||||||
|
return document.createElement('admin-home-page');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Guard-Logik
|
||||||
|
{
|
||||||
|
canActivate: async (auth: AuthLevel) => {
|
||||||
|
if (auth === 'public') return true;
|
||||||
|
if (auth === 'code') return authService.hasTeamCode;
|
||||||
|
if (auth === 'user') return authService.isLoggedIn;
|
||||||
|
if (auth === 'admin') return authService.isAdmin;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onFail: (auth: AuthLevel, navigate) => {
|
||||||
|
if (auth === 'code') navigate('/tt/login'); // Code-Eingabe-Seite
|
||||||
|
if (auth === 'user') navigate('/login');
|
||||||
|
if (auth === 'admin') navigate('/login');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.router.onRouteChange = (path) => {
|
||||||
|
this.currentPath = path;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.router.resolve();
|
this.router.resolve();
|
||||||
this.addEventListener('nav', (e: any) => this.router.navigate(e.detail.path));
|
window.addEventListener('nav', (e: any) => this.router.navigate(e.detail.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private get section() {
|
||||||
|
if (this.currentPath.startsWith('/cc')) return 'cc';
|
||||||
|
if (this.currentPath.startsWith('/admin')) return 'admin';
|
||||||
|
if (this.currentPath.startsWith('/tt')) return 'tt';
|
||||||
|
return 'public';
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
${this.isCompetitionCenter
|
${this.section === 'cc' ? html`<cc-nav-bar></cc-nav-bar>`
|
||||||
? html`<cc-nav-bar></cc-nav-bar>`
|
: this.section === 'admin' ? html`<admin-nav-bar></admin-nav-bar>`
|
||||||
|
: this.section === 'tt' ? html`<tt-nav-bar></tt-nav-bar>`
|
||||||
: html`<nav-bar></nav-bar>`}
|
: html`<nav-bar></nav-bar>`}
|
||||||
<main id="outlet"></main>
|
<main id="outlet"></main>
|
||||||
<footer-bar></footer-bar>
|
<footer-bar></footer-bar>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { LitElement, html } from 'lit';
|
||||||
|
import { customElement } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
@customElement('cc-home-page')
|
||||||
|
export class CCHomePage extends LitElement {
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<h1>Welcome to the Competition Center Homepage</h1>
|
||||||
|
<p>Analyze your tracks visually.</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { LitElement, html, css } from 'lit';
|
import { LitElement, html, css } from "lit";
|
||||||
import { customElement, state } from 'lit/decorators.js';
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { apiPost } from '../../api/api';
|
import { authService, type UserSession } from "../../services/auth.service";
|
||||||
import '../../components/auth-card';
|
import "../../components/auth-card";
|
||||||
import '../../components/form-input';
|
import "../../components/form-input";
|
||||||
import '../../components/ui-button';
|
import "../../components/ui-button";
|
||||||
import '../../components/notify-bar';
|
import "../../components/notify-bar";
|
||||||
import '../../components/horizontal-divider';
|
import "../../components/horizontal-divider";
|
||||||
import '../../components/ui-link';
|
import "../../components/ui-link";
|
||||||
|
|
||||||
@customElement('login-page')
|
@customElement("login-page")
|
||||||
export class LoginPage extends LitElement {
|
export class LoginPage extends LitElement {
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
@@ -24,8 +24,8 @@ export class LoginPage extends LitElement {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@state() email = '';
|
@state() email = "";
|
||||||
@state() password = '';
|
@state() password = "";
|
||||||
@state() error: string | null = null;
|
@state() error: string | null = null;
|
||||||
@state() loading = false;
|
@state() loading = false;
|
||||||
|
|
||||||
@@ -33,21 +33,50 @@ export class LoginPage extends LitElement {
|
|||||||
this.error = null;
|
this.error = null;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await apiPost<{ token: string }>('/api/auth/login', {
|
//todo const response = await fetch('/api/auth/login', {
|
||||||
email: this.email,
|
const response = await fetch("http://127.0.0.1:8080/auth/login", {
|
||||||
password: this.password,
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email: this.email, password: this.password }),
|
||||||
});
|
});
|
||||||
localStorage.setItem('token', res.token);
|
|
||||||
window.dispatchEvent(new CustomEvent('auth-changed'));
|
if (!response.ok) {
|
||||||
dispatchEvent(
|
this.error =
|
||||||
new CustomEvent('nav', {
|
response.status === 401
|
||||||
detail: { path: '/cc/' },
|
? "E-Mail oder Passwort falsch."
|
||||||
|
: "Login fehlgeschlagen.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await response.json();
|
||||||
|
const base64url = raw.accessToken.split(".")[1];
|
||||||
|
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const payload = JSON.parse(atob(base64));
|
||||||
|
|
||||||
|
authService.setSession(
|
||||||
|
{
|
||||||
|
id: payload.sub,
|
||||||
|
email: payload.email,
|
||||||
|
role: payload.role ?? null,
|
||||||
|
permissions: payload.permissions ?? [],
|
||||||
|
},
|
||||||
|
raw.accessToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent("auth-changed"));
|
||||||
|
|
||||||
|
const target = authService.isAdmin ? "/admin" : "/cc";
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("nav", {
|
||||||
|
detail: { path: target },
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
this.error = e.message || 'Login failed';
|
console.error("Fehler:", e);
|
||||||
|
this.error = "Netzwerkfehler. Bitte erneut versuchen.";
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@@ -64,8 +93,7 @@ export class LoginPage extends LitElement {
|
|||||||
type="email"
|
type="email"
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
.value=${this.email}
|
.value=${this.email}
|
||||||
@value-changed=${(e: CustomEvent) =>
|
@value-changed=${(e: CustomEvent) => (this.email = e.detail.value)}
|
||||||
(this.email = e.detail.value)}
|
|
||||||
></form-input>
|
></form-input>
|
||||||
|
|
||||||
<form-input
|
<form-input
|
||||||
@@ -73,21 +101,15 @@ export class LoginPage extends LitElement {
|
|||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
.value=${this.password}
|
.value=${this.password}
|
||||||
@value-changed=${(e: CustomEvent) =>
|
@value-changed=${(e: CustomEvent) => (this.password = e.detail.value)}
|
||||||
(this.password = e.detail.value)}
|
|
||||||
></form-input>
|
></form-input>
|
||||||
|
|
||||||
<notify-bar type="error" .message=${this.error}></notify-bar>
|
<notify-bar type="error" .message=${this.error}></notify-bar>
|
||||||
|
|
||||||
<ui-button
|
<ui-button full ?disabled=${this.loading} @click=${this.handleLogin}>
|
||||||
full
|
${this.loading ? "Signing in..." : "Sign in"}
|
||||||
?disabled=${this.loading}
|
|
||||||
@click=${this.handleLogin}
|
|
||||||
>
|
|
||||||
${this.loading ? 'Signing in...' : 'Sign in'}
|
|
||||||
</ui-button>
|
</ui-button>
|
||||||
|
|
||||||
|
|
||||||
<horizontal-divider></horizontal-divider>
|
<horizontal-divider></horizontal-divider>
|
||||||
|
|
||||||
<p class="footer">
|
<p class="footer">
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { LitElement, html } from 'lit';
|
||||||
|
import { customElement } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
@customElement('cc-home-page')
|
||||||
|
export class CCHomePage extends LitElement {
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<h1>Welcome to the Target Team Homepage</h1>
|
||||||
|
<p>Analyze your tracks visually.</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +1,61 @@
|
|||||||
|
export type AuthLevel = "public" | "code" | "user" | "admin";
|
||||||
|
|
||||||
export type Route = {
|
export type Route = {
|
||||||
path: string;
|
path: string;
|
||||||
|
auth: AuthLevel;
|
||||||
|
exact?: boolean;
|
||||||
view: () => Promise<HTMLElement> | HTMLElement;
|
view: () => Promise<HTMLElement> | HTMLElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthGuard = {
|
||||||
|
canActivate: (auth: AuthLevel) => Promise<boolean>;
|
||||||
|
onFail: (auth: AuthLevel, navigate: (path: string) => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export class Router {
|
export class Router {
|
||||||
private routes: Route[];
|
private routes: Route[];
|
||||||
private outlet: HTMLElement;
|
private outlet: HTMLElement;
|
||||||
|
private guard: AuthGuard;
|
||||||
onRouteChange?: (path: string) => void;
|
onRouteChange?: (path: string) => void;
|
||||||
|
|
||||||
constructor(outlet: HTMLElement, routes: Route[]) {
|
constructor(outlet: HTMLElement, routes: Route[], guard: AuthGuard) {
|
||||||
this.routes = routes;
|
this.routes = routes;
|
||||||
this.outlet = outlet;
|
this.outlet = outlet;
|
||||||
window.addEventListener('popstate', () => this.resolve());
|
this.guard = guard;
|
||||||
this.resolve();
|
window.addEventListener("popstate", () => this.resolve());
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(path: string) {
|
navigate(path: string) {
|
||||||
window.history.pushState({}, '', path);
|
window.history.pushState({}, "", path);
|
||||||
this.resolve();
|
this.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolve() {
|
async resolve() {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
const match = this.routes.find((r) => r.path === path);
|
|
||||||
this.outlet.innerHTML = '';
|
const match = this.routes.find((r) =>
|
||||||
|
r.exact
|
||||||
|
? r.path === path
|
||||||
|
: path === r.path || path.startsWith(r.path + "/"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const route = match ?? { auth: "public" as AuthLevel, view: null };
|
||||||
|
|
||||||
|
const allowed = await this.guard.canActivate(route.auth);
|
||||||
|
if (!allowed) {
|
||||||
|
this.guard.onFail(route.auth, this.navigate.bind(this));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.outlet.innerHTML = "";
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const view = await match.view();
|
const view = await match.view();
|
||||||
this.outlet.append(view);
|
this.outlet.append(view);
|
||||||
} else {
|
} else {
|
||||||
this.outlet.append(document.createElement('not-found-page'));
|
this.outlet.append(document.createElement("not-found-page"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.onRouteChange) {
|
this.onRouteChange?.(path);
|
||||||
this.onRouteChange(path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { authService } from './auth.service';
|
||||||
|
|
||||||
|
class ApiService {
|
||||||
|
|
||||||
|
async fetch(path: string, options: RequestInit = {}): Promise<Response> {
|
||||||
|
const response = await this.doFetch(path, options);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
const refreshed = await this.refresh();
|
||||||
|
if (refreshed) {
|
||||||
|
return this.doFetch(path, options);
|
||||||
|
} else {
|
||||||
|
authService.clearSession();
|
||||||
|
window.dispatchEvent(new CustomEvent('session-expired'));
|
||||||
|
throw new Error('Session expired');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doFetch(path: string, options: RequestInit): Promise<Response> {
|
||||||
|
//todo return fetch(`/api${path}`, {
|
||||||
|
return fetch(`http://127.0.0.1:8080${path}`, {
|
||||||
|
...options,
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(authService.accessToken
|
||||||
|
? { Authorization: `Bearer ${authService.accessToken}` }
|
||||||
|
: {}),
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refresh(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
//const response = await fetch('/api/auth/refresh', {
|
||||||
|
const response = await fetch('http://127.0.0.1:8080/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return false;
|
||||||
|
|
||||||
|
const { accessToken } = await response.json();
|
||||||
|
authService.setSession(authService.user!, accessToken);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiService = new ApiService();
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
export type UserSession = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
role: string | null;
|
||||||
|
permissions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
private _user: UserSession | null = null;
|
||||||
|
private _accessToken: string | null = null;
|
||||||
|
private _teamCode: string | null = null;
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const stored = sessionStorage.getItem("session");
|
||||||
|
if (stored) this._user = JSON.parse(stored);
|
||||||
|
this._teamCode = sessionStorage.getItem("team_code");
|
||||||
|
}
|
||||||
|
|
||||||
|
get user() {
|
||||||
|
return this._user;
|
||||||
|
}
|
||||||
|
get isLoggedIn() {
|
||||||
|
return this._accessToken !== null;
|
||||||
|
}
|
||||||
|
get isAdmin() {
|
||||||
|
return this._user?.role === "admin";
|
||||||
|
}
|
||||||
|
get accessToken() {
|
||||||
|
return this._accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasTeamCode() {
|
||||||
|
return this._teamCode !== null;
|
||||||
|
}
|
||||||
|
get teamCode() {
|
||||||
|
return this._teamCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSession(user: UserSession, accessToken: string) {
|
||||||
|
if (user == undefined || accessToken == undefined) {
|
||||||
|
console.warn("not saving undefined session");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._user = user;
|
||||||
|
this._accessToken = accessToken;
|
||||||
|
sessionStorage.setItem("session", JSON.stringify(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
setTeamCode(code: string) {
|
||||||
|
this._teamCode = code;
|
||||||
|
sessionStorage.setItem("team_code", code);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTeamCode() {
|
||||||
|
this._teamCode = null;
|
||||||
|
sessionStorage.removeItem("team_code");
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSession() {
|
||||||
|
this._user = null;
|
||||||
|
this._accessToken = null;
|
||||||
|
this._teamCode = null;
|
||||||
|
sessionStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPermission(permission: string) {
|
||||||
|
return this._user?.permissions.includes(permission) ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authService = new AuthService();
|
||||||
Reference in New Issue
Block a user