start frontend

This commit is contained in:
TopchetoEU 2023-06-30 13:19:21 +03:00
parent 9a63331a95
commit ebe52b3aad
35 changed files with 614 additions and 60 deletions

View File

@ -1,5 +1,5 @@
import clonegur from "./clonegur.ts"; import clonegur from "./clonegur.ts";
const app = await clonegur(); const app = await clonegur();
const server = Deno.listen({ port: 4000, hostname: 'localhost' }); const server = Deno.listen({ port: 80, hostname: '127.0.0.1' });
app.attach(server); app.attach(server);

View File

@ -12,4 +12,5 @@ export default interface Image {
name: string; name: string;
visibility: Visibility; visibility: Visibility;
created: number; created: number;
file: string;
} }

View File

@ -5,7 +5,7 @@ import { JWTPayload } from "../utils/JWT.ts";
import AppRouter from "./AppRouter.ts"; import AppRouter from "./AppRouter.ts";
import HttpError from "../server/HttpError.ts"; import HttpError from "../server/HttpError.ts";
import Image, { Visibility } from "../models/Image.ts"; import Image, { Visibility } from "../models/Image.ts";
import { Page } from "../server/decorators/page.ts"; import uuid, { Page } from "../server/decorators/page.ts";
import { Headers } from "../server/RestRequest.ts"; import { Headers } from "../server/RestRequest.ts";
import { convert } from "../server/decorators/schema.ts"; import { convert } from "../server/decorators/schema.ts";
import { now } from "../utils/utils.ts"; import { now } from "../utils/utils.ts";
@ -20,6 +20,7 @@ export default class ImageRouter extends AppRouter {
name: image.name, name: image.name,
visibility: image.visibility, visibility: image.visibility,
id: image._id, id: image._id,
file: image.file,
}; };
} }
@ -53,7 +54,7 @@ export default class ImageRouter extends AppRouter {
} }
@rest('GET', '/feed') @rest('GET', '/feed')
async self(@page() page: Page) { async self(@page() page: Page) {
const res = await page.apply(this.db.images.find({})).toArray(); const res = await page.apply(this.db.images.find({ visibility: Visibility.Public })).sort({ created: -1 }).toArray();
if (!res) throw new HttpError('User not found.'); if (!res) throw new HttpError('User not found.');
return res.map(v => ImageRouter.deserialize(v)); return res.map(v => ImageRouter.deserialize(v));
} }
@ -95,11 +96,18 @@ export default class ImageRouter extends AppRouter {
req.name ??= new UUID().toString(); req.name ??= new UUID().toString();
req.visibility = 0; req.visibility = 0;
if (req.visibility < 0 || req.visibility > 2) throw new HttpError("body.visibility: Must be 0, 1, or 2"); if (req.visibility < 0 || req.visibility > 2) throw new HttpError("body.visibility: Must be 0, 1, or 2");
const id = new UUID();
// Create file // Create file
const img: Image = { _id: new UUID(), author: user.username, created: now(), name: req.name!, visibility: req.visibility }; const img: Image = {
_id: id,
author: user.username,
created: now(),
name: req.name!,
visibility: req.visibility,
file: `${id}.${ext}`
};
await Deno.mkdir('images', { recursive: true }); await Deno.mkdir('images', { recursive: true });
const out = await Deno.open(`images/${img._id}.${ext}`, { write: true, create: true }); const out = await Deno.open(`images/${id}.${ext}`, { write: true, create: true });
for await (const bit of rawFile.stream()) out.write(bit); for await (const bit of rawFile.stream()) out.write(bit);
out.close(); out.close();
@ -107,29 +115,30 @@ export default class ImageRouter extends AppRouter {
// Write to DB // Write to DB
try { try {
await this.db.images.insertOne(img); await this.db.images.insertOne(img);
await this.db.users.updateOne({ username: user.username }, { $push: { images: img._id } }); await this.db.users.updateOne({ username: user.username }, { $push: { images: id } });
return ImageRouter.deserialize(img); return ImageRouter.deserialize(img);
} }
catch (e) { catch (e) {
await Deno.remove(`images/${img._id}.${ext}`); await Deno.remove(`images/${id}.${ext}`);
throw e; throw e;
} }
} }
@rest('POST', '/change') @rest('POST', '/change')
async change(@body() raw: unknown, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) { async change(@uuid() id: UUID, @body() raw: unknown, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
const body = await convert(raw, { id: 'uuid', name: 'string?', visibility: 'number?' }); const body = await convert(raw, { name: 'string?', visibility: 'number?' });
const user = await this.db.users.findOne({ username: jwt.name }); const user = await this.db.users.findOne({ username: jwt.name });
if (!user) throw new HttpError("You don't exist."); if (!user) throw new HttpError("You don't exist.");
const img = await this.db.images.findOne({ _id: body.id }); const img = await this.db.images.findOne({ _id: id });
if (!img) throw new HttpError("Image doesn't exist."); if (!img) throw new HttpError("Image doesn't exist.");
if (user.username !== img.author) throw new HttpError("You don't own the image."); if (user.username !== img.author) throw new HttpError("You don't own the image.");
await this.db.images.updateOne({ _id: body.id }, { $set: { name: body.name, visibility: body.visibility } }); await this.db.images.updateOne({ _id: id }, { $set: { name: body.name, visibility: body.visibility } });
return ImageRouter.deserialize(img); return ImageRouter.deserialize(img);
} }
public constructor(private db: AppDatabase) { public constructor(private db: AppDatabase) {
super(); super();
db.images.createIndexes({ indexes: [ { key: { created: -1 }, name: 'Image Order' } ] });
} }
} }

View File

@ -19,6 +19,10 @@ export class RootRouter extends AppRouter {
} }
@route('/*') static; @route('/*') static;
@rest('OPTIONS', '*')
options() {
return new RestResponse();
}
@rest('*', '*') @rest('*', '*')
default() { default() {
return new RestResponse().body(stream('Page not found :/')).status(404); return new RestResponse().body(stream('Page not found :/')).status(404);

View File

@ -65,7 +65,12 @@ export default class Router {
if (_req) { if (_req) {
try { try {
const res = await hnd.handler.handle(_req); const res = await hnd.handler.handle(_req);
if (res) return res; if (res) {
// damn you cors
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', '*')
return res;
}
} }
catch (e) { catch (e) {
const res = await this.onError(_req, e); const res = await this.onError(_req, e);

View File

@ -6,7 +6,10 @@ import Router, { makeParameterModifier } from "../Router.ts";
export default function jwt<T extends Router = any>(salt: ((self: T) => string) | string, required = false) { export default function jwt<T extends Router = any>(salt: ((self: T) => string) | string, required = false) {
return makeParameterModifier<T>(function (_req, val?: string) { return makeParameterModifier<T>(function (_req, val?: string) {
if (val === undefined) return undefined; if (val === undefined) {
if (required) throw new HttpError('You are not logged in.');
else return undefined;
}
const s = typeof salt === 'function' ? salt(this) : (this as any)[salt] as string; const s = typeof salt === 'function' ? salt(this) : (this as any)[salt] as string;
try { try {
const res = JWT.decode(val, s); const res = JWT.decode(val, s);

View File

@ -5,7 +5,7 @@ import RestResponse from "../RestResponse.ts";
import Router, { ProcessFunc, addMetaQuery } from "../Router.ts"; import Router, { ProcessFunc, addMetaQuery } from "../Router.ts";
import serialize from "../serialize.ts"; import serialize from "../serialize.ts";
export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE'; export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE' | 'OPTIONS';
type Base<KeyT extends string> = Router & { [X in KeyT]: Function; }; type Base<KeyT extends string> = Router & { [X in KeyT]: Function; };
type ModArray<T extends Router> = ([ProcessFunc<T>, ...ProcessFunc<T>[]] | undefined)[]; type ModArray<T extends Router> = ([ProcessFunc<T>, ...ProcessFunc<T>[]] | undefined)[];

View File

@ -19,7 +19,6 @@ export default async function serialize(val: unknown, depth = 16): Promise<Reada
if (val === undefined) return undefinedBuff.stream(); if (val === undefined) return undefinedBuff.stream();
if (val === null) return nullBuff.stream(); if (val === null) return nullBuff.stream();
if (typeof val === 'string') return stream(val);
if (val instanceof Blob) return val.stream(); if (val instanceof Blob) return val.stream();
if (val instanceof ReadableStream) return val; if (val instanceof ReadableStream) return val;
if (val instanceof Uint8Array) return new Blob([val]).stream(); if (val instanceof Uint8Array) return new Blob([val]).stream();

18
frontend/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Frontend",
"type": "msedge",
"request": "launch",
// "preLaunchTask": "npm: start",
"url": "http://localhost:4200/",
"skipFiles": [
"<node_internals>/**"
]
}
]
}

42
frontend/.vscode/tasks.json vendored Normal file
View File

@ -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": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@ -1,7 +1,18 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { PageHomeComponent } from './page-home/page-home.component';
import { PageLoginComponent } from './page-login/page-login.component';
import { PageSignupComponent } from './page-signup/page-signup.component';
import { PageUploadComponent } from './page-upload/page-upload.component';
import { PageUserComponent } from './page-user/page-user.component';
const routes: Routes = []; const routes: Routes = [
{ path: '', component: PageHomeComponent },
{ path: 'login', component: PageLoginComponent },
{ path: 'signup', component: PageSignupComponent },
{ path: 'upload', component: PageUploadComponent },
{ path: 'user/:name', component: PageUserComponent },
];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes)], imports: [RouterModule.forRoot(routes)],

View File

@ -0,0 +1,17 @@
<div class="navbar-container">
<div class="navbar">
<div>
<a class="selectable" routerLink="/">clonegur</a>
</div>
<div>
<a class="selectable" (click)="users.logoff()" *ngIf="users.$user()">Logoff</a>
<a class="selectable" [routerLink]="'/user/' + users.$user()!.username" *ngIf="users.$user()">My Profile</a>
<a class="selectable" routerLink="/login" *ngIf="!users.$user()">Log in</a>
<a class="selectable" routerLink="/signup" *ngIf="!users.$user()">Sign up</a>
</div>
</div>
</div>
<div class="content">
<router-outlet #o="outlet"></router-outlet>
</div>

View File

@ -0,0 +1,41 @@
:host {
display: grid;
grid-template-rows: max-content auto;
flex-direction: column;
width: 100vw;
height: 100vh;
}
.navbar-container {
z-index: 100;
background-color: #fff;
box-shadow: #666 0px -10px 10px 10px;
.navbar {
width: 100%;
// width: 1000px;
justify-content: space-around;
display: flex;
box-sizing: border-box;
div {
a {
user-select: none;
font-size: 1.2rem;
color: black;
text-decoration: none;
padding: .75rem 1.5rem;
display: inline-block;
}
}
}
}
.content {
width: 100%;
height: 100%;
box-sizing: border-box;
position: relative;
overflow: hidden;
overflow-y: auto;
}

View File

@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { UsersService } from './services/users.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -6,5 +7,5 @@ import { Component } from '@angular/core';
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent { export class AppComponent {
title = 'frontend'; constructor(public users: UsersService) {}
} }

View File

@ -3,14 +3,28 @@ import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { PageHomeComponent } from './page-home/page-home.component';
import { HttpClientModule } from '@angular/common/http';
import { PageLoginComponent } from './page-login/page-login.component';
import { ReactiveFormsModule } from '@angular/forms';
import { PageSignupComponent } from './page-signup/page-signup.component';
import { PageUploadComponent } from './page-upload/page-upload.component';
import { PageUserComponent } from './page-user/page-user.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent AppComponent,
PageHomeComponent,
PageLoginComponent,
PageSignupComponent,
PageUploadComponent,
PageUserComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
AppRoutingModule AppRoutingModule,
ReactiveFormsModule,
HttpClientModule,
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@ -0,0 +1,10 @@
<button routerLink="/upload" *ngIf="users.$user()">Upload image</button>
<!-- <button routerLink="/projects" *ngIf="users.$user()">Към проектите</button> -->
<div class="images">
<div class="image" *ngFor="let img of images">
<img [src]="environment.apiURL + '/images/img/' + img.file">
<h3 class="image-name">{{img.name}}</h3>
<h5 class="image-author">By {{img.author}}</h5>
</div>
</div>

View File

@ -0,0 +1,75 @@
.title {
display: flex;
flex-direction: column;
gap: 1rem;
h1 {
text-align: center;
margin: 0;
}
h2 {
text-align: center;
font-weight: normal;
margin: 0;
}
}
.images {
margin-top: 5rem;
width: 85rem;
max-width: 100%;
gap: 5rem;
align-items: stretch;
box-sizing: border-box;
padding: 0 1rem;
text-align: center;
.image {
user-select: none;
img {
pointer-events: none;
height: 15rem;
display: block;
}
width: min-content;
border-radius: 1rem;
box-shadow: .25rem .25rem 1rem -.5rem #000;
padding: 1rem;
box-sizing: border-box;
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin: 1rem;
p, h3, h5 {
padding: 0;
margin: 0;
}
h3, h5 {
text-align: center;
}
p {
text-align: justify;
}
}
}
@media screen and (width < 50rem) {
.images {
margin-top: 5rem;
display: flex;
flex-direction: column;
gap: 5rem;
align-items: stretch;
.image {
width: 100%;
}
}
}
:host {
margin-top: 5rem;
display: flex;
flex-direction: column;
gap: 2rem;
align-items: center;
}

View File

@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { UsersService } from '../services/users.service';
import { Image, ImagesService } from '../services/images.service';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-page-home',
templateUrl: './page-home.component.html',
styleUrls: ['./page-home.component.scss']
})
export class PageHomeComponent {
public images: Image[] = [];
public environment = environment;
public constructor(
images: ImagesService,
public users: UsersService,
) {
images.feed().then(v => this.images = v);
}
}

View File

@ -0,0 +1,5 @@
<form class="content" [formGroup]="form" (ngSubmit)="login()">
<input type="text" formControlName="username" placeholder="Username">
<input type="password" formControlName="password" placeholder="Password">
<button type="submit">Log in</button>
</form>

View File

@ -0,0 +1,22 @@
.content {
display: flex;
flex-direction: column;
gap: 1rem;
box-sizing: border-box;
max-width: 100%;
width: 20rem;
input {
box-sizing: border-box;
width: 100%;
}
button {
margin: auto
}
}
:host {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

View File

@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { UsersService } from '../services/users.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-page-login',
templateUrl: './page-login.component.html',
styleUrls: ['./page-login.component.scss']
})
export class PageLoginComponent {
public form = new FormGroup({
username: new FormControl(''),
password: new FormControl(''),
});
public async login() {
await this.users.login(this.form.value.username ?? '', this.form.value.password ?? '');
this.router.navigateByUrl('/');
}
public constructor(
private users: UsersService,
private router: Router
) { }
}

View File

@ -0,0 +1,5 @@
<form class="content" [formGroup]="form" (ngSubmit)="signup()">
<input type="text" formControlName="username" placeholder="Username">
<input type="password" formControlName="password" placeholder="Password">
<button type="submit">Sign up</button>
</form>

View File

@ -0,0 +1,22 @@
.content {
display: flex;
flex-direction: column;
gap: 1rem;
box-sizing: border-box;
max-width: 100%;
width: 20rem;
input {
box-sizing: border-box;
width: 100%;
}
button {
margin: auto
}
}
:host {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

View File

@ -0,0 +1,29 @@
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { UsersService } from '../services/users.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-page-signup',
templateUrl: './page-signup.component.html',
styleUrls: ['./page-signup.component.scss']
})
export class PageSignupComponent {
public form = new FormGroup({
username: new FormControl<string>(''),
password: new FormControl<string>(''),
});
public async signup() {
await this.users.signup(
this.form.value.username ?? '',
this.form.value.password ?? ''
);
this.router.navigateByUrl('/');
}
public constructor(
private users: UsersService,
private router: Router
) { }
}

View File

@ -0,0 +1,10 @@
<form class="content" [formGroup]="form" (ngSubmit)="submit()">
<input type="text" placeholder="Име" formControlName="name">
<input type="file" (change)="setFile($event)"/>
<select formControlName="visibility">
<option [value]="0">Public</option>
<option [value]="1">Unlisted</option>
<option [value]="2">Private</option>
</select>
<button type="submit">Създай проект</button>
</form>

View File

@ -0,0 +1,28 @@
.content {
display: flex;
flex-direction: column;
gap: 1rem;
box-sizing: border-box;
max-width: 100%;
width: 30rem;
input {
box-sizing: border-box;
width: 100%;
}
textarea {
resize: none;
height: 10rem;
box-sizing: border-box;
width: 100%;
}
button {
margin: auto
}
}
:host {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

View File

@ -0,0 +1,33 @@
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { ImagesService, Visibility } from '../services/images.service';
@Component({
selector: 'app-page-upload',
templateUrl: './page-upload.component.html',
styleUrls: ['./page-upload.component.scss']
})
export class PageUploadComponent {
public form = new FormGroup({
name: new FormControl<string | null>(null, [ Validators.required ]),
file: new FormControl<File | null>(null, [ Validators.required ]),
visibility: new FormControl(Visibility.Unlisted, [ Validators.required ]),
});
public setFile($event: any) {
this.form.patchValue({ file: $event.target.files[0] });
}
public async submit() {
await this.images.upload({
name: this.form.value.name ?? ''
}, this.form.value.file!);
this.router.navigateByUrl('/');
}
public constructor(
private images: ImagesService,
private router: Router
) {}
}

View File

@ -0,0 +1 @@
<p>page-user works!</p>

View File

@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { User, UsersService } from '../services/users.service';
@Component({
selector: 'app-page-user',
templateUrl: './page-user.component.html',
styleUrls: ['./page-user.component.scss']
})
export class PageUserComponent {
public user: User = { images: [], username: 'loading...' };
public constructor(
private route: ActivatedRoute,
private users: UsersService
) {
route.paramMap.subscribe(async v => {
this.user = await users.get(v.get('name')!);
});
}
}

View File

@ -0,0 +1,56 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { environment } from 'src/environments/environment';
import { UsersService } from './users.service';
export enum Visibility {
Public,
Unlisted,
Private,
}
export interface Image {
id: string;
author: string;
name: string;
visibility: Visibility;
created: number;
file: string;
}
export interface CreateImageBody {
name?: string;
visibility?: Visibility;
}
@Injectable({
providedIn: 'root'
})
export class ImagesService {
private readonly url = environment.apiURL + '/images';
public async get(id: string) {
return await firstValueFrom(this.http.get<Image>(`${this.url}`, {
...this.users.httpOptions,
params: { id }
}));
}
public async feed(n?: number, i?: number) {
return await firstValueFrom(this.http.get<Image[]>(`${this.url}/feed`, {
...this.users.httpOptions,
// params: { n, i }
}));
}
public async upload(proto: CreateImageBody, file: File) {
const data = new FormData();
data.append('file', file, file.name);
data.append('body', JSON.stringify(proto));
return await firstValueFrom(this.http.post<string>(`${this.url}/upload`, data, this.users.httpOptions({})));
}
public async change(id: string, proto: CreateImageBody) {
return await firstValueFrom(this.http.post<string>(`${this.url}/change?id=${id}`, proto, this.users.httpOptions({})));
}
public constructor(private http: HttpClient, private users: UsersService) {
}
}

View File

@ -1,24 +1,11 @@
import { Injectable, WritableSignal, signal } from '@angular/core'; import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { first, firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
export enum Role {
Admin = 3,
Employer = 2,
User = 1,
API = 0,
Deactivated = -1,
}
export interface User { export interface User {
username: string; username: string;
role: Role; images: string[];
projects: string[];
}
export interface ThisUser extends User {
chats: string[];
email: string;
} }
@Injectable({ @Injectable({
@ -26,7 +13,7 @@ export interface ThisUser extends User {
}) })
export class UsersService { export class UsersService {
private readonly url = environment.apiURL + '/users'; private readonly url = environment.apiURL + '/users';
public $user = signal<ThisUser | undefined>(undefined); public $user = signal<User | undefined>(undefined);
public get token() { public get token() {
return localStorage.getItem('token') ?? undefined; return localStorage.getItem('token') ?? undefined;
@ -36,43 +23,46 @@ export class UsersService {
else localStorage.setItem('token', token); else localStorage.setItem('token', token);
} }
public httpOptions<T extends object>(other: T): T & { headers?: HttpHeaders } {
if (this.token) {
(other as any).headers ??= {};
(other as any).headers.authorization = 'Bearer ' + this.token;
}
return other;
}
public async updateUser() { public async updateUser() {
if (this.token) { if (this.token) {
const user = await firstValueFrom(this.http.get<ThisUser>(`${this.url}/get?token=${this.token}`)); const user = await firstValueFrom(this.http.get<User>(`${this.url}/self`, {
headers: { authorization: 'Bearer ' + this.token }
}));
this.$user.set(user); this.$user.set(user);
} }
else this.$user.set(undefined); else this.$user.set(undefined);
} }
public async get(username: string) {
const user = await firstValueFrom(this.http.get<User>(`${this.url}?username=${username}`, this.httpOptions({})));
return user;
}
public async logoff() { public async logoff() {
if (!this.token) return; if (!this.token) return;
await firstValueFrom(this.http.post(this.url + `/logout?token=${this.token}`, {}));
this.token = undefined; this.token = undefined;
this.$user.set(undefined); this.$user.set(undefined);
} }
public async login(username: string, password: string) { public async login(username: string, password: string) {
await this.logoff(); await this.logoff();
const token = (await firstValueFrom(this.http.post<any>(this.url + `/login`, { username, password }))).token; const token = await firstValueFrom(this.http.post<string>(`${this.url}/login`, { username, password }));
this.token = token; this.token = token;
await this.updateUser(); await this.updateUser();
} }
public async signup(username: string, password: string, email: string) { public async signup(username: string, password: string) {
await this.logoff(); await this.logoff();
await firstValueFrom(this.http.post(this.url + `/signup`, { username, password, email })); await firstValueFrom(this.http.post(`${this.url}/signup`, { username, password }));
await this.updateUser(); await this.updateUser();
} }
public async requestCode(username: string) {
await firstValueFrom(this.http.post<string>(this.url + `/requestCode`, { username }));
}
public async confirm(username: string, code: string) {
await firstValueFrom(this.http.post<string>(this.url + `/confirm`, { username, code }));
}
public async all() {
return await firstValueFrom(this.http.get<User[]>(this.url + `/all`));
}
public constructor(private http: HttpClient) { public constructor(private http: HttpClient) {
this.updateUser(); this.updateUser();
} }

View File

@ -1,8 +1,6 @@
declare function $__getHTTP(): string; declare function $__getHTTP(): string;
declare function $__getWS(): string;
export const environment = { export const environment = {
production: true, production: true,
apiURL: $__getHTTP(), apiURL: $__getHTTP(),
wsURL: $__getWS()
}; };

View File

@ -1,5 +1,4 @@
export const environment = { export const environment = {
production: false, production: false,
apiURL: 'http://127.0.0.1/api', apiURL: 'http://127.0.0.1/api',
wsURL: 'ws://127.0.0.1/api'
}; };

View File

@ -1 +1,39 @@
/* You can add global styles to this file, and also import other style files */ body, html {
margin: 0;
padding: 0;
height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
button {
border: 1px solid #777;
background-color: #fff;
padding: .5rem 1rem;
border-radius: 100rem;
transition: 100ms;
}
input, textarea {
font-family: inherit;
border: 1px solid #777;
background-color: #fff;
padding: .5rem 1rem;
border-radius: .5rem;
transition: 100ms;
outline: none;
}
button, .hoverable, .selectable {
transition: 250ms background-color;
cursor: pointer;
}
button, .hoverable, .selectable {
&:hover {
background-color: #f0f0f0;
}
}
button:enabled, .selectable {
&:active {
background-color: #aaa;
}
}