mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
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:
62
server/api/plextv.ts
Normal file
62
server/api/plextv.ts
Normal 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
15
server/entity/Session.ts
Normal 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 = '';
|
||||
}
|
@@ -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
29
server/middleware/auth.ts
Normal 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
65
server/routes/auth.ts
Normal 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;
|
@@ -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"]
|
||||
}
|
||||
|
21
server/types/express.d.ts
vendored
Normal file
21
server/types/express.d.ts
vendored
Normal 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;
|
Reference in New Issue
Block a user