Browse Source

Movie searching and request modal designs

master
David Ludwig 4 years ago
parent
commit
aaadfc7f9e
23 changed files with 543 additions and 51 deletions
  1. +4
    -0
      .env.example
  2. +34
    -2
      src/app/auth.ts
  3. +149
    -0
      src/app/components/MovieModal.vue
  4. +46
    -0
      src/app/util.ts
  5. +88
    -7
      src/app/views/Home.vue
  6. +1
    -1
      src/app/views/Login.vue
  7. +7
    -0
      src/common/api_schema.ts
  8. +21
    -0
      src/common/validation.ts
  9. +2
    -2
      src/server/index.ts
  10. +48
    -1
      src/server/services/MovieSearch.ts
  11. +35
    -2
      src/server/services/TvDb.ts
  12. +1
    -1
      src/server/services/WebServer/index.ts
  13. +2
    -0
      src/server/services/WebServer/middleware/auth.ts
  14. +11
    -7
      src/server/services/WebServer/middleware/index.ts
  15. +14
    -0
      src/server/services/WebServer/requests/MovieSearchRequest.ts
  16. +14
    -0
      src/server/services/WebServer/routes/RouteRegisterFactory.ts
  17. +19
    -3
      src/server/services/WebServer/routes/api.ts
  18. +13
    -11
      src/server/services/WebServer/routes/auth.ts
  19. +0
    -3
      src/server/services/WebServer/validators.ts
  20. +23
    -9
      src/server/util.ts
  21. +2
    -1
      tailwind.config.js
  22. +4
    -1
      tsconfig.json
  23. +5
    -0
      yarn.lock

+ 4
- 0
.env.example View File

@ -6,9 +6,13 @@ APP_KEY_FILE = /run/secrets/app_key
# Discord bot token
DISCORD_BOT_KEY_FILE = /run/secrets/discord_bot_key
# The Movie DB key
TMDB_KEY_FILE = /run/secrets/tmdb_key
# TVDB API key
TVDB_KEY_FILE = /run/secrets/tvdb_key
TVDB_PIN =
TVDB_REFRESH_PERIOD = 480 # (8 hours) How frequently to refresh the token (in minutes)
# Database -----------------------------------------------------------------------------------------


+ 34
- 2
src/app/auth.ts View File

@ -1,6 +1,7 @@
import jwtDecode from "jwt-decode";
import router from "./routes";
interface IUser {
export interface IUser {
id: number,
name: string,
isAdmin: boolean
@ -9,7 +10,7 @@ interface IUser {
/**
* The active JWT
*/
let token: string | null;
let token: string | null;
/**
* The decoded user object
@ -50,6 +51,7 @@ export function loadToken() {
user = null;
return false;
}
console.log("Token loaded", token);
return true;
}
@ -77,3 +79,33 @@ export function storeToken(jwtToken: string) {
}
return true;
}
/**
* Fetch request providing authentication and logout upon unauthorized requests
*/
export async function authFetch(path: string, options: RequestInit = {}): Promise<Response> {
console.log("Performing auth fetch");
options.credentials = "include";
options.headers = Object.assign(options.headers ?? {}, {
"Authorization": `Bearer ${token}`
});
let response = await fetch(path, options);
if (response.status === 401) {
// forgetToken();
console.log("Forgetting token...");
router.push({ name: "Login" });
throw Error("Unauthorized");
}
return response;
}
/**
* @TODO Remove later
*/
(<any>window).forgetToken = forgetToken;
(<any>window).authFetch = authFetch;
/**
* Ensure the token is loaded upon startup
*/
loadToken();

+ 149
- 0
src/app/components/MovieModal.vue View File

@ -0,0 +1,149 @@
<template>
<div class="modal p-8 overflow-scroll items-center">
<div class="w-full max-w-6xl bg-white overflow-hidden rounded-xl flex-col flex-shrink-0 shadow-xl">
<div class="bg-center bg-cover" :class="{ 'text-white': isDark }"
:style="`background-image: url('/api/tmdb/image/w1280${movie.backdrop_path}')`">
<div :style="backdropOverlayStyle" class="flex">
<div class="flex p-4">
<div class="w-24 h-40 flex-shrink-0 md:h-auto md:w-72 rounded-lg md:rounded-xl overflow-hidden shadow-md mdshadow-xl">
<i v-if="!movie.poster_path" class="fas fa-image mx-auto my-auto"></i>
<img v-else class="w-full h-full object-cover"
:src="`/api/tmdb/image/w342${movie.poster_path}`">
</div>
<div class="p-4 flex flex-col justify-center">
<h2 class="text-lg md:text-4xl">
<span class="font-bold">{{ movie.title }}</span>
<span class="opacity-70"> ({{ releaseYear }})</span>
</h2>
<div class="flex font-light dot-separated">
<span>{{ releaseDate }}</span>
<span>{{ runtime }}</span>
</div>
<div v-if="movieDetails?.overview" class="mt-4 hidden md:block">
<p class="">{{movieDetails?.overview}}</p>
</div>
<div class="mt-4 hidden md:block">
<button class="py-2 px-8 rounded-full" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }">Request</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 space-y-4 md:hidden">
<div v-if="movieDetails?.overview">
<h3 class="font-bold">Overview</h3>
<p>{{ movieDetails?.overview }}</p>
</div>
<div class="text-center">
<button class="py-2 px-8 rounded-full bg-red-500 text-white">Request</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IApiDataResponse } from "@common/api_schema";
import { IMovieDetails } from "@lib/tmdb/schema";
import { authFetch } from "../auth";
import { getAverageRgb } from "../util";
type MovieDetails = IApiDataResponse<IMovieDetails>;
export default defineComponent({
computed: {
averageRgb(): { r: number, g: number, b: number } {
let rgb = getAverageRgb(this.imgElem);
// Adjust contrast
let c = 60;
let f = 259*(c + 255) / (255*(259 - c));
for (let k in rgb) {
rgb[k] = Math.min(Math.max(f*(rgb[k] - 128) + 128, 0), 255);
}
// Adjust brightness
for (let k in rgb) {
rgb[k] *= 0.90;
}
return rgb;
},
backdropOverlayStyle(): string {
let { r, g, b } = this.averageRgb;
return `background: linear-gradient(to right, rgba(${r}, ${g}, ${b}, 1.0), rgba(${r}, ${g}, ${b}, 0.84)`;
},
isDark(): boolean {
let { r, g, b } = this.averageRgb;
return (r*0.299 + g*0.587 + b*0.114) <= 186;
// r /= 255;
// g /= 255;
// b /= 255;
// r = r <= 0.03928 ? r/12.92 : Math.pow((r+0.055)/1.055, 2.4);
// g = g <= 0.03928 ? r/12.92 : Math.pow((r+0.055)/1.055, 2.4);
// b = b <= 0.03928 ? r/12.92 : Math.pow((r+0.055)/1.055, 2.4);
// let L = 0.2126*r + 0.7152*g + 0.0722*b;
// return L <= 0.179;
},
releaseYear(): string {
if (!this.movieDetails || !this.movieDetails.release_date) {
return "";
}
return this.movieDetails.release_date.slice(0, 4);
},
releaseDate(): string {
if (!this.movieDetails || !this.movieDetails.release_date) {
return "";
}
let [year, month, day] = this.movieDetails.release_date.split('-');
return `${month}/${day}/${year}`;
},
runtime(): string {
if (!this.movieDetails || !this.movieDetails.runtime) {
return "";
}
let hours = Math.floor(this.movieDetails.runtime / 60);
let minutes = Math.floor(this.movieDetails.runtime % 60);
return (hours > 0 ? `${hours}h ` : "") + `${minutes}m`;
}
},
data() {
return {
movieDetails: <IMovieDetails|undefined>undefined
}
},
methods: {
async displayMovie() {
let response = <MovieDetails> await (authFetch(`/api/movie/details/${this.movie.id}`).then(response => response.json()));
this.movieDetails = response.data;
console.log(response);
}
},
mounted() {
this.displayMovie();
},
props: {
imgElem: {
type: HTMLImageElement,
required: true
},
movie: {
type: Object,
required: true
}
}
});
</script>
<style>
.modal {
@apply fixed inset-0 flex flex-col;
background: rgba(0, 0, 0, 0.5);
}
.dot-separated > *:not(:first-child)::before {
@apply px-2;
content: '\2022'
}
</style>

+ 46
- 0
src/app/util.ts View File

@ -3,3 +3,49 @@ import { single as validate } from "validate.js";
export function validateValue(value: string, constraints: any) {
return <string>(validate(value || null, constraints) ?? [""])[0];
}
export function getAverageRgb(imgEl: HTMLImageElement) {
var blockSize = 5, // only visit every 5 pixels
defaultRGB = {r:0,g:0,b:0}, // for non-supporting envs
canvas = document.createElement('canvas'),
context = canvas.getContext && canvas.getContext('2d'),
data, width, height,
i = -4,
length,
rgb = {r:0,g:0,b:0},
count = 0;
if (!context) {
return defaultRGB;
}
height = canvas.height = imgEl.naturalHeight || imgEl.offsetHeight || imgEl.height;
width = canvas.width = imgEl.naturalWidth || imgEl.offsetWidth || imgEl.width;
context.drawImage(imgEl, 0, 0);
try {
data = context.getImageData(0, 0, width, height);
} catch(e) {
/* security error, img on diff domain */alert('x');
return defaultRGB;
}
length = data.data.length;
while ( (i += blockSize * 4) < length ) {
++count;
rgb.r += data.data[i];
rgb.g += data.data[i+1];
rgb.b += data.data[i+2];
}
// ~~ used to floor values
rgb.r = ~~(rgb.r/count);
rgb.g = ~~(rgb.g/count);
rgb.b = ~~(rgb.b/count);
return rgb;
}

+ 88
- 7
src/app/views/Home.vue View File

@ -1,20 +1,101 @@
<template>
<div class="mx-auto my-auto flex flex-col">
<span class="text-2xl">Welcome, {{ name }}</span>
<router-link :to="{name: 'Logout'}" class="bg-red-500 text-white p-3 rounded-lg shadow-md focus:outline-none hover:bg-red-600 text-center">Sign Out</router-link>
</div>
<form @submit.prevent="search">
<div class="flex rounded-xl overflow-hidden shadow-md mb-8">
<input type="text" v-model="searchValue" :placeholder="exampleTitles[Math.floor(exampleTitles.length*Math.random())]"
class="w-full outline-none p-2 bg-white 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">
<button class="py-3 px-6 bg-indigo-500 text-white" type="submit">Search</button>
</div>
</form>
<ul class="w-full grid gap-8 grid-cols-2 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10 self-center ">
<li class="inline-block" v-for="(movie, index) in movies" @click="activeMovie = index">
<div class="w-full h-full flex shadow-lg bg-gray-300 text-gray-500 text-4xl overflow-hidden rounded-xl cursor-pointer motion-safe:transform hover:scale-105 transition-transform ease-out">
<i v-if="!movie.poster_path" class="fas fa-image mx-auto my-auto"></i>
<img v-else class="w-full h-full object-cover" loading="lazy" :ref="`poster-${index}`"
:src="`/api/tmdb/image/w185${movie.poster_path}`">
</div>
<div>
<h3></h3>
</div>
</li>
</ul>
<movie-modal v-if="activeMovie != -1" :movie="movies[activeMovie]" :img-elem="$refs[`poster-${activeMovie}`]"/>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import * as auth from "../auth";
import { IApiDataResponse } from "@common/api_schema";
import { IMovieDetails, IMovieSearchResult, IPaginatedResponse } from "@lib/tmdb/schema";
import MovieModal from "../components/MovieModal.vue";
import { authFetch } from "../auth";
type MovieResults = IApiDataResponse<IPaginatedResponse<IMovieSearchResult>>;
export default defineComponent({
components: {},
components: {
MovieModal
},
data() {
return {
name: auth.getUser().name.split(' ')[0]
activeMovie: -1,
exampleTitles: ["John Wick", "The Incredibles 2", "Stand by Me", "Shawshank Redemption", "The Dark Knight", "Pulp Fiction", "Forest Gump"],
expandedIndex: -1,
isSubmitting: false,
movies: <IMovieSearchResult[]>[],
searchValue: <string>this.$route.query["query"] || "",
page: -1,
totalPages: 0,
totalResults: 0
}
},
methods: {
async search() {
if (this.isSubmitting || this.searchValue.trim().length == 0) {
return;
}
this.$router.replace({ name: "Home", query: { query: this.searchValue } });
this.isSubmitting = true;
try {
let response = <MovieResults> await (authFetch(`/api/movie/search?query=${encodeURI(this.searchValue)}`).then(response => response.json()));
this.movies = response.data.results;
this.page = response.data.page;
this.totalPages = response.data.total_pages;
this.totalResults = response.data.total_results;
console.log("Got results", this.totalResults);
} catch(e) {
console.log("Error fetching movies:", e);
}
this.isSubmitting = false;
}
},
mounted() {
console.log("Mounted main component");
this.search();
}
});
</script>
<style scoped>
.overlay {
position: relative;
}
.overlay::before {
content: "";
position: absolute;
left: 0; top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
filter: grayscale(100%);
z-index: 0;
}
.overlay > * {
z-index: 1;
}
.active .body {
display: block !important;
}
</style>

+ 1
- 1
src/app/views/Login.vue View File

@ -49,7 +49,7 @@ export default defineComponent({
return;
}
this.isSubmitting = true;
fetch("/auth/login", {
fetch(`/auth/login?use_cookies=${navigator.cookieEnabled}`, {
method: "post",
headers: {
"Content-Type": "application/json"


+ 7
- 0
src/common/api_schema.ts View File

@ -0,0 +1,7 @@
export interface IApiResponse {
status: string
}
export interface IApiDataResponse<T> extends IApiResponse {
data: T
}

+ 21
- 0
src/common/validation.ts View File

@ -1,4 +1,25 @@
export const constraints = {
api: {
movie: {
search: {
query: {
presence: {
allowEmpty: false,
message: "The query cannot be blank"
}
},
year: {
numericality: {
onlyInteger: true,
greaterThan: 0,
notGreaterThan: "Invalid year",
notValid: "Invalid year",
notInteger: "Invalid year"
}
}
}
}
},
login: {
email: {
presence: {


+ 2
- 2
src/server/index.ts View File

@ -1,10 +1,10 @@
import { readFileSync } from "fs";
import Application from "./Application";
import { env, secretSync } from "./util";
/**
* Load the application key
*/
let appKey = readFileSync(<string>process.env["APP_KEY_FILE"]).toString();
let appKey = secretSync(env("APP_KEY_FILE"));
/**
* Create a new application instance


+ 48
- 1
src/server/services/MovieSearch.ts View File

@ -1,18 +1,65 @@
import Application from "@server/Application";
import TheMovieDb from "@lib/tmdb";
import { env, secret } from "@server/util";
import { readFile } from "fs/promises";
import TVDB from "tvdb-v4";
import Service from "./Service";
import TvDb from "./TvDb";
export default class MovieSearch extends Service
{
protected tmdb!: TheMovieDb;
/**
* The instance of TVDB
*/
protected tvdb!: TvDb;
public constructor(app: Application) {
super("Movie Search", app);
}
public async boot() {
/**
* Start the service
*/
public start() {
this.tvdb = this.app.service<TvDb>("TVDB");
}
/**
* Boot the service
*/
public async boot() {
let apiKey = await secret(env("TMDB_KEY_FILE"));
this.tmdb = new TheMovieDb(apiKey);
}
/**
* Shutdown the service
*/
public async shutdown() {
// no-op
}
// Interface -----------------------------------------------------------------------------------
/**
* Get the details of a movie
*/
public async details(id: number) {
return await this.tmdb.movie(id);
}
/**
* Search for a movie
*/
public async search(query: string, year?: number) {
return await this.tmdb.searchMovie(query, year);
// let results = await this.tvdb.searchMovie(query, year);
// return results.map(movie => <any>{
// image : movie.image_url ? `/api/tvdb/artwork${new URL(movie.image_url).pathname}`: null,
// name : movie.name,
// year : movie.year
// });
}
}

+ 35
- 2
src/server/services/TvDb.ts View File

@ -3,6 +3,12 @@ import Application from "@server/Application";
import TVDB from "tvdb-v4";
import Service from "./Service";
/**
* The token refresh period in milliseconds
*/
// const TOKEN_REFRESH_PERIOD = 1000*60*parseInt(<string>process.env["TVDB_REFRESH_PERIOD"]);
const TOKEN_REFRESH_PERIOD = 1000*60;
export default class TvDb extends Service
{
/**
@ -10,25 +16,52 @@ export default class TvDb extends Service
*/
protected tvdb!: TVDB;
/**
* Store the next timestamp when a token refresh is needed
*/
protected nextTokenRefreshTimestamp: number = 0;
/**
* Create a new TvDb service instance
*/
public constructor(app: Application) {
super("TVDB", app);
this.tvdb;
}
/**
* Boot the service
*/
public async boot() {
let apiKey = await readFile(<string>process.env["TVDB_KEY_FILE"]);
let apiKey = (await readFile(<string>process.env["TVDB_KEY_FILE"])).toString().trim();
this.tvdb = new TVDB(apiKey);
await this.refreshLogin();
}
/**
* Shutdown the service
*/
public async shutdown() {
// no-op
}
/**
* Refresh the login token if necessary
*/
protected async refreshLogin() {
if (Date.now() < this.nextTokenRefreshTimestamp) {
return;
}
this.log("Refreshing login token...");
let timestamp = Date.now() + TOKEN_REFRESH_PERIOD; // Save the time before the request
await this.tvdb.login(<string>process.env["TVDB_PIN"]);
this.nextTokenRefreshTimestamp = timestamp // if succeeds, update the timestamp
}
/**
* Search for a movie
*/
public async searchMovie(query: string, year?: number) {
await this.refreshLogin();
return await this.tvdb.search(query, "movie", year);
}
}

+ 1
- 1
src/server/services/WebServer/index.ts View File

@ -108,7 +108,7 @@ export default class WebServer extends Service
return reply.sendFile("index.html");
});
} else {
console.log("Using Vite proxy");
this.log("Using Vite proxy");
this.fastify.register(fastifyHttpProxy, {
upstream: "http://localhost:3001"
});


+ 2
- 0
src/server/services/WebServer/middleware/auth.ts View File

@ -7,6 +7,8 @@ import { IteratorNext } from ".";
* Attempt to authenticate a client's JWT token
*/
function authenticateJwtToken<T = any>(request: FastifyRequest, reply: FastifyReply): T | undefined {
console.log("JWT Signature:", request.cookies["jwt_signature"]);
console.log(request.headers["authorization"]);
// Verify headers
if (!request.headers["authorization"]) {
reply.status(401);


+ 11
- 7
src/server/services/WebServer/middleware/index.ts View File

@ -1,23 +1,27 @@
import { FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
export type HandlerMethod = (request: FastifyRequest, reply: FastifyReply) => void;
export type HandlerMethod = (request: FastifyRequest, reply: FastifyReply, next?: any) => void;
export type MiddlewareMethod = (request: FastifyRequest, reply: FastifyReply, next: () => void) => void;
export type IteratorNext = () => void;
/**
* A route handler that supports middleware methods
*/
export function handleMiddleware(middleware: MiddlewareMethod[], handler: RouteHandlerMethod) {
return <HandlerMethod> (async (request: FastifyRequest, reply: FastifyReply) => {
export function handleMiddleware(middleware: MiddlewareMethod[], handler?: RouteHandlerMethod) {
return <HandlerMethod> (async (request, reply, next) => {
var iterator = middleware[Symbol.iterator]();
var next = async () => {
var nextMiddleware = async () => {
let result = iterator.next();
if (result.done) {
(<any>handler)(request, reply);
if (handler) {
(<any>handler)(request, reply);
} else if (next !== undefined) {
next();
}
return;
}
result.value(request, reply, next);
result.value(request, reply, nextMiddleware);
};
next();
nextMiddleware();
});
}

+ 14
- 0
src/server/services/WebServer/requests/MovieSearchRequest.ts View File

@ -0,0 +1,14 @@
import { constraints } from "@common/validation";
import { FastifyRequest } from "fastify";
import validate from "validate.js";
import Request from "./Request";
export default class MovieSearchRequest extends Request
{
public validate(request: FastifyRequest) {
return validate.async(request.query, {
query: constraints.api.movie.search.query,
year: constraints.api.movie.search.year
},<any>{ fullMessages: false });
}
}

+ 14
- 0
src/server/services/WebServer/routes/RouteRegisterFactory.ts View File

@ -1,6 +1,8 @@
import { FastifyInstance, RouteHandlerMethod } from "fastify";
import Application from "@server/Application";
import { handleMiddleware, MiddlewareMethod } from "../middleware";
import fastifyHttpProxy from "fastify-http-proxy";
import { auth } from "../middleware/auth";
export type RouteFactory = ((factory: RouteRegisterFactory) => void)
| ((factory: RouteRegisterFactory, app: Application) => void);
@ -63,6 +65,7 @@ export default class RouteRegisterFactory
public get(path: string, middleware: RouteHandlerMethod|MiddlewareMethod[], handler?: RouteHandlerMethod) {
handler = (handler ?? <RouteHandlerMethod>middleware);
middleware = (middleware instanceof Array) ? this.middleware.concat(middleware) : this.middleware;
console.log("Registering route:", `${this.pathPrefix}${path}`);
this.fastify.get(`${this.pathPrefix}${path}`, handleMiddleware(middleware, handler));
}
@ -76,4 +79,15 @@ export default class RouteRegisterFactory
middleware = (middleware instanceof Array) ? this.middleware.concat(middleware) : this.middleware;
this.fastify.post(`${this.pathPrefix}${path}`, handleMiddleware(middleware, handler));
}
/**
* Register a proxy route
*/
public proxy(path: string, upstream: string, middleware?: MiddlewareMethod[]) {
this.fastify.register(fastifyHttpProxy, {
prefix: path,
beforeHandler: middleware ? handleMiddleware(middleware) : undefined,
upstream
});
}
}

+ 19
- 3
src/server/services/WebServer/routes/api.ts View File

@ -1,11 +1,27 @@
import Application from "@server/Application";
import { FastifyInstance } from "fastify";
import MovieSearch from "@server/services/MovieSearch";
import { auth } from "../middleware/auth";
import RouteRegisterFactory from "./RouteRegisterFactory";
/**
* Register API routes
*/
export default function register(server: FastifyInstance, app: Application) {
export default function register(factory: RouteRegisterFactory, app: Application) {
server.get("/api/movie/search", async (request, reply) => { });
factory.proxy("/api/tvdb/artwork", "https://artworks.thetvdb.com/");
factory.proxy("/api/tmdb/image", "https://image.tmdb.org/t/p/");
factory.get("/api/movie/search", [auth], async (request, reply) => {
let query = <string>(<any>request.query)["query"];
let year = parseInt((<any>request.query)["year"]) || undefined;
let results = await app.service<MovieSearch>("Movie Search").search(query, year);
reply.send({ status: "success", data: results });
});
factory.get("/api/movie/details/:id", [auth], async (request, reply) => {
let id = parseInt((<any>request.params)["id"]);
let results = await app.service<MovieSearch>("Movie Search").details(id);
reply.send({ status: "success", data: results});
});
}

+ 13
- 11
src/server/services/WebServer/routes/auth.ts View File

@ -4,9 +4,7 @@ import { RegisterToken, User } from "@server/database/entities";
import LoginRequest, { ILoginFormBody } from "../requests/LoginRequest";
import RegisterRequest, { IRegisterFormBody } from "../requests/RegisterRequest";
import handle from "../requests";
import jwt from "jsonwebtoken";
import { auth } from "../middleware/auth";
import { handleMiddleware } from "../middleware";
import RouteRegisterFactory from "./RouteRegisterFactory";
/**
@ -14,10 +12,10 @@ import RouteRegisterFactory from "./RouteRegisterFactory";
*/
export default function register(factory: RouteRegisterFactory, app: Application) {
factory.get("/auth/verify", handleMiddleware([auth], (request, reply) => {
factory.get("/auth/verify", [auth], (request, reply) => {
console.log("Authentication has been verified");
reply.send({ status: "success" });
}));
});
// Login ---------------------------------------------------------------------------------------
@ -30,13 +28,17 @@ export default function register(factory: RouteRegisterFactory, app: Application
return
}
// Store the header/payload in the client, store the signature in a secure httpOnly cookie
let [header, payload, signature] = token.split('.');
reply.setCookie("jwt_signature", signature, {
httpOnly: true,
sameSite: true,
secure: true
});
reply.send({ status: "success", token: `${header}.${payload}` });
// if ((<any>request.query)["use_cookies"] || (<any>request.query)["use_cookies"] === undefined) {
// let [header, payload, signature] = token.split('.');
// token = `${header}.${payload}`;
// reply.setCookie("jwt_signature", signature, {
// path: '/',
// httpOnly: true,
// sameSite: true,
// secure: true
// });
// }
reply.send({ status: "success", token });
}));
// Registration --------------------------------------------------------------------------------


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

@ -15,10 +15,7 @@ validate.validators.token = async function(value: any, options: IValidTokenOptio
Object.assign({
message: "is not a valid token"
}, options);
if (!(await RegisterToken.isValid(value))) {
return options.message;
}
}
console.log("Custom validators installed");

+ 23
- 9
src/server/util.ts View File

@ -1,11 +1,25 @@
export function hasAllProperties(obj: any, properties: string[]) {
if (Object.keys(obj).length !== properties.length) {
return false;
import assert from "assert";
import { readFile } from "fs/promises";
import { readFileSync } from "fs";
/**
* Fetch an environment variable
*/
export function env(variable: string, throwIfNotFound = true) {
let value = process.env[variable];
if (throwIfNotFound) {
assert(value !== undefined);
}
for (let key of properties) {
if (!(key in obj)) {
return false;
}
}
return true;
return <string>value;
}
/**
* Fetch a secret from a file
*/
export async function secret(path: string) {
return (await readFile(path)).toString().trim();
}
export function secretSync(path: string) {
return readFileSync(path).toString().trim();
}

+ 2
- 1
tailwind.config.js View File

@ -17,7 +17,8 @@ module.exports = {
backgroundColor: ["disabled"],
opacity: ["disabled"],
borderWidth: ["disabled"],
borderColor: ["disabled"]
borderColor: ["disabled"],
transform: ["motion-safe"]
},
},
plugins: [],


+ 4
- 1
tsconfig.json View File

@ -48,10 +48,13 @@
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"@app/*": ["app/*"],
"@common/*": ["common/*"],
"@lib/*": ["lib/*"],
"@server/*": ["server/*"],
},
// "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. */
"typeRoots": [ /* List of folders to include type definitions from. */
"src/typings"
],
// "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'. */


+ 5
- 0
yarn.lock View File

@ -142,6 +142,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e"
integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
"@types/node@^14.14.39":
version "14.14.39"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.39.tgz#9ef394d4eb52953d2890e4839393c309aa25d2d1"
integrity sha512-Qipn7rfTxGEDqZiezH+wxqWYR8vcXq5LRpZrETD19Gs4o8LbklbmqotSUsMU+s5G3PJwMRDfNEYoxrcBwIxOuw==
"@types/zen-observable@^0.8.2":
version "0.8.2"
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71"


Loading…
Cancel
Save