Browse Source

Initial commit. Added login and register forms

master
David Ludwig 4 years ago
commit
fe8221aff3
39 changed files with 4320 additions and 0 deletions
  1. +9
    -0
      .env.example
  2. +118
    -0
      .gitignore
  3. +2
    -0
      Dockerfile
  4. +3
    -0
      README.md
  5. +13
    -0
      index.html
  6. +9
    -0
      nodemon.json
  7. +7
    -0
      ormconfig.json
  8. +48
    -0
      package.json
  9. +6
    -0
      postcss.config.js
  10. BIN
      public/favicon.ico
  11. +10
    -0
      src/app/App.vue
  12. +86
    -0
      src/app/components/CheckBox.vue
  13. +56
    -0
      src/app/components/TextBox.vue
  14. +17
    -0
      src/app/index.ts
  15. +26
    -0
      src/app/routes/index.ts
  16. +5
    -0
      src/app/shims-vue.d.ts
  17. +8
    -0
      src/app/styles/index.css
  18. +11
    -0
      src/app/views/Home.vue
  19. +54
    -0
      src/app/views/Login.vue
  20. +76
    -0
      src/app/views/Register.vue
  21. +105
    -0
      src/server/Application.ts
  22. +36
    -0
      src/server/common.ts
  23. +25
    -0
      src/server/database/entities/MovieTicket.ts
  24. +15
    -0
      src/server/database/entities/MovieTorrent.ts
  25. +11
    -0
      src/server/database/entities/RegisterToken.ts
  26. +24
    -0
      src/server/database/entities/User.ts
  27. +4
    -0
      src/server/database/entities/index.ts
  28. +11
    -0
      src/server/index.ts
  29. +40
    -0
      src/server/services/Database.ts
  30. +18
    -0
      src/server/services/DiscordBot.ts
  31. +52
    -0
      src/server/services/Service.ts
  32. +164
    -0
      src/server/services/TorrentClientIpc.ts
  33. +120
    -0
      src/server/services/WebServer.ts
  34. +24
    -0
      tailwind.config.js
  35. +74
    -0
      tsconfig.json
  36. +12
    -0
      tsconfig.server.json
  37. +22
    -0
      tsconfig.vite.json
  38. +15
    -0
      vite.config.ts
  39. +2984
    -0
      yarn.lock

+ 9
- 0
.env.example View File

@ -0,0 +1,9 @@
DB_TYPE = mysql
DB_HOST = database
DB_PORT = 3306
DB_USER = root
DB_PASSWORD_FILE = /run/secrets/mysql_root_password
DB_DATABASE = autoplex_request
SERVER_PORT = 3200
TORRENT_CLIENT_IPC_SOCKET = /tmp/torrent_client.sock

+ 118
- 0
.gitignore View File

@ -0,0 +1,118 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.data/
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
build
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

+ 2
- 0
Dockerfile View File

@ -0,0 +1,2 @@
FROM node:14-alpine AS base
WORKDIR /app

+ 3
- 0
README.md View File

@ -0,0 +1,3 @@
# Autoplex Request
A complete utility for accepting, managing, and fulfilling media requests.

+ 13
- 0
index.html View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Autoplex Torrent</title>
</head>
<body class="bg-blue-50">
<div id="app" class="contents"></div>
<script type="module" src="/src/app/index.ts"></script>
</body>
</html>

+ 9
- 0
nodemon.json View File

@ -0,0 +1,9 @@
{
"watch": ["src/server"],
"ext": "ts,json",
"ignore": ["src/**/*.spec.ts"],
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register src/server/index.ts",
"events": {
"start": "clear"
}
}

+ 7
- 0
ormconfig.json View File

@ -0,0 +1,7 @@
{
"cli": {
"entitiesDir": "src/database/entities",
"migrationsDir": "src/database/migrations",
"subscribersDir": "src/database/subscribers"
}
}

+ 48
- 0
package.json View File

@ -0,0 +1,48 @@
{
"name": "request",
"version": "0.0.0",
"keywords": [],
"author": "David Ludwig",
"license": "ISC",
"main": "./build/server/index.js",
"scripts": {
"clean": "rimraf ./build",
"dev": "vite",
"build": "yarn run build:backend && yarn run build:frontend",
"build:backend": "tsc -p ./tsconfig.server.json",
"build:frontend": "vue-tsc --noEmit -p ./tsconfig.vite.json && vite build",
"start": "NODE_ENV=production node .",
"start:dev": "nodemon"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"fastify": "^3.14.1",
"fastify-formbody": "^5.0.0",
"fastify-http-proxy": "^5.0.0",
"fastify-multipart": "^4.0.3",
"fastify-static": "^4.0.1",
"jsonwebtoken": "^8.5.1",
"mysql": "^2.18.1",
"node-ipc": "^9.1.4",
"typeorm": "^0.2.32",
"vue": "^3.0.5",
"vue-router": "^4.0.6",
"vuedraggable": "^4.0.1",
"websocket": "^1.0.33"
},
"devDependencies": {
"@types/jsonwebtoken": "^8.5.1",
"@types/node-ipc": "^9.1.3",
"@vitejs/plugin-vue": "^1.2.1",
"@vue/compiler-sfc": "^3.0.5",
"autoprefixer": "^10.2.5",
"nodemon": "^2.0.7",
"postcss": "^8.2.9",
"rimraf": "^3.0.2",
"tailwindcss": "^2.1.1",
"ts-node": "^9.1.1",
"typescript": "^4.1.3",
"vite": "^2.1.5",
"vue-tsc": "^0.0.15"
}
}

+ 6
- 0
postcss.config.js View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico View File

Before After

+ 10
- 0
src/app/App.vue View File

@ -0,0 +1,10 @@
<template>
<div class="min-h-full p-4 flex flex-col">
<router-view></router-view>
</div>
</template>
<script lang="ts">
// import { defineComponent } from 'vue'
// export default defineComponent({});
</script>

+ 86
- 0
src/app/components/CheckBox.vue View File

@ -0,0 +1,86 @@
<template>
<label class="checkbox-label relative flex items-center">
<input type="checkbox">
<span></span>
<span v-if="label" class="ml-2">{{label}}</span>
</label>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: {
label: {
type: String,
required: true
}
}
});
</script>
<style>
.checkbox-label input[type="checkbox"] {
width: 0;
height: 0;
}
.checkbox-label input[type="checkbox"]:focus-visible + span {
@apply ring ring-offset-2 ring-indigo-500;
}
.checkbox-label input[type="checkbox"] + span {
@apply inline-block w-4 h-4 bg-white border rounded-sm border-gray-300;
}
.checkbox-label input[type="checkbox"]:checked + span {
@apply bg-indigo-500 border-indigo-500;
}
.checkbox-label input[type="checkbox"] + span::before {
content: '';
position: absolute;
border-color: white;
border-width: 0;
/* transform: translateY(0.3rem) rotate(45deg); */
}
.checkbox-label input[type="checkbox"]:checked + span::before {
border-width: 0 0px 2px 2px;
width: .5rem;
height: .25rem;
transform: translateX(calc(50% - 1px)) translateY(calc(0.3rem - 1px)) rotate(-45deg);
}
@keyframes shrink-bounce{
0%{
transform: scale(1);
}
33%{
transform: scale(.85);
}
100%{
transform: scale(1);
}
}
@keyframes checkbox-check{
0%{
width: 0;
height: 0;
border-color: #212121;
transform: translate3d(0,0,0) rotate(45deg);
}
33%{
width: .2em;
height: 0;
transform: translate3d(0,0,0) rotate(45deg);
}
100%{
width: .2em;
height: .5em;
border-color: #212121;
transform: translate3d(0,-.5em,0) rotate(45deg);
}
}
</style>

+ 56
- 0
src/app/components/TextBox.vue View File

@ -0,0 +1,56 @@
<template>
<div>
<label class="block text-sm mb-1 text-gray-700" :class="{ 'opacity-50': disabled, 'text-red-600': error }">
{{ label }}
</label>
<div class="block relative">
<input :type="type" :placeholder="placeholder" v-model="inputValue" ref="textbox" :disabled="disabled"
class="w-full outline-none p-2 rounded-lg bg-gray-100 border border-gray-100 text-gray-800 placeholder-gray-400 disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-gray-50 disabled:border-gray-400"
:class="{ 'pr-10 border-red-600 text-red-600': (valid || error) }">
<span class="w-10 absolute flex items-center top-0 bottom-0 right-0 justify-center">
<i v-if="error" class="fas fa-exclamation-circle align-middle text-red-600"></i>
<i v-else-if="valid" class="fas fa-check-circle align-middle text-green-500"></i>
</span>
</div>
<div class="w-full error-message text-xs text-red-600 text-center" ref="error">{{ errorMessage }}</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
data() {
return {
inputValue: ""
}
},
props: {
disabled: {
type: Boolean,
default: false
},
error: {
type: Boolean,
default: false
},
errorMessage: {
type: String,
default: ""
},
label: String,
placeholder: {
type: String,
default: ""
},
type: {
type: String,
default: "text"
},
valid: {
type: Boolean,
default: false
}
}
});
</script>

+ 17
- 0
src/app/index.ts View File

@ -0,0 +1,17 @@
import { createApp } from 'vue'
import router from "./routes";
import App from './App.vue'
import "./styles/index.css";
let app = createApp(App);
app.use(router);
app.mount("#app");
/**
* Dropdown menus
*/
document.addEventListener("click", (event) => {
let dropdownMenus = document.querySelectorAll(".dropdown.active").forEach((elem) => {
elem.classList.remove("active");
});
});

+ 26
- 0
src/app/routes/index.ts View File

@ -0,0 +1,26 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
const routes: RouteRecordRaw[] = [
{
path: "/",
name: "Home",
component: () => import("../views/Home.vue")
},
{
path: "/login",
name: "Login",
component: () => import("../views/Login.vue")
},
{
path: "/register",
name: "Register",
component: () => import("../views/Register.vue"),
}
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;

+ 5
- 0
src/app/shims-vue.d.ts View File

@ -0,0 +1,5 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

+ 8
- 0
src/app/styles/index.css View File

@ -0,0 +1,8 @@
@import '@fortawesome/fontawesome-free/css/all.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
html, body {
@apply h-full;
}

+ 11
- 0
src/app/views/Home.vue View File

@ -0,0 +1,11 @@
<template>
<div></div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
components: {}
});
</script>

+ 54
- 0
src/app/views/Login.vue View File

@ -0,0 +1,54 @@
<template>
<div class="w-full max-w-sm lg:mx-auto lg:my-auto p-6 space-y-4 bg-white rounded-lg shadow-md">
<div class="text-center font-thin text-4xl py-4">AUTOPLEX</div>
<div class="font-medium text-center text-xl">Sign in</div>
<form>
<div class="space-y-4">
<div>
<text-box label="Email" type="email" placeholder="john@example.com" :error-message="errors.email"/>
</div>
<div>
<text-box label="Password" type="password" placeholder="············" :error-message="errors.password"/>
</div>
<div>
<check-box label="Remember Me"/>
</div>
<div>
<button @click="login" class="block w-full rounded-full bg-indigo-500 text-white p-2 focus:outline-none">Sign In</button>
</div>
</div>
</form>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import CheckBox from "../components/CheckBox.vue";
import TextBox from "../components/TextBox.vue";
export default defineComponent({
components: {
CheckBox,
TextBox
},
data() {
return {
errors: {
email: "",
password: ""
}
}
},
methods: {
login() {
}
}
});
</script>
<style>
button:focus-visible {
@apply ring-2 ring-indigo-500 ring-offset-2;
}
</style>

+ 76
- 0
src/app/views/Register.vue View File

@ -0,0 +1,76 @@
<template>
<div class="w-full max-w-sm lg:mx-auto lg:my-auto p-6 space-y-4 bg-white rounded-lg shadow-md">
<div class="text-center font-thin text-4xl py-4">AUTOPLEX</div>
<div class="font-medium text-center text-xl">Sign Up</div>
<form>
<div class="space-y-4">
<div>
<text-box label="Acess Token" type="text" ref="token" disabled/>
</div>
<div>
<text-box label="Your Name" type="text" ref="name" placeholder="John Doe" :error-message="errors.email"/>
</div>
<div>
<text-box label="Email" type="email" ref="email" placeholder="john@example.com" :error-message="errors.email"/>
</div>
<div>
<text-box label="Password" type="password" ref="password" placeholder="············" :error-message="errors.password"/>
</div>
<div>
<text-box label="Re-type Password" type="password" ref="password" placeholder="············" :error-message="errors.password"/>
</div>
<div>
<button type="submit" class="block w-full rounded-full bg-indigo-500 text-white p-2 focus:outline-none">Register</button>
</div>
</div>
</form>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import CheckBox from "../components/CheckBox.vue";
import TextBox from "../components/TextBox.vue";
export default defineComponent({
components: {
CheckBox,
TextBox
},
computed: {
accessToken(): string {
return <string>this.$route.query["access_token"];
}
},
data() {
return {
errors: {
email: "",
password: ""
}
}
},
methods: {
login() {
}
},
mounted() {
(<any>this.$refs["token"]).inputValue = this.accessToken;
console.log("The token is", this.accessToken);
},
beforeRouteEnter(to, from, next) {
if (to.query["access_token"] === undefined) {
next({ name: "Login" });
} else {
next();
}
}
});
</script>
<style>
button:focus-visible {
@apply ring-2 ring-indigo-500 ring-offset-2;
}
</style>

+ 105
- 0
src/server/Application.ts View File

@ -0,0 +1,105 @@
import { randomBytes } from "crypto";
import { promisify } from "util";
import DiscordBot from "./services/DiscordBot";
import Service from "./services/Service";
import TorrentClientIpc from "./services/TorrentClientIpc";
import WebServer from "./services/WebServer";
import { User, RegisterToken } from "./database/entities";
import Database from "./services/Database";
/**
* @TODO Not sure where to put this yet... here's fine for now
*/
async function createRegisterToken() {
let randomBytesPromise = promisify(randomBytes);
let token = new RegisterToken();
token.token = (await randomBytesPromise(48)).toString("hex");
await token.save();
return token;
}
/**
* The main application class
*/
export default class Application
{
/**
* All available services
*/
protected services: Service[];
/**
* The database service
*/
protected database: Database;
/**
* The discord bot service
*/
// protected discord: DiscordBot;
/**
* The webserver service
*/
protected web: WebServer;
/**
* The torrent client service
*/
// protected torrent: TorrentClientIpc;
public constructor() {
this.services = [
this.database = new Database(this),
// this.discord = new DiscordBot(this),
this.web = new WebServer(this),
// this.torrent = new TorrentClientIpc(this)
]
}
/**
* Boot all of the services
*/
protected async boot() {
return Promise.all(this.services.map(service => service.boot()));
}
/**
* Initialize the application if necessary
*/
protected async initialize() {
let numUsers = await User.count();
if (numUsers == 0) {
console.log("Found 0 users");
let token = await RegisterToken.findOne();
if (token === undefined) {
token = await createRegisterToken();
}
console.log("First time register with: ", token.token);
}
}
/**
* Shutdown the application
*/
protected shutdown() {
return Promise.all(this.services.map(service => service.shutdown()));
}
/**
* Start the application
*/
public async start() {
await this.boot();
await this.initialize();
this.services.forEach(service => service.start());
}
/**
* Quit the application
*/
public async quit(code: number = 0) {
await this.shutdown();
process.exit(code);
}
}

+ 36
- 0
src/server/common.ts View File

@ -0,0 +1,36 @@
export interface ITorrent {
name: string,
infoHash: string,
progress: number,
state: TorrentState
}
export enum TorrentState {
Ready = 0x1,
Paused = 0x2,
Done = 0x4
}
export interface ISerializedTorrent {
name : string;
infoHash : string;
downloaded : number;
uploaded : number;
ratio : number;
size : number;
downloadSpeed: number;
uploadSpeed : number;
numPeers : number;
progress : number;
path : string;
state : TorrentState;
files : ISerializedFile[];
}
export interface ISerializedFile {
path : string;
size : number;
downloaded: number;
progress : number;
selected : boolean;
}

+ 25
- 0
src/server/database/entities/MovieTicket.ts View File

@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, OneToMany } from "typeorm";
import { MovieTorrent } from "./MovieTorrent";
import { User } from "./User";
@Entity()
export class MovieTicket extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
imdbId!: string;
@Column()
name!: string;
@Column()
number!: number;
@ManyToOne(() => User, user => user.movieTickets)
user!: User;
@OneToMany(() => MovieTorrent, torrent => torrent.movieTicket)
torrents!: MovieTorrent[];
}

+ 15
- 0
src/server/database/entities/MovieTorrent.ts View File

@ -0,0 +1,15 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne } from "typeorm";
import { MovieTicket } from "./MovieTicket";
@Entity()
export class MovieTorrent extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
infoHash!: string;
@ManyToOne(() => MovieTicket, ticket => ticket.torrents)
movieTicket!: MovieTicket;
}

+ 11
- 0
src/server/database/entities/RegisterToken.ts View File

@ -0,0 +1,11 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne } from "typeorm";
@Entity()
export class RegisterToken extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
token!: string
}

+ 24
- 0
src/server/database/entities/User.ts View File

@ -0,0 +1,24 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, OneToMany } from "typeorm";
import { MovieTicket } from "./MovieTicket";
@Entity()
export class User extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
imdbId!: string;
@Column()
firstName!: string;
@Column()
lastName!: string;
@Column()
number!: number;
@OneToMany(() => User, user => user.movieTickets)
movieTickets!: MovieTicket[];
}

+ 4
- 0
src/server/database/entities/index.ts View File

@ -0,0 +1,4 @@
export * from "./MovieTicket";
export * from "./MovieTorrent";
export * from "./RegisterToken";
export * from "./User";

+ 11
- 0
src/server/index.ts View File

@ -0,0 +1,11 @@
import Application from "./Application";
/**
* Create a new application instance
*/
let app = new Application();
/**
* Start the application
*/
app.start();

+ 40
- 0
src/server/services/Database.ts View File

@ -0,0 +1,40 @@
import { Connection, createConnection } from "typeorm";
import * as entities from "../database/entities";
import Service from "./Service";
import { readFile } from "fs/promises";
export default class Database extends Service
{
/**
* The active database connection
*/
protected connection!: Connection;
/**
* Boot the database service
*/
public async boot() {
// Fetch the database password from the secret file
let password = (await readFile(<string>process.env["DB_PASSWORD_FILE"])).toString().trim();
// Create the database connection
await createConnection({
type : <"mysql" | "mariadb">process.env["DB_TYPE"],
host : process.env["DB_HOST"],
port : parseInt(<string>process.env["DB_PORT"]),
username : process.env["DB_USER"],
password : password,
database : process.env["DB_DATABASE"],
synchronize: process.env["NODE_ENV"] != "production",
entities : Object.values(entities),
migrations : ["src/migrations/*.ts"]
});
}
/**
* Shutdown the database service
*/
public async shutdown() {
await this.connection.close();
}
}

+ 18
- 0
src/server/services/DiscordBot.ts View File

@ -0,0 +1,18 @@
import Application from "../Application";
import Service from "./Service";
export default class DiscordBot extends Service
{
public constructor(app: Application) {
super(app);
}
public async boot() {
}
public async shutdown() {
}
}

+ 52
- 0
src/server/services/Service.ts View File

@ -0,0 +1,52 @@
import Application from "../Application";
export default abstract class Service
{
/**
* The application instance
*/
protected readonly app: Application;
/**
* Enable/disable logging for this service
*/
public logging: boolean = true;
/**
* Create a new service
*/
public constructor(app: Application) {
this.app = app;
}
// Required Service Implementation -------------------------------------------------------------
/**
* Boot the service
*/
public abstract boot(): Promise<void>;
/**
* Shut the application down
*/
public abstract shutdown(): Promise<void>;
// Miscellaneous ------------------------------------------------------------------------------
/**
* Indicate the application is ready
*/
public start() {
// no-op
};
/**
* Log to the console
*/
protected log(...args: any[]) {
if (!this.logging) {
return;
}
console.log(...args);
}
}

+ 164
- 0
src/server/services/TorrentClientIpc.ts View File

@ -0,0 +1,164 @@
import ipc from "node-ipc";
import { Socket } from "net";
import { ISerializedTorrent, ITorrent } from "../common";
import Service from "./Service";
import Application from "../Application";
interface IResponse {
response?: any,
error?: string | Error
}
export default class TorrentClientIpc extends Service
{
/**
* Indicate if there is an active connection to the IPC
*/
private __isConnected: boolean;
/**
* The active IPC socket
*/
protected socket!: Socket;
/**
* Create a new IPC client for the torrent client
*/
constructor(app: Application) {
super(app);
ipc.config.id = "request";
ipc.config.retry = 1500;
ipc.config.silent = true;
this.__isConnected = false;
}
/**
* Boot the torrent client IPC service
*/
public boot() {
return new Promise<void>((resolve, reject) => {
ipc.connectTo("torrent_client", process.env["TORRENT_CLIENT_IPC_SOCKET"], () => {
this.socket = ipc.of["torrent_client"];
this.installSocketEventHandlers(this.socket);
this.installSocketMessageHandlers(this.socket);
resolve();
});
});
}
public async shutdown() {
ipc.disconnect("torrent_client");
}
/**
* Install the event handlers for the IPC socket
*/
protected installSocketEventHandlers(socket: Socket) {
socket.on("connect", () => this.onConnect());
socket.on("error", (error: any) => this.onError(error));
socket.on("disconnect", () => this.onDisconnect());
socket.on("destroy", () => this.onDestroy());
}
protected installSocketMessageHandlers(socket: Socket) {
}
// Socket Event Handlers -----------------------------------------------------------------------
protected onConnect() {
console.log("IPC: Connection established");
this.__isConnected = true;
}
protected onError(error: string | Error) {
console.log("IPC: Error occurred:", error);
}
protected onDisconnect() {
console.log("IPC: Disconnected");
this.__isConnected = false;
}
protected onDestroy() {
console.log("IPC: Destroyed");
}
// Methods -------------------------------------------------------------------------------------
/**
* Perform a general request to the torrent client
*/
protected async request(method: string, message?: any) {
return new Promise<IResponse>((resolve, reject) => {
if (!this.isConnected) {
throw new Error("Not connected to torrent client");
}
let respond = (response: any) => {
clearTimeout(timeout);
resolve(response);
}
// Include timeout mechanism in the off chance something breaks
let timeout = setTimeout(() => {
this.socket.off(method, respond);
reject("Torrent client IPC request timeout")
}, 1000);
this.socket.once(method, respond);
this.socket.emit(method, message);
});
}
/**
* Add a torrent to the client
* @param torrent Magnet URI or file buffer
*/
public async add(torrent: string | Buffer) {
let response = await this.request("add", torrent);
if (response.error) {
throw new Error("Failed to add torrent");
}
return <string>response.response;
}
/**
* Remove a torrent from the client
* @param torrent Torrent info hash
*/
public async remove(torrent: string) {
let response = await this.request("remove", torrent);
if (response.error) {
throw new Error("Failed to remove torrent");
}
}
/**
* Get a list of all torrents in the client
*/
public async list() {
let response = await this.request("list");
if (response.error) {
console.error(response.error);
throw new Error("Failed to obtain torrent list");
}
return <ITorrent[]>response.response;
}
/**
* Get full details of each of the provided torrents
* @param torrentIds Array of torrent info hashes
*/
public async details(...torrentIds: string[]) {
let response = await this.request("details", torrentIds);
if (response.error) {
console.error(response.error);
throw new Error("Failed to retrieve torrent details");
}
return <ISerializedTorrent[]>response.response;
}
// Accessors -----------------------------------------------------------------------------------
get isConnected() {
return this.__isConnected;
}
}

+ 120
- 0
src/server/services/WebServer.ts View File

@ -0,0 +1,120 @@
import fastify from "fastify";
import fastifyFormBody from "fastify-formbody";
import fastifyHttpProxy from "fastify-http-proxy";
import fastifyMultipart from "fastify-multipart";
import fastifyStatic from "fastify-static";
import Application from "../Application";
import Service from "./Service";
import { join } from "path";
import { RegisterToken } from "../database/entities";
export default class WebServer extends Service
{
/**
* The port to host the webserver on
*/
protected readonly port: number;
/**
* The internal webserver instance
*/
protected server: ReturnType<typeof fastify>;
/**
* Create a new webserver instance
*/
public constructor(app: Application) {
super(app);
this.port = parseInt(<string>process.env["SERVER_PORT"]);
this.server = fastify();
this.registerPlugins();
}
/**
* Register required Fastify plugins
*/
protected registerPlugins() {
this.server.register(fastifyMultipart, {
limits: {
fileSize: 16*1024*1024,
files: 50
}
});
this.server.register(fastifyFormBody);
}
/**
* Boot the webserver
*/
public async boot() {
// Register the available routes
this.registerRoutes();
}
/**
* Start the webserver
*/
public start() {
// Start listening
this.server.listen(this.port, "0.0.0.0");
this.log("Webserver listening on port:", this.port);
}
/**
* Shutdown the webserver
*/
public async shutdown() {
this.log("Webserver shutting down");
await this.server.close();
}
// ---------------------------------------------------------------------------------------------
/**
* Register all route groups
*/
protected registerRoutes() {
this.registerSpaRoutes();
this.registerApiRoutes();
}
/**
* Register the routes required for the single-page application
*/
protected registerSpaRoutes() {
/**
* If the app is in production mode, serve static assets.
* If the app is in development mode, forward 404's to Vite.
* NOTE: Static assets may be moved to nginx later... not sure yet
*/
if (process.env["NODE_ENV"] == "production") {
this.server.register(fastifyStatic, {
root: join(__dirname, "../../public")
});
this.server.setNotFoundHandler((request, reply) => {
console.log("Replying with default");
return reply.sendFile("index.html");
});
} else {
console.log("Using proxy");
this.server.register(fastifyHttpProxy, {
upstream: "http://localhost:3001"
});
}
}
/**
* Register all available API routes
*/
protected registerApiRoutes() {
this.server.get("/validate/register_token/:token", async (request, reply) => {
let token: string = (<any>request.params)["token"];
return {
is_valid: Boolean(token) && await RegisterToken.count({token}) > 0
}
});
this.server.get("/api/movie/search", async (request, reply) => {
});
}
}

+ 24
- 0
tailwind.config.js View File

@ -0,0 +1,24 @@
module.exports = {
purge: ["./index.html", "src/app/*.{vue,ts}"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
colors: {
ui: {
bg: "#191726",
fg: "#202332",
}
},
},
},
variants: {
extend: {
cursor: ["disabled"],
backgroundColor: ["disabled"],
opacity: ["disabled"],
borderWidth: ["disabled"],
borderColor: ["disabled"]
},
},
plugins: [],
}

+ 74
- 0
tsconfig.json View File

@ -0,0 +1,74 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
// "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./build", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": [
"src"
]
}

+ 12
- 0
tsconfig.server.json View File

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"outDir": "build/server", /* Redirect output structure to the directory. */
"sourceRoot": "src/server" /* Specify the location where debugger should locate TypeScript files instead of source locations. */
},
"exclude": [
"src/app"
]
}

+ 22
- 0
tsconfig.vite.json View File

@ -0,0 +1,22 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"jsx": "preserve",
"resolveJsonModule": true,
"lib": ["esnext", "dom"],
"types": ["vite/client"],
"sourceRoot": "src/app"
},
"include": [
"src/app/**/*.ts",
"src/app**/*.d.ts",
"src/app/**/*.tsx",
"src/app/**/*.vue"
],
"exclude": [
"src/server"
]
}

+ 15
- 0
vite.config.ts View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
manifest: true,
outDir: "./build/public"
},
server: {
port: 3001
}
})

+ 2984
- 0
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save