mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-25 19:13:54 +00:00
Approve/reject skins from the website
This commit is contained in:
parent
918fd4b4da
commit
68dadec7ac
13 changed files with 382 additions and 35 deletions
|
|
@ -19,11 +19,9 @@ export default class DiscordEventHandler {
|
|||
return this._clientPromise;
|
||||
}
|
||||
|
||||
async getNsfwChannel(): Promise<TextChannel> {
|
||||
async getChannel(channelId: string): Promise<TextChannel> {
|
||||
const client = await this.getClient();
|
||||
const dest = client.channels.get(
|
||||
Config.NSFW_SKIN_CHANNEL_ID
|
||||
) as TextChannel | null;
|
||||
const dest = client.channels.get(channelId) as TextChannel | null;
|
||||
if (dest == null) {
|
||||
throw new Error("Could not get NSFW channel");
|
||||
}
|
||||
|
|
@ -33,16 +31,50 @@ export default class DiscordEventHandler {
|
|||
async handle(action: ApiAction, ctx: UserContext) {
|
||||
switch (action.type) {
|
||||
case "REVIEW_REQUESTED":
|
||||
this.requestReview(action.md5, ctx);
|
||||
await this.requestReview(action.md5, ctx);
|
||||
break;
|
||||
case "APPROVED_SKIN":
|
||||
await this.reportApproved(action.md5, ctx);
|
||||
break;
|
||||
case "REJECTED_SKIN":
|
||||
await this.reportRejected(action.md5, ctx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async reportApproved(md5: string, ctx: UserContext) {
|
||||
const skin = await SkinModel.fromMd5(ctx, md5);
|
||||
if (skin == null) {
|
||||
return;
|
||||
}
|
||||
const dest = await this.getChannel(Config.TWEET_BOT_CHANNEL_ID);
|
||||
await DiscordUtils.postSkin({
|
||||
md5,
|
||||
title: (filename) => `Approved by ${ctx.username}: ${filename}`,
|
||||
dest,
|
||||
});
|
||||
}
|
||||
|
||||
async reportRejected(md5: string, ctx: UserContext) {
|
||||
console.log("Report rejected");
|
||||
const skin = await SkinModel.fromMd5(ctx, md5);
|
||||
if (skin == null) {
|
||||
return;
|
||||
}
|
||||
const dest = await this.getChannel(Config.TWEET_BOT_CHANNEL_ID);
|
||||
await DiscordUtils.postSkin({
|
||||
md5,
|
||||
title: (filename) => `Rejected by ${ctx.username}: ${filename}`,
|
||||
dest,
|
||||
});
|
||||
}
|
||||
|
||||
async requestReview(md5: string, ctx: UserContext) {
|
||||
const skin = await SkinModel.fromMd5(ctx, md5);
|
||||
if (skin == null) {
|
||||
return;
|
||||
}
|
||||
const dest = await this.getNsfwChannel();
|
||||
const dest = await this.getChannel(Config.NSFW_SKIN_CHANNEL_ID);
|
||||
const tweetStatus = await skin.getTweetStatus();
|
||||
if (tweetStatus === "UNREVIEWED") {
|
||||
await DiscordUtils.postSkin({
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import { Application } from "express";
|
|||
import { knex } from "../../db";
|
||||
import request from "supertest"; // supertest is a framework that allows to easily test web apis
|
||||
import { createApp } from "../app";
|
||||
import SkinModel from "../../data/SkinModel";
|
||||
import * as S3 from "../../s3";
|
||||
import { processUserUploads } from "../processUserUploads";
|
||||
import UserContext from "../../data/UserContext";
|
||||
jest.mock("../../s3");
|
||||
jest.mock("../processUserUploads");
|
||||
|
||||
|
|
@ -13,7 +15,13 @@ const handler = jest.fn();
|
|||
beforeEach(async () => {
|
||||
handler.mockReset();
|
||||
// We ignore the ctx
|
||||
app = createApp((action, _ctx) => handler(action));
|
||||
app = createApp({
|
||||
eventHandler: (action, _ctx) => handler(action),
|
||||
extraMiddleware: (req, res, next) => {
|
||||
req.session.username = "<MOCKED>";
|
||||
next();
|
||||
},
|
||||
});
|
||||
await knex.migrate.latest();
|
||||
await knex.seed.run();
|
||||
});
|
||||
|
|
@ -79,7 +87,9 @@ describe("/skins/", () => {
|
|||
});
|
||||
|
||||
test("/skins/a_fake_md5/report", async () => {
|
||||
const { body } = await request(app).post("/skins/a_fake_md5/report");
|
||||
const { body } = await request(app)
|
||||
.post("/skins/a_fake_md5/report")
|
||||
.expect(200);
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
type: "REVIEW_REQUESTED",
|
||||
md5: "a_fake_md5",
|
||||
|
|
@ -87,6 +97,36 @@ test("/skins/a_fake_md5/report", async () => {
|
|||
expect(body).toEqual({}); // TODO: Where does the text response go?
|
||||
});
|
||||
|
||||
test("/skins/a_fake_md5/approve", async () => {
|
||||
const ctx = new UserContext();
|
||||
const { body } = await request(app)
|
||||
.post("/skins/a_fake_md5/approve")
|
||||
.expect(200);
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
type: "APPROVED_SKIN",
|
||||
md5: "a_fake_md5",
|
||||
});
|
||||
expect(body).toEqual({}); // TODO: Where does the text response go?
|
||||
const skin = await SkinModel.fromMd5(ctx, "a_fake_md5");
|
||||
|
||||
expect(await skin?.getTweetStatus()).toEqual("APPROVED");
|
||||
});
|
||||
|
||||
test("/skins/a_fake_md5/reject", async () => {
|
||||
const ctx = new UserContext();
|
||||
const { body } = await request(app)
|
||||
.post("/skins/a_fake_md5/reject")
|
||||
.expect(200);
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
type: "REJECTED_SKIN",
|
||||
md5: "a_fake_md5",
|
||||
});
|
||||
expect(body).toEqual({}); // TODO: Where does the text response go?
|
||||
const skin = await SkinModel.fromMd5(ctx, "a_fake_md5");
|
||||
|
||||
expect(await skin?.getTweetStatus()).toEqual("REJECTED");
|
||||
});
|
||||
|
||||
// TODO: Actually upload some skins?
|
||||
test("/skins/status", async () => {
|
||||
const { body } = await request(app)
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@ import bodyParser from "body-parser";
|
|||
import Sentry from "@sentry/node";
|
||||
import expressSitemapXml from "express-sitemap-xml";
|
||||
import * as Skins from "../data/skins";
|
||||
import express from "express";
|
||||
import express, { Handler } from "express";
|
||||
import UserContext from "../data/UserContext";
|
||||
import cookieSession from "cookie-session";
|
||||
import { SECRET } from "../config";
|
||||
|
||||
export type ApiAction =
|
||||
| { type: "REVIEW_REQUESTED"; md5: string }
|
||||
| { type: "REJECTED_SKIN"; md5: string }
|
||||
| { type: "APPROVED_SKIN"; md5: string }
|
||||
| { type: "SKIN_UPLOADED"; md5: string }
|
||||
| { type: "ERROR_PROCESSING_UPLOAD"; id: string; message: string };
|
||||
|
||||
|
|
@ -23,19 +27,46 @@ declare global {
|
|||
notify(action: ApiAction): void;
|
||||
log(message: string): void;
|
||||
logError(message: string): void;
|
||||
session: {
|
||||
username: string | undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createApp(eventHandler?: EventHandler) {
|
||||
type Options = {
|
||||
eventHandler?: EventHandler;
|
||||
extraMiddleware?: Handler;
|
||||
};
|
||||
|
||||
export function createApp({ eventHandler, extraMiddleware }: Options) {
|
||||
const app = express();
|
||||
if (Sentry) {
|
||||
app.use(Sentry.Handlers.requestHandler());
|
||||
}
|
||||
|
||||
// https://expressjs.com/en/guide/behind-proxies.html
|
||||
// This is needed in order to allow `cookieSession({secure: true})` cookies to be sent.
|
||||
app.set("trust proxy", "loopback");
|
||||
|
||||
app.use(
|
||||
cookieSession({
|
||||
secure: true,
|
||||
sameSite: "none",
|
||||
httpOnly: false,
|
||||
name: "session",
|
||||
secret: SECRET,
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
})
|
||||
);
|
||||
|
||||
if (extraMiddleware != null) {
|
||||
app.use(extraMiddleware);
|
||||
}
|
||||
|
||||
// Add UserContext to request
|
||||
app.use((req, res, next) => {
|
||||
req.ctx = new UserContext();
|
||||
req.ctx = new UserContext(req.session.username);
|
||||
next();
|
||||
// TODO: Dispose of context?
|
||||
});
|
||||
|
|
@ -52,7 +83,12 @@ export function createApp(eventHandler?: EventHandler) {
|
|||
|
||||
// Attach logger
|
||||
app.use((req, res, next) => {
|
||||
const context = { url: req.url, params: req.params, query: req.query };
|
||||
const context = {
|
||||
url: req.url,
|
||||
params: req.params,
|
||||
query: req.query,
|
||||
username: req.ctx.username,
|
||||
};
|
||||
req.log = (message) => console.log(message, context);
|
||||
req.logError = (message) => console.error(message, context);
|
||||
next();
|
||||
|
|
@ -105,6 +141,7 @@ const allowList = [
|
|||
];
|
||||
|
||||
const corsOptions: CorsOptions = {
|
||||
credentials: true,
|
||||
origin: function (origin, callback) {
|
||||
if (!origin || allowList.some((regex) => regex.test(origin))) {
|
||||
callback(null, true);
|
||||
|
|
|
|||
77
packages/skin-database/api/auth.ts
Normal file
77
packages/skin-database/api/auth.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import {
|
||||
DISCORD_CLIENT_ID,
|
||||
DISCORD_CLIENT_SECRET,
|
||||
DISCORD_REDIRECT_URL,
|
||||
DISCORD_WEBAMP_SERVER_ID,
|
||||
} from "../config";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
type AuthResponse = {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
discriminator: string;
|
||||
public_flags: number;
|
||||
flags: number;
|
||||
localse: string;
|
||||
mfa_enabled: boolean;
|
||||
premium_type: number;
|
||||
};
|
||||
|
||||
// Given a code from Discord auth, check that it's valid
|
||||
export async function auth(code: string): Promise<string> {
|
||||
const data = {
|
||||
client_id: DISCORD_CLIENT_ID,
|
||||
client_secret: DISCORD_CLIENT_SECRET,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: DISCORD_REDIRECT_URL,
|
||||
code,
|
||||
scope: "identity",
|
||||
};
|
||||
|
||||
const fetchResponse = await fetch("https://discord.com/api/oauth2/token", {
|
||||
method: "POST",
|
||||
body: new URLSearchParams(data),
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
if (!fetchResponse.ok) {
|
||||
throw new Error("Error getting auth token from Discord");
|
||||
}
|
||||
const info = await fetchResponse.json();
|
||||
|
||||
const authResponse = await fetch("https://discord.com/api/users/@me", {
|
||||
headers: {
|
||||
authorization: `${info.token_type} ${info.access_token}`,
|
||||
},
|
||||
});
|
||||
if (!authResponse.ok) {
|
||||
throw new Error("Error getting auth data from Discord");
|
||||
}
|
||||
|
||||
const authBody = (await authResponse.json()) as AuthResponse;
|
||||
|
||||
const guildsResponse = await fetch(
|
||||
"https://discord.com/api/users/@me/guilds",
|
||||
{
|
||||
headers: {
|
||||
authorization: `${info.token_type} ${info.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!guildsResponse.ok) {
|
||||
throw new Error("Error getting guild data from Discord");
|
||||
}
|
||||
|
||||
const guildsBody = (await guildsResponse.json()) as any[];
|
||||
const hasMemership = guildsBody.some(
|
||||
(guild) => guild.id === DISCORD_WEBAMP_SERVER_ID
|
||||
);
|
||||
|
||||
if (!hasMemership) {
|
||||
throw new Error("User is not a member of the Webamp server.");
|
||||
}
|
||||
|
||||
return authBody.username;
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@ import { Router } from "express";
|
|||
import asyncHandler from "express-async-handler";
|
||||
import SkinModel from "../data/SkinModel";
|
||||
import * as Skins from "../data/skins";
|
||||
import { DISCORD_CLIENT_ID, DISCORD_REDIRECT_URL } from "../config";
|
||||
import S3 from "../s3";
|
||||
import LRU from "lru-cache";
|
||||
import { MuseumPage } from "../data/skins";
|
||||
import { processUserUploads } from "./processUserUploads";
|
||||
import { auth } from "./auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -16,6 +18,47 @@ const options = {
|
|||
let skinCount: number | null = null;
|
||||
const cache = new LRU<string, MuseumPage>(options);
|
||||
|
||||
router.get(
|
||||
"/auth/",
|
||||
asyncHandler(async (req, res) => {
|
||||
res.redirect(
|
||||
302,
|
||||
`https://discord.com/api/oauth2/authorize?client_id=${encodeURIComponent(
|
||||
DISCORD_CLIENT_ID
|
||||
)}&redirect_uri=${encodeURIComponent(
|
||||
DISCORD_REDIRECT_URL
|
||||
)}&response_type=code&scope=identify%20guilds`
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/authed/",
|
||||
asyncHandler(async (req, res) => {
|
||||
res.json({ username: req.ctx.username });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/auth/discord",
|
||||
asyncHandler(async (req, res) => {
|
||||
const code = req.query.code as string | undefined;
|
||||
|
||||
if (code == null) {
|
||||
// TODO 400
|
||||
throw new Error("Expected to get a code");
|
||||
}
|
||||
const username = await auth(code);
|
||||
if (username == null) {
|
||||
throw new Error("Expected to get a username");
|
||||
}
|
||||
req.session.username = username;
|
||||
|
||||
// TODO: What about dev?
|
||||
res.redirect(302, `https://skins.webamp.org/review/`);
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/skins/",
|
||||
asyncHandler(async (req, res) => {
|
||||
|
|
@ -86,6 +129,60 @@ router.get(
|
|||
})
|
||||
);
|
||||
|
||||
function requireAuthed(req, res, next) {
|
||||
if (!req.ctx.authed()) {
|
||||
res.status(403);
|
||||
res.send("You must be logged in");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
router.get(
|
||||
"/to_review",
|
||||
requireAuthed,
|
||||
asyncHandler(async (req, res) => {
|
||||
const { filename, md5 } = await Skins.getSkinToReview();
|
||||
res.json({ filename, md5 });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/skins/:md5/reject",
|
||||
requireAuthed,
|
||||
asyncHandler(async (req, res) => {
|
||||
const { md5 } = req.params;
|
||||
req.log(`Rejecting skin with hash "${md5}"`);
|
||||
const skin = await SkinModel.fromMd5(req.ctx, md5);
|
||||
if (skin == null) {
|
||||
req.log(`No skin skin`);
|
||||
throw new Error(`Could not locate as skin with md5 ${md5}`);
|
||||
}
|
||||
await Skins.reject(req.ctx, md5);
|
||||
req.notify({ type: "REJECTED_SKIN", md5 });
|
||||
res.send("The skin has been rejected.");
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/skins/:md5/approve",
|
||||
requireAuthed,
|
||||
asyncHandler(async (req, res) => {
|
||||
if (!req.ctx.authed()) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
const { md5 } = req.params;
|
||||
req.log(`Approving skin with hash "${md5}"`);
|
||||
const skin = await SkinModel.fromMd5(req.ctx, md5);
|
||||
if (skin == null) {
|
||||
throw new Error(`Cold not locate as skin with md5 ${md5}`);
|
||||
}
|
||||
await Skins.approve(req.ctx, md5);
|
||||
req.notify({ type: "APPROVED_SKIN", md5 });
|
||||
res.send("The skin has been approved.");
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/skins/:md5/report",
|
||||
asyncHandler(async (req, res) => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ const port = process.env.PORT ? Number(process.env.PORT) : 3001;
|
|||
const handler = new DiscordEventHandler();
|
||||
|
||||
// GO!
|
||||
const app = createApp((action, ctx) => handler.handle(action, ctx));
|
||||
const app = createApp({
|
||||
eventHandler: (action, ctx) => handler.handle(action, ctx),
|
||||
});
|
||||
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
|
||||
|
||||
// Initialize Sentry after we start listening. Any crash at start time will appear in the console and we'll notice.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
// Currently only used as a WeakMap key
|
||||
export default class UserContext {}
|
||||
export default class UserContext {
|
||||
username: string | null;
|
||||
constructor(username?: string) {
|
||||
this.username = username || null;
|
||||
}
|
||||
authed() {
|
||||
return this.username != null;
|
||||
}
|
||||
}
|
||||
|
||||
export function ctxWeakMapMemoize<T>(factory: () => T) {
|
||||
const cache: WeakMap<UserContext, T> = new WeakMap();
|
||||
|
|
|
|||
|
|
@ -136,12 +136,15 @@ export async function markAsTweeted(md5: string, url: string): Promise<void> {
|
|||
}
|
||||
|
||||
// TODO: Also path actor
|
||||
export async function markAsNSFW(md5: string): Promise<void> {
|
||||
export async function markAsNSFW(ctx: UserContext, md5: string): Promise<void> {
|
||||
const index = { objectID: md5, nsfw: true };
|
||||
// TODO: Await here, but for some reason this never completes
|
||||
await searchIndex.partialUpdateObjects([index]);
|
||||
await recordSearchIndexUpdates(md5, Object.keys(index));
|
||||
await knex("skin_reviews").insert({ skin_md5: md5, review: "NSFW" }, []);
|
||||
await knex("skin_reviews").insert(
|
||||
{ skin_md5: md5, review: "NSFW", reviewer: ctx.username || "UNKNOWN" },
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUploadStatuses(
|
||||
|
|
@ -330,13 +333,19 @@ export async function recordUserUploadRequest(
|
|||
}
|
||||
|
||||
// TODO: Also path actor
|
||||
export async function approve(md5: string): Promise<void> {
|
||||
await knex("skin_reviews").insert({ skin_md5: md5, review: "APPROVED" }, []);
|
||||
export async function approve(ctx: UserContext, md5: string): Promise<void> {
|
||||
await knex("skin_reviews").insert(
|
||||
{ skin_md5: md5, review: "APPROVED", reviewer: ctx.username || "UNKNOWN" },
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Also path actor
|
||||
export async function reject(md5: string): Promise<void> {
|
||||
await knex("skin_reviews").insert({ skin_md5: md5, review: "REJECTED" }, []);
|
||||
export async function reject(ctx: UserContext, md5: string): Promise<void> {
|
||||
await knex("skin_reviews").insert(
|
||||
{ skin_md5: md5, review: "REJECTED", reviewer: ctx.username || "UNKNOWN" },
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
export async function getSkinToReview(): Promise<{
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export async function postSkin({
|
|||
switch (vote.emoji.name) {
|
||||
case "👍":
|
||||
case "👏":
|
||||
await Skins.approve(md5);
|
||||
await Skins.approve(ctx, md5);
|
||||
logger.info(`${user.username} approved ${md5}`);
|
||||
await msg.channel.send(
|
||||
`${canonicalFilename} was approved by ${user.username}`
|
||||
|
|
@ -130,7 +130,7 @@ export async function postSkin({
|
|||
break;
|
||||
case "😔":
|
||||
case "👎":
|
||||
await Skins.reject(md5);
|
||||
await Skins.reject(ctx, md5);
|
||||
logger.info(`${user.username} rejected ${md5}`);
|
||||
await msg.channel.send(
|
||||
`${canonicalFilename} was rejected by ${user.username}`
|
||||
|
|
@ -139,11 +139,11 @@ export async function postSkin({
|
|||
break;
|
||||
case "🔞":
|
||||
logger.info(`${user.username} marked ${md5} as NSFW`);
|
||||
await Skins.markAsNSFW(md5);
|
||||
await Skins.markAsNSFW(ctx, md5);
|
||||
await msg.channel.send(
|
||||
`${canonicalFilename} was marked as NSFW by ${user.username}`
|
||||
);
|
||||
await Skins.reject(md5);
|
||||
await Skins.reject(ctx, md5);
|
||||
logger.info(`${user.username} rejected ${md5}`);
|
||||
await msg.channel.send(
|
||||
`${canonicalFilename} was rejected by ${user.username}`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import * as Knex from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<any> {
|
||||
await knex.schema.table("skin_reviews", function (table) {
|
||||
table.text("reviewer");
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<any> {
|
||||
await knex.schema.table("skin_reviews", function (table) {
|
||||
table.dropColumn("reviewer");
|
||||
});
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
"dependencies": {
|
||||
"@sentry/node": "^5.27.3",
|
||||
"@sentry/tracing": "^5.27.3",
|
||||
"@types/cookie-session": "^2.0.41",
|
||||
"@types/cors": "^2.8.8",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/express-fileupload": "^1.1.5",
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
"@types/node-fetch": "^2.5.7",
|
||||
"algoliasearch": "^4.3.0",
|
||||
"aws-sdk": "^2.663.0",
|
||||
"cookie-session": "^1.4.0",
|
||||
"cors": "^2.8.5",
|
||||
"dataloader": "^2.0.0",
|
||||
"discord.js": "^11.4.2",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export type TweetRow = {
|
|||
export type ReviewRow = {
|
||||
skin_md5: string;
|
||||
review: "APPROVED" | "REJECTED" | "NSFW";
|
||||
reviewer: string;
|
||||
};
|
||||
|
||||
export type FileRow = {
|
||||
|
|
|
|||
51
yarn.lock
51
yarn.lock
|
|
@ -1702,6 +1702,13 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/cookie-session@^2.0.41":
|
||||
version "2.0.41"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookie-session/-/cookie-session-2.0.41.tgz#ba1cf00114a505795269bf46c5b14c8e9f9c639a"
|
||||
dependencies:
|
||||
"@types/express" "*"
|
||||
"@types/keygrip" "*"
|
||||
|
||||
"@types/cookiejar@*":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8"
|
||||
|
|
@ -1821,6 +1828,10 @@
|
|||
dependencies:
|
||||
jszip "*"
|
||||
|
||||
"@types/keygrip@*":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
|
||||
|
||||
"@types/lodash-es@^4.17.1":
|
||||
version "4.17.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.3.tgz#87eb0b3673b076b8ee655f1890260a136af09a2d"
|
||||
|
|
@ -3771,6 +3782,14 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
|||
dependencies:
|
||||
safe-buffer "~5.1.1"
|
||||
|
||||
cookie-session@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/cookie-session/-/cookie-session-1.4.0.tgz#c325aea685ceb9c8e4fd00b0313a46d547747380"
|
||||
dependencies:
|
||||
cookies "0.8.0"
|
||||
debug "2.6.9"
|
||||
on-headers "~1.0.2"
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
|
|
@ -3787,6 +3806,13 @@ cookiejar@^2.1.2:
|
|||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
|
||||
|
||||
cookies@0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
|
||||
dependencies:
|
||||
depd "~2.0.0"
|
||||
keygrip "~1.1.0"
|
||||
|
||||
copy-concurrently@^1.0.0:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
|
||||
|
|
@ -4343,6 +4369,10 @@ depd@~1.1.2:
|
|||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
|
||||
depd@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||
|
||||
des.js@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
|
||||
|
|
@ -4849,10 +4879,6 @@ eslint-plugin-prettier@^3.1.0:
|
|||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
|
||||
eslint-plugin-react-hooks@^1.5.1:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz#6210b6d5a37205f0b92858f895a4e827020a7d04"
|
||||
|
||||
eslint-plugin-react-hooks@^2.1.2:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.1.tgz#4ef5930592588ce171abeb26f400c7fbcbc23cd0"
|
||||
|
|
@ -8102,6 +8128,12 @@ junk@^3.1.0:
|
|||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
|
||||
|
||||
keygrip@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
|
||||
dependencies:
|
||||
tsscmp "1.0.6"
|
||||
|
||||
keyv@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
|
||||
|
|
@ -12235,6 +12267,10 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
|
|||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
|
||||
|
||||
tsscmp@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
|
||||
|
||||
tsutils@^3.17.1:
|
||||
version "3.17.1"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
|
||||
|
|
@ -12659,13 +12695,6 @@ wbuf@^1.1.0, wbuf@^1.7.3:
|
|||
dependencies:
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
webamp@1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/webamp/-/webamp-1.4.0.tgz#36c0ef2621ed3d14835d8cbd58206a408903e602"
|
||||
dependencies:
|
||||
eslint-plugin-react-hooks "^1.5.1"
|
||||
fscreen "^1.0.2"
|
||||
|
||||
webidl-conversions@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue