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
This commit is contained in:
sct
2020-08-28 09:34:15 +09:00
committed by GitHub
parent 7ac4bb01f0
commit 5343f35e5b
10 changed files with 315 additions and 6 deletions

62
server/api/plextv.ts Normal file
View File

@@ -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;

15
server/entity/Session.ts Normal file
View File

@@ -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 = '';
}

View File

@@ -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));

29
server/middleware/auth.ts Normal file
View File

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

65
server/routes/auth.ts Normal file
View File

@@ -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;

View File

@@ -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;

View File

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

21
server/types/express.d.ts vendored Normal file
View File

@@ -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;