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:
@@ -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",
|
||||
|
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;
|
85
yarn.lock
85
yarn.lock
@@ -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"
|
||||
|
Reference in New Issue
Block a user