feat: add angular support to server
This commit is contained in:
parent
7318cac349
commit
9a63331a95
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
/backend/keys
|
/backend/keys
|
||||||
/backend/images
|
/backend/images
|
||||||
|
/backend/static
|
||||||
/backend/pacakage.json
|
/backend/pacakage.json
|
||||||
|
|
||||||
/frontend/dist
|
/frontend/dist
|
||||||
|
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -8,8 +8,8 @@
|
|||||||
"name": "Launch Backend",
|
"name": "Launch Backend",
|
||||||
"type": "pwa-node",
|
"type": "pwa-node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/main.ts",
|
"program": "${workspaceFolder}/backend/main.ts",
|
||||||
"cwd": "${workspaceFolder}/cwd",
|
"cwd": "${workspaceFolder}/backend",
|
||||||
"runtimeExecutable": "deno",
|
"runtimeExecutable": "deno",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
"run",
|
"run",
|
||||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"deno.enable": true,
|
"deno.enable": true,
|
||||||
"deno.unstable": true,
|
"deno.unstable": true,
|
||||||
|
"deno.enablePaths": [ "backend" ],
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||||
}
|
}
|
@ -11,6 +11,7 @@ export default async function clonegur() {
|
|||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
salt = await bcrypt.genSalt();
|
salt = await bcrypt.genSalt();
|
||||||
|
await Deno.mkdir('keys', { recursive: true });
|
||||||
await Deno.writeFile('keys/salt.txt', new TextEncoder().encode(salt));
|
await Deno.writeFile('keys/salt.txt', new TextEncoder().encode(salt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ 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";
|
||||||
import AppDatabase from "../AppDatabase.ts";
|
import AppDatabase from "../AppDatabase.ts";
|
||||||
|
import RestResponse from "../server/RestResponse.ts";
|
||||||
|
|
||||||
export default class ImageRouter extends AppRouter {
|
export default class ImageRouter extends AppRouter {
|
||||||
public static deserialize(image: Image) {
|
public static deserialize(image: Image) {
|
||||||
@ -33,6 +34,23 @@ export default class ImageRouter extends AppRouter {
|
|||||||
|
|
||||||
return ImageRouter.deserialize(image);
|
return ImageRouter.deserialize(image);
|
||||||
}
|
}
|
||||||
|
@rest('GET', '/img/:id')
|
||||||
|
async file(id: string, @jwt(v => v.salt, false) @auth() jwt?: JWTPayload) {
|
||||||
|
try {
|
||||||
|
const start = await Deno.realPath("images");
|
||||||
|
const file = await Deno.realPath(`images/${id}`);
|
||||||
|
if (!file.startsWith(start)) throw new HttpError("What the fuck are you doing?", 418);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (!(e instanceof HttpError)) throw new HttpError("File doesn't exist.", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = await this.db.images.findOne({ _id: new UUID(id.split('.')[0]) }).catch(() => undefined);
|
||||||
|
if (img?.visibility === Visibility.Private && img.author !== jwt?.name) throw new HttpError("File doesn't exist.", 404);
|
||||||
|
return new RestResponse()
|
||||||
|
.body((await Deno.open(`images/${id}`)).readable)
|
||||||
|
.contentType(id.split('.')[1]);
|
||||||
|
}
|
||||||
@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({})).toArray();
|
||||||
|
@ -5,10 +5,19 @@ import RestResponse from "../server/RestResponse.ts";
|
|||||||
import { rest, route } from "../server/decorators.ts";
|
import { rest, route } from "../server/decorators.ts";
|
||||||
import { stream } from "../utils/utils.ts";
|
import { stream } from "../utils/utils.ts";
|
||||||
import AppDatabase from "../AppDatabase.ts";
|
import AppDatabase from "../AppDatabase.ts";
|
||||||
|
import staticHandler from "../server/staticHandler.ts";
|
||||||
|
|
||||||
export class RootRouter extends AppRouter {
|
export class RootRouter extends AppRouter {
|
||||||
@route('users/*') users;
|
@route('api/users/*') users;
|
||||||
@route('images/*') images;
|
@route('api/images/*') images;
|
||||||
|
|
||||||
|
@rest('GET', '/')
|
||||||
|
async index() {
|
||||||
|
return new RestResponse()
|
||||||
|
.body((await Deno.open('static/index.html')).readable)
|
||||||
|
.contentType('html');
|
||||||
|
}
|
||||||
|
@route('/*') static;
|
||||||
|
|
||||||
@rest('*', '*')
|
@rest('*', '*')
|
||||||
default() {
|
default() {
|
||||||
@ -18,6 +27,7 @@ export class RootRouter extends AppRouter {
|
|||||||
constructor(salt: string, db: AppDatabase) {
|
constructor(salt: string, db: AppDatabase) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.static = staticHandler('static');
|
||||||
this.users = new UserRouter(salt, db);
|
this.users = new UserRouter(salt, db);
|
||||||
this.images = new ImageRouter(db);
|
this.images = new ImageRouter(db);
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ export default class UserRouter extends AppRouter {
|
|||||||
|
|
||||||
if (res === undefined) throw new HttpError('User not found.');
|
if (res === undefined) throw new HttpError('User not found.');
|
||||||
|
|
||||||
return this.deserialize(res);
|
return this.deserialize(res, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@rest('POST', '/signup')
|
@rest('POST', '/signup')
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { lookup } from "https://deno.land/x/mrmime@v1.0.1/mod.ts"
|
||||||
|
|
||||||
import { stream } from "../utils/utils.ts";
|
import { stream } from "../utils/utils.ts";
|
||||||
import { Headers } from "./RestRequest.ts";
|
import { Headers } from "./RestRequest.ts";
|
||||||
|
|
||||||
@ -14,6 +16,11 @@ export default class RestResponse {
|
|||||||
public get statusMessage() { return this.#statusMsg; }
|
public get statusMessage() { return this.#statusMsg; }
|
||||||
public get content() { return this.#body; }
|
public get content() { return this.#body; }
|
||||||
|
|
||||||
|
public contentType(name: string) {
|
||||||
|
const res = lookup(name);
|
||||||
|
if (res) this.header('content-type', res);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
public header(name: string, val: string | string[]) {
|
public header(name: string, val: string | string[]) {
|
||||||
this.headers[name] = val;
|
this.headers[name] = val;
|
||||||
return this;
|
return this;
|
||||||
|
@ -97,11 +97,17 @@ export default class Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async attach(server: AsyncIterable<Deno.Conn>) {
|
public async attach(server: Deno.Listener) {
|
||||||
for await (const conn of server) {
|
while (true) {
|
||||||
for await (const req of Deno.serveHttp(conn)) {
|
for await (const conn of server) {
|
||||||
const r = await this.handle(RestRequest.fromMessage(req));
|
(async () => {
|
||||||
if (r) req.respondWith(r.toFetchResponse());
|
for await (const req of Deno.serveHttp(conn)) {
|
||||||
|
console.log(req.request.url);
|
||||||
|
const r = await this.handle(RestRequest.fromMessage(req));
|
||||||
|
console.log(req.request.url);
|
||||||
|
if (r) req.respondWith(r.toFetchResponse());
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export default function rest<KeyT extends keyof T & string, T extends Base<KeyT>
|
|||||||
args.push(arg);
|
args.push(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = r[key].apply(r, args);
|
const res = await r[key].apply(r, args);
|
||||||
if (res instanceof RestResponse) return res;
|
if (res instanceof RestResponse) return res;
|
||||||
return new RestResponse().body(await serialize(res));
|
return new RestResponse().body(await serialize(res));
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,16 @@ export default function staticHandler(path: string): Handler {
|
|||||||
return {
|
return {
|
||||||
async handle(req) {
|
async handle(req) {
|
||||||
try {
|
try {
|
||||||
const stream = await Deno.open(`${req.url}/${path}`);
|
const realPath = await Deno.realPath(`${path}/${req.url}`);
|
||||||
const res: Uint8Array[] = [];
|
const stream = await Deno.open(realPath);
|
||||||
for await (const bit of stream.readable) res.push(bit);
|
const i = realPath.lastIndexOf('.');
|
||||||
stream.close();
|
const res = new RestResponse().body(stream.readable);
|
||||||
return new RestResponse().body(new Blob(res).stream());
|
|
||||||
|
if (i >= 0) res.contentType(realPath.substring(i + 1));
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
catch {
|
catch (e) {
|
||||||
|
console.log(e);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,12 @@
|
|||||||
"maximumError": "4kb"
|
"maximumError": "4kb"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
|
79
frontend/src/app/services/users.service.ts
Normal file
79
frontend/src/app/services/users.service.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Injectable, WritableSignal, signal } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { first, firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
export enum Role {
|
||||||
|
Admin = 3,
|
||||||
|
Employer = 2,
|
||||||
|
User = 1,
|
||||||
|
API = 0,
|
||||||
|
Deactivated = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
username: string;
|
||||||
|
role: Role;
|
||||||
|
projects: string[];
|
||||||
|
}
|
||||||
|
export interface ThisUser extends User {
|
||||||
|
chats: string[];
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UsersService {
|
||||||
|
private readonly url = environment.apiURL + '/users';
|
||||||
|
public $user = signal<ThisUser | undefined>(undefined);
|
||||||
|
|
||||||
|
public get token() {
|
||||||
|
return localStorage.getItem('token') ?? undefined;
|
||||||
|
}
|
||||||
|
public set token(token: string | undefined) {
|
||||||
|
if (token === undefined) localStorage.removeItem('token');
|
||||||
|
else localStorage.setItem('token', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateUser() {
|
||||||
|
if (this.token) {
|
||||||
|
const user = await firstValueFrom(this.http.get<ThisUser>(`${this.url}/get?token=${this.token}`));
|
||||||
|
this.$user.set(user);
|
||||||
|
}
|
||||||
|
else this.$user.set(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logoff() {
|
||||||
|
if (!this.token) return;
|
||||||
|
await firstValueFrom(this.http.post(this.url + `/logout?token=${this.token}`, {}));
|
||||||
|
this.token = undefined;
|
||||||
|
this.$user.set(undefined);
|
||||||
|
}
|
||||||
|
public async login(username: string, password: string) {
|
||||||
|
await this.logoff();
|
||||||
|
const token = (await firstValueFrom(this.http.post<any>(this.url + `/login`, { username, password }))).token;
|
||||||
|
this.token = token;
|
||||||
|
await this.updateUser();
|
||||||
|
}
|
||||||
|
public async signup(username: string, password: string, email: string) {
|
||||||
|
await this.logoff();
|
||||||
|
await firstValueFrom(this.http.post(this.url + `/signup`, { username, password, email }));
|
||||||
|
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) {
|
||||||
|
this.updateUser();
|
||||||
|
}
|
||||||
|
}
|
8
frontend/src/environments/environment.prod.ts
Normal file
8
frontend/src/environments/environment.prod.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
declare function $__getHTTP(): string;
|
||||||
|
declare function $__getWS(): string;
|
||||||
|
|
||||||
|
export const environment = {
|
||||||
|
production: true,
|
||||||
|
apiURL: $__getHTTP(),
|
||||||
|
wsURL: $__getWS()
|
||||||
|
};
|
5
frontend/src/environments/environment.ts
Normal file
5
frontend/src/environments/environment.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
apiURL: 'http://127.0.0.1/api',
|
||||||
|
wsURL: 'ws://127.0.0.1/api'
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user