feat: add angular support to server

This commit is contained in:
TopchetoEU 2023-06-30 03:37:07 +03:00
parent 7318cac349
commit 9a63331a95
15 changed files with 162 additions and 17 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@
/backend/keys
/backend/images
/backend/static
/backend/pacakage.json
/frontend/dist

4
.vscode/launch.json vendored
View File

@ -8,8 +8,8 @@
"name": "Launch Backend",
"type": "pwa-node",
"request": "launch",
"program": "${workspaceFolder}/main.ts",
"cwd": "${workspaceFolder}/cwd",
"program": "${workspaceFolder}/backend/main.ts",
"cwd": "${workspaceFolder}/backend",
"runtimeExecutable": "deno",
"runtimeArgs": [
"run",

View File

@ -1,5 +1,6 @@
{
"deno.enable": true,
"deno.unstable": true,
"deno.enablePaths": [ "backend" ],
"typescript.tsdk": "node_modules\\typescript\\lib"
}

View File

@ -11,6 +11,7 @@ export default async function clonegur() {
}
catch {
salt = await bcrypt.genSalt();
await Deno.mkdir('keys', { recursive: true });
await Deno.writeFile('keys/salt.txt', new TextEncoder().encode(salt));
}

View File

@ -10,6 +10,7 @@ import { Headers } from "../server/RestRequest.ts";
import { convert } from "../server/decorators/schema.ts";
import { now } from "../utils/utils.ts";
import AppDatabase from "../AppDatabase.ts";
import RestResponse from "../server/RestResponse.ts";
export default class ImageRouter extends AppRouter {
public static deserialize(image: Image) {
@ -33,6 +34,23 @@ export default class ImageRouter extends AppRouter {
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')
async self(@page() page: Page) {
const res = await page.apply(this.db.images.find({})).toArray();

View File

@ -5,10 +5,19 @@ import RestResponse from "../server/RestResponse.ts";
import { rest, route } from "../server/decorators.ts";
import { stream } from "../utils/utils.ts";
import AppDatabase from "../AppDatabase.ts";
import staticHandler from "../server/staticHandler.ts";
export class RootRouter extends AppRouter {
@route('users/*') users;
@route('images/*') images;
@route('api/users/*') users;
@route('api/images/*') images;
@rest('GET', '/')
async index() {
return new RestResponse()
.body((await Deno.open('static/index.html')).readable)
.contentType('html');
}
@route('/*') static;
@rest('*', '*')
default() {
@ -18,6 +27,7 @@ export class RootRouter extends AppRouter {
constructor(salt: string, db: AppDatabase) {
super();
this.static = staticHandler('static');
this.users = new UserRouter(salt, db);
this.images = new ImageRouter(db);
}

View File

@ -44,7 +44,7 @@ export default class UserRouter extends AppRouter {
if (res === undefined) throw new HttpError('User not found.');
return this.deserialize(res);
return this.deserialize(res, true);
}
@rest('POST', '/signup')

View File

@ -1,3 +1,5 @@
import { lookup } from "https://deno.land/x/mrmime@v1.0.1/mod.ts"
import { stream } from "../utils/utils.ts";
import { Headers } from "./RestRequest.ts";
@ -14,6 +16,11 @@ export default class RestResponse {
public get statusMessage() { return this.#statusMsg; }
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[]) {
this.headers[name] = val;
return this;

View File

@ -97,12 +97,18 @@ export default class Router {
}
}
public async attach(server: AsyncIterable<Deno.Conn>) {
public async attach(server: Deno.Listener) {
while (true) {
for await (const conn of server) {
(async () => {
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());
}
})();
}
}
}
}

View File

@ -50,7 +50,7 @@ export default function rest<KeyT extends keyof T & string, T extends Base<KeyT>
args.push(arg);
}
const res = r[key].apply(r, args);
const res = await r[key].apply(r, args);
if (res instanceof RestResponse) return res;
return new RestResponse().body(await serialize(res));
}

View File

@ -5,13 +5,16 @@ export default function staticHandler(path: string): Handler {
return {
async handle(req) {
try {
const stream = await Deno.open(`${req.url}/${path}`);
const res: Uint8Array[] = [];
for await (const bit of stream.readable) res.push(bit);
stream.close();
return new RestResponse().body(new Blob(res).stream());
const realPath = await Deno.realPath(`${path}/${req.url}`);
const stream = await Deno.open(realPath);
const i = realPath.lastIndexOf('.');
const res = new RestResponse().body(stream.readable);
if (i >= 0) res.contentType(realPath.substring(i + 1));
return res;
}
catch {
catch (e) {
console.log(e);
return undefined;
}
}

View File

@ -48,6 +48,12 @@
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {

View 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();
}
}

View File

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

View File

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