feat(api): initial implementation of the auth system (#30)

Adds the auth system but does not add all required features. They will be handled in other tickets
pull/17/head
sct 4 years ago committed by GitHub
parent 7ac4bb01f0
commit 5343f35e5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,8 +13,12 @@
"dependencies": {
"@tailwindcss/ui": "^0.5.0",
"axios": "^0.19.2",
"body-parser": "^1.19.0",
"bowser": "^2.10.0",
"connect-typeorm": "^1.1.4",
"cookie-parser": "^1.4.5",
"express": "^4.17.1",
"express-session": "^1.17.1",
"next": "9.5.2",
"react": "16.13.1",
"react-dom": "16.13.1",
@ -26,7 +30,10 @@
"devDependencies": {
"@commitlint/cli": "^9.1.2",
"@commitlint/config-conventional": "^9.1.2",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.7",
"@types/express-session": "^1.17.0",
"@types/node": "^14.0.27",
"@types/react": "^16.9.46",
"@types/react-transition-group": "^4.4.0",

@ -0,0 +1,62 @@
import axios, { AxiosInstance } from 'axios';
interface PlexAccountResponse {
user: PlexUser;
}
interface PlexUser {
id: number;
uuid: string;
email: string;
joined_at: string;
username: string;
title: string;
thumb: string;
hasPassword: boolean;
authToken: string;
subscription: {
active: boolean;
status: string;
plan: string;
features: string[];
};
roles: {
roles: string[];
};
entitlements: string[];
}
class PlexTvAPI {
private authToken: string;
private axios: AxiosInstance;
constructor(authToken: string) {
this.authToken = authToken;
this.axios = axios.create({
baseURL: 'https://plex.tv',
headers: {
'X-Plex-Token': this.authToken,
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
}
public async getUser(): Promise<PlexUser> {
try {
const account = await this.axios.get<PlexAccountResponse>(
'/users/account.json'
);
return account.data.user;
} catch (e) {
console.error(
'Something broke when getting account from plex.tv',
e.message
);
throw new Error('Invalid auth token');
}
}
}
export default PlexTvAPI;

@ -0,0 +1,15 @@
import { ISession } from 'connect-typeorm';
import { Index, Column, PrimaryColumn, Entity } from 'typeorm';
@Entity()
export class Session implements ISession {
@Index()
@Column('bigint')
public expiredAt = Date.now();
@PrimaryColumn('varchar', { length: 255 })
public id = '';
@Column('text')
public json = '';
}

@ -1,7 +1,12 @@
import express from 'express';
import next from 'next';
import { createConnection } from 'typeorm';
import { createConnection, getRepository } from 'typeorm';
import routes from './routes';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import { TypeormStore } from 'connect-typeorm/out';
import { Session } from './entity/Session';
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
@ -13,6 +18,23 @@ app
.prepare()
.then(() => {
const server = express();
server.use(cookieParser());
server.use(bodyParser.json());
server.use(bodyParser.urlencoded({ extended: true }));
// Setup sessions
const sessionRespository = getRepository(Session);
server.use(
session({
secret: 'verysecret',
resave: false,
saveUninitialized: false,
store: new TypeormStore({
cleanupLimit: 2,
ttl: 86400,
}).connect(sessionRespository),
})
);
server.use('/api', routes);
server.get('*', (req, res) => handle(req, res));

@ -0,0 +1,29 @@
import { getRepository } from 'typeorm';
import { User } from '../entity/User';
import { Middleware } from '../types/express';
export const checkUser: Middleware = async (req, _res, next) => {
if (req.session?.userId) {
const userRepository = getRepository(User);
const user = await userRepository.findOne({
where: { id: req.session.userId },
});
if (user) {
req.user = user;
}
}
next();
};
export const isAuthenticated: Middleware = async (req, res, next) => {
if (!req.user) {
res.status(403).json({
status: 403,
error: 'You do not have permisson to access this endpoint',
});
} else {
next();
}
};

@ -0,0 +1,65 @@
import { Router } from 'express';
import { getRepository } from 'typeorm';
import { User } from '../entity/User';
import PlexTvAPI from '../api/plextv';
const authRoutes = Router();
authRoutes.post('/login', async (req, res) => {
const userRepository = getRepository(User);
const body = req.body as { authToken?: string };
if (!body.authToken) {
return res.status(500).json({ error: 'You must provide an auth token' });
}
try {
// First we need to use this auth token to get the users email from plex tv
const plextv = new PlexTvAPI(body.authToken);
const account = await plextv.getUser();
// Next let's see if the user already exists
let user = await userRepository.findOne({
where: { email: account.email },
});
if (user) {
// Let's check if their plex token is up to date
if (user.plexToken !== body.authToken) {
user.plexToken = body.authToken;
await userRepository.save(user);
}
} else {
// Here we check if it's the first user. If it is, we create the user with no check
// and give them admin permissions
const totalUsers = await userRepository.count();
if (totalUsers === 0) {
user = new User({
email: account.email,
plexToken: account.authToken,
// TODO: When we add permissions in #52, set admin here
});
await userRepository.save(user);
}
// If we get to this point, the user does not already exist so we need to create the
// user _assuming_ they have access to the plex server
// (We cant do this until we finish the settings sytem and actually
// store the user token in ticket #55)
}
// Set logged in session
if (req.session && user) {
req.session.userId = user.id;
}
return res.status(200).json({ status: 'ok' });
} catch (e) {
console.error(e);
res
.status(500)
.json({ error: 'Something went wrong. Is your auth token valid?' });
}
});
export default authRoutes;

@ -1,9 +1,13 @@
import { Router } from 'express';
import user from './user';
import authRoutes from './auth';
import { checkUser, isAuthenticated } from '../middleware/auth';
const router = Router();
router.use('/user', user);
router.use(checkUser);
router.use('/user', isAuthenticated, user);
router.use('/auth', authRoutes);
router.get('/', (req, res) => {
return res.status(200).json({
@ -12,4 +16,8 @@ router.get('/', (req, res) => {
});
});
router.all('*', (req, res) =>
res.status(404).json({ status: 404, message: '404 Not Found' })
);
export default router;

@ -6,7 +6,8 @@
"noEmit": false,
"strictPropertyInitialization": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
"emitDecoratorMetadata": true,
"typeRoots": ["types"]
},
"include": ["**/*.ts", "**/*.tsx"]
}

@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { NextFunction, Request, Response } from 'express';
import type { User } from '../entity/User';
declare global {
namespace Express {
export interface Session {
userId?: number;
}
export interface Request {
user?: User;
}
}
}
export type Middleware = <ParamsDictionary, any, any>(
req: Request,
res: Response,
next: NextFunction
) => Promise<void | NextFunction> | void | NextFunction;

@ -1242,7 +1242,7 @@
hex-rgb "^4.1.0"
postcss-selector-parser "^6.0.2"
"@types/body-parser@*":
"@types/body-parser@*", "@types/body-parser@^1.19.0":
version "1.19.0"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
@ -1262,6 +1262,18 @@
dependencies:
"@types/node" "*"
"@types/cookie-parser@^1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5"
integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==
dependencies:
"@types/express" "*"
"@types/debug@0.0.31":
version "0.0.31"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.31.tgz#bac8d8aab6a823e91deb7f79083b2a35fa638f33"
integrity sha512-LS1MCPaQKqspg7FvexuhmDbWUhE2yIJ+4AgVIyObfc06/UKZ8REgxGNjZc82wPLWmbeOm7S+gSsLgo75TanG4A==
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@ -1276,7 +1288,15 @@
"@types/qs" "*"
"@types/range-parser" "*"
"@types/express@^4.17.7":
"@types/express-session@^1.15.5", "@types/express-session@^1.17.0":
version "1.17.0"
resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.17.0.tgz#770daf81368f6278e3e40dd894e1e52abbdca0cd"
integrity sha512-OQEHeBFE1UhChVIBhRh9qElHUvTp4BzKKHxMDkGHT7WuYk5eL93hPG7D8YAIkoBSbhNEY0RjreF15zn+U0eLjA==
dependencies:
"@types/express" "*"
"@types/node" "*"
"@types/express@*", "@types/express@^4.17.7":
version "4.17.7"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.7.tgz#42045be6475636d9801369cd4418ef65cdb0dd59"
integrity sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==
@ -2092,7 +2112,7 @@ bn.js@^5.1.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
body-parser@1.19.0:
body-parser@1.19.0, body-parser@^1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
@ -2764,6 +2784,16 @@ configstore@^5.0.1:
write-file-atomic "^3.0.0"
xdg-basedir "^4.0.0"
connect-typeorm@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/connect-typeorm/-/connect-typeorm-1.1.4.tgz#80f4e60dc4eeb6c04de334c30bf3e4bb766a34e5"
integrity sha512-1/0b1aFzip0UBzuaSUkVZE4EW3n4UZzi3x2fmKeCaOb+sB4VZGj79txzLvtWIHLYY6kls/PKNcQHfEurdEjsUw==
dependencies:
"@types/debug" "0.0.31"
"@types/express-session" "^1.15.5"
debug "^4.1.1"
express-session "^1.15.6"
console-browserify@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
@ -2838,6 +2868,14 @@ convert-source-map@^0.3.3:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA=
cookie-parser@^1.4.5:
version "1.4.5"
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49"
integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==
dependencies:
cookie "0.4.0"
cookie-signature "1.0.6"
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@ -3253,6 +3291,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
depd@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
des.js@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@ -3802,6 +3845,20 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies:
homedir-polyfill "^1.0.1"
express-session@^1.15.6, express-session@^1.17.1:
version "1.17.1"
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.1.tgz#36ecbc7034566d38c8509885c044d461c11bf357"
integrity sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==
dependencies:
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~2.0.0"
on-headers "~1.0.2"
parseurl "~1.3.3"
safe-buffer "5.2.0"
uid-safe "~2.1.5"
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@ -6194,6 +6251,11 @@ on-finished@~2.3.0:
dependencies:
ee-first "1.1.1"
on-headers@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@ -7095,6 +7157,11 @@ quick-lru@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
random-bytes@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -7536,6 +7603,11 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@ -8614,6 +8686,13 @@ typescript@^3.9.7:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
uid-safe@~2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==
dependencies:
random-bytes "~1.0.0"
undefsafe@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae"

Loading…
Cancel
Save