server: add and run prettier
This commit is contained in:
parent
056a2bd3ce
commit
2823c217ea
19 changed files with 668 additions and 553 deletions
7
server/.prettierrc
Normal file
7
server/.prettierrc
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"singleQuote": false,
|
||||||
|
"printWidth": 80,
|
||||||
|
"useTabs": true
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from "dotenv"
|
||||||
dotenv.config();
|
dotenv.config()
|
||||||
|
|
||||||
import './src/server';
|
import "./src/server"
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
"start": "ts-node index.ts",
|
"start": "ts-node index.ts",
|
||||||
"dev": "nodemon index.ts",
|
"dev": "nodemon index.ts",
|
||||||
"build": "tsc -p .",
|
"build": "tsc -p .",
|
||||||
"migrate": "sequelize db:migrate"
|
"migrate": "sequelize db:migrate",
|
||||||
|
"lint": "prettier --config .prettierrc 'src/**/*.ts' 'index.ts' --write"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
"@types/marked": "^4.0.3",
|
"@types/marked": "^4.0.3",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/react-dom": "^17.0.14",
|
"@types/react-dom": "^17.0.14",
|
||||||
|
"prettier": "^2.6.0",
|
||||||
"ts-node": "^10.6.0",
|
"ts-node": "^10.6.0",
|
||||||
"tsconfig-paths": "^3.14.1",
|
"tsconfig-paths": "^3.14.1",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
|
|
|
@ -1,29 +1,30 @@
|
||||||
import * as express from 'express';
|
import * as express from "express"
|
||||||
import * as bodyParser from 'body-parser';
|
import * as bodyParser from "body-parser"
|
||||||
import * as errorhandler from 'strong-error-handler';
|
import * as errorhandler from "strong-error-handler"
|
||||||
import * as cors from 'cors';
|
import * as cors from "cors"
|
||||||
import { posts, users, auth, files } from '@routes/index';
|
import { posts, users, auth, files } from "@routes/index"
|
||||||
import { errors } from 'celebrate'
|
import { errors } from "celebrate"
|
||||||
|
|
||||||
export const app = express();
|
export const app = express()
|
||||||
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }))
|
||||||
app.use(bodyParser.json({ limit: '5mb' }));
|
app.use(bodyParser.json({ limit: "5mb" }))
|
||||||
|
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin: `http://localhost:3001`,
|
origin: `http://localhost:3001`
|
||||||
};
|
}
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions))
|
||||||
|
|
||||||
app.use("/auth", auth)
|
app.use("/auth", auth)
|
||||||
app.use("/posts", posts)
|
app.use("/posts", posts)
|
||||||
app.use("/users", users)
|
app.use("/users", users)
|
||||||
app.use("/files", files)
|
app.use("/files", files)
|
||||||
|
|
||||||
app.use(errors());
|
app.use(errors())
|
||||||
|
|
||||||
app.use(errorhandler({
|
|
||||||
debug: process.env.ENV !== 'production',
|
|
||||||
log: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
errorhandler({
|
||||||
|
debug: process.env.ENV !== "production",
|
||||||
|
log: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default {
|
export default {
|
||||||
port: process.env.PORT || 3000,
|
port: process.env.PORT || 3000,
|
||||||
jwt_secret: process.env.JWT_SECRET || 'myjwtsecret',
|
jwt_secret: process.env.JWT_SECRET || "myjwtsecret"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,34 @@
|
||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, Response } from "express"
|
||||||
import * as jwt from 'jsonwebtoken';
|
import * as jwt from "jsonwebtoken"
|
||||||
import config from '../config';
|
import config from "../config"
|
||||||
import { User as UserModel } from '../models/User';
|
import { User as UserModel } from "../models/User"
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserJwtRequest extends Request {
|
export interface UserJwtRequest extends Request {
|
||||||
user?: User;
|
user?: User
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function authenticateToken(req: UserJwtRequest, res: Response, next: NextFunction) {
|
export default function authenticateToken(
|
||||||
const authHeader = req.headers['authorization']
|
req: UserJwtRequest,
|
||||||
const token = authHeader && authHeader.split(' ')[1]
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const authHeader = req.headers["authorization"]
|
||||||
|
const token = authHeader && authHeader.split(" ")[1]
|
||||||
|
|
||||||
if (token == null) return res.sendStatus(401)
|
if (token == null) return res.sendStatus(401)
|
||||||
|
|
||||||
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
|
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
|
||||||
if (err) return res.sendStatus(403)
|
if (err) return res.sendStatus(403)
|
||||||
const userObj = await UserModel.findByPk(user.id);
|
const userObj = await UserModel.findByPk(user.id)
|
||||||
if (!userObj) {
|
if (!userObj) {
|
||||||
return res.sendStatus(403);
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
req.user = user
|
req.user = user
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, Response } from "express"
|
||||||
|
|
||||||
const key = process.env.SECRET_KEY;
|
const key = process.env.SECRET_KEY
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new Error('SECRET_KEY is not set.');
|
throw new Error("SECRET_KEY is not set.")
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function authenticateToken(req: Request, res: Response, next: NextFunction) {
|
export default function authenticateToken(
|
||||||
const requestKey = req.headers['x-secret-key']
|
req: Request,
|
||||||
if (requestKey !== key) {
|
res: Response,
|
||||||
return res.sendStatus(401)
|
next: NextFunction
|
||||||
}
|
) {
|
||||||
next()
|
const requestKey = req.headers["x-secret-key"]
|
||||||
|
if (requestKey !== key) {
|
||||||
|
return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +1,65 @@
|
||||||
import { BelongsTo, Column, CreatedAt, DataType, ForeignKey, IsUUID, Model, PrimaryKey, Scopes, Table, Unique } from 'sequelize-typescript';
|
import {
|
||||||
import { Post } from './Post';
|
BelongsTo,
|
||||||
import { User } from './User';
|
Column,
|
||||||
|
CreatedAt,
|
||||||
|
DataType,
|
||||||
|
ForeignKey,
|
||||||
|
IsUUID,
|
||||||
|
Model,
|
||||||
|
PrimaryKey,
|
||||||
|
Scopes,
|
||||||
|
Table,
|
||||||
|
Unique
|
||||||
|
} from "sequelize-typescript"
|
||||||
|
import { Post } from "./Post"
|
||||||
|
import { User } from "./User"
|
||||||
|
|
||||||
@Scopes(() => ({
|
@Scopes(() => ({
|
||||||
full: {
|
full: {
|
||||||
include: [{
|
include: [
|
||||||
model: User,
|
{
|
||||||
through: { attributes: [] },
|
model: User,
|
||||||
},
|
through: { attributes: [] }
|
||||||
{
|
},
|
||||||
model: Post,
|
{
|
||||||
through: { attributes: [] },
|
model: Post,
|
||||||
}]
|
through: { attributes: [] }
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@Table
|
@Table
|
||||||
export class File extends Model {
|
export class File extends Model {
|
||||||
@IsUUID(4)
|
@IsUUID(4)
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@Unique
|
@Unique
|
||||||
@Column({
|
@Column({
|
||||||
type: DataType.UUID,
|
type: DataType.UUID,
|
||||||
defaultValue: DataType.UUIDV4,
|
defaultValue: DataType.UUIDV4
|
||||||
})
|
})
|
||||||
id!: string
|
id!: string
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
title!: string;
|
title!: string
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
content!: string;
|
content!: string
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
sha!: string;
|
sha!: string
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
html!: string;
|
html!: string
|
||||||
|
|
||||||
@ForeignKey(() => User)
|
@ForeignKey(() => User)
|
||||||
@BelongsTo(() => User, 'userId')
|
@BelongsTo(() => User, "userId")
|
||||||
user!: User;
|
user!: User
|
||||||
|
|
||||||
@ForeignKey(() => Post)
|
@ForeignKey(() => Post)
|
||||||
@BelongsTo(() => Post, 'postId')
|
@BelongsTo(() => Post, "postId")
|
||||||
post!: Post;
|
post!: Post
|
||||||
|
|
||||||
@CreatedAt
|
@CreatedAt
|
||||||
@Column
|
@Column
|
||||||
createdAt!: Date;
|
createdAt!: Date
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,58 +1,74 @@
|
||||||
import { BelongsToMany, Column, CreatedAt, DataType, HasMany, IsUUID, Model, PrimaryKey, Scopes, Table, Unique, UpdatedAt } from 'sequelize-typescript';
|
import {
|
||||||
import { PostAuthor } from './PostAuthor';
|
BelongsToMany,
|
||||||
import { User } from './User';
|
Column,
|
||||||
import { File } from './File';
|
CreatedAt,
|
||||||
|
DataType,
|
||||||
|
HasMany,
|
||||||
|
IsUUID,
|
||||||
|
Model,
|
||||||
|
PrimaryKey,
|
||||||
|
Scopes,
|
||||||
|
Table,
|
||||||
|
Unique,
|
||||||
|
UpdatedAt
|
||||||
|
} from "sequelize-typescript"
|
||||||
|
import { PostAuthor } from "./PostAuthor"
|
||||||
|
import { User } from "./User"
|
||||||
|
import { File } from "./File"
|
||||||
|
|
||||||
@Scopes(() => ({
|
@Scopes(() => ({
|
||||||
user: {
|
user: {
|
||||||
include: [{
|
include: [
|
||||||
model: User,
|
{
|
||||||
through: { attributes: [] },
|
model: User,
|
||||||
}],
|
through: { attributes: [] }
|
||||||
},
|
}
|
||||||
full: {
|
]
|
||||||
include: [{
|
},
|
||||||
model: User,
|
full: {
|
||||||
through: { attributes: [] },
|
include: [
|
||||||
},
|
{
|
||||||
{
|
model: User,
|
||||||
model: File,
|
through: { attributes: [] }
|
||||||
through: { attributes: [] },
|
},
|
||||||
}]
|
{
|
||||||
}
|
model: File,
|
||||||
|
through: { attributes: [] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@Table
|
@Table
|
||||||
export class Post extends Model {
|
export class Post extends Model {
|
||||||
@IsUUID(4)
|
@IsUUID(4)
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@Unique
|
@Unique
|
||||||
@Column({
|
@Column({
|
||||||
type: DataType.UUID,
|
type: DataType.UUID,
|
||||||
defaultValue: DataType.UUIDV4,
|
defaultValue: DataType.UUIDV4
|
||||||
})
|
})
|
||||||
id!: string
|
id!: string
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
title!: string;
|
title!: string
|
||||||
|
|
||||||
@BelongsToMany(() => User, () => PostAuthor)
|
@BelongsToMany(() => User, () => PostAuthor)
|
||||||
users?: User[];
|
users?: User[]
|
||||||
|
|
||||||
@HasMany(() => File, { constraints: false })
|
@HasMany(() => File, { constraints: false })
|
||||||
files?: File[];
|
files?: File[]
|
||||||
|
|
||||||
@CreatedAt
|
@CreatedAt
|
||||||
@Column
|
@Column
|
||||||
createdAt!: Date;
|
createdAt!: Date
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
visibility!: string;
|
visibility!: string
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
password?: string;
|
password?: string
|
||||||
|
|
||||||
@UpdatedAt
|
@UpdatedAt
|
||||||
@Column
|
@Column
|
||||||
updatedAt!: Date;
|
updatedAt!: Date
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
import { Model, Column, Table, ForeignKey, IsUUID, PrimaryKey, DataType, Unique } from "sequelize-typescript";
|
import {
|
||||||
import { Post } from "./Post";
|
Model,
|
||||||
import { User } from "./User";
|
Column,
|
||||||
|
Table,
|
||||||
|
ForeignKey,
|
||||||
|
IsUUID,
|
||||||
|
PrimaryKey,
|
||||||
|
DataType,
|
||||||
|
Unique
|
||||||
|
} from "sequelize-typescript"
|
||||||
|
import { Post } from "./Post"
|
||||||
|
import { User } from "./User"
|
||||||
|
|
||||||
@Table
|
@Table
|
||||||
export class PostAuthor extends Model {
|
export class PostAuthor extends Model {
|
||||||
@IsUUID(4)
|
@IsUUID(4)
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@Unique
|
@Unique
|
||||||
@Column({
|
@Column({
|
||||||
type: DataType.UUID,
|
type: DataType.UUID,
|
||||||
defaultValue: DataType.UUIDV4,
|
defaultValue: DataType.UUIDV4
|
||||||
})
|
})
|
||||||
id!: string
|
id!: string
|
||||||
|
|
||||||
@ForeignKey(() => Post)
|
@ForeignKey(() => Post)
|
||||||
@Column
|
@Column
|
||||||
postId!: number;
|
postId!: number
|
||||||
|
|
||||||
@ForeignKey(() => User)
|
@ForeignKey(() => User)
|
||||||
@Column
|
@Column
|
||||||
authorId!: number;
|
authorId!: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,59 @@
|
||||||
import { Model, Column, Table, BelongsToMany, Scopes, CreatedAt, UpdatedAt, IsUUID, PrimaryKey, DataType, Unique } from "sequelize-typescript";
|
import {
|
||||||
import { Post } from "./Post";
|
Model,
|
||||||
import { PostAuthor } from "./PostAuthor";
|
Column,
|
||||||
|
Table,
|
||||||
|
BelongsToMany,
|
||||||
|
Scopes,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
IsUUID,
|
||||||
|
PrimaryKey,
|
||||||
|
DataType,
|
||||||
|
Unique
|
||||||
|
} from "sequelize-typescript"
|
||||||
|
import { Post } from "./Post"
|
||||||
|
import { PostAuthor } from "./PostAuthor"
|
||||||
|
|
||||||
@Scopes(() => ({
|
@Scopes(() => ({
|
||||||
posts: {
|
posts: {
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Post,
|
model: Post,
|
||||||
through: { attributes: [] },
|
through: { attributes: [] }
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
withoutPassword: {
|
withoutPassword: {
|
||||||
attributes: {
|
attributes: {
|
||||||
exclude: ["password"]
|
exclude: ["password"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@Table
|
@Table
|
||||||
export class User extends Model {
|
export class User extends Model {
|
||||||
@IsUUID(4)
|
@IsUUID(4)
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@Unique
|
@Unique
|
||||||
@Column({
|
@Column({
|
||||||
type: DataType.UUID,
|
type: DataType.UUID,
|
||||||
defaultValue: DataType.UUIDV4,
|
defaultValue: DataType.UUIDV4
|
||||||
})
|
})
|
||||||
id!: string
|
id!: string
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
username!: string;
|
username!: string
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
password!: string;
|
password!: string
|
||||||
|
|
||||||
@BelongsToMany(() => Post, () => PostAuthor)
|
@BelongsToMany(() => Post, () => PostAuthor)
|
||||||
posts?: Post[];
|
posts?: Post[]
|
||||||
|
|
||||||
@CreatedAt
|
@CreatedAt
|
||||||
@Column
|
@Column
|
||||||
createdAt!: Date;
|
createdAt!: Date
|
||||||
|
|
||||||
@UpdatedAt
|
@UpdatedAt
|
||||||
@Column
|
@Column
|
||||||
updatedAt!: Date;
|
updatedAt!: Date
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { Sequelize } from 'sequelize-typescript';
|
import { Sequelize } from "sequelize-typescript"
|
||||||
|
|
||||||
export const sequelize = new Sequelize({
|
export const sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: "sqlite",
|
||||||
database: 'drift',
|
database: "drift",
|
||||||
storage: process.env.MEMORY_DB === "true" ? ":memory:" : __dirname + '/../../drift.sqlite',
|
storage:
|
||||||
models: [__dirname + '/models'],
|
process.env.MEMORY_DB === "true"
|
||||||
host: 'localhost',
|
? ":memory:"
|
||||||
});
|
: __dirname + "/../../drift.sqlite",
|
||||||
|
models: [__dirname + "/models"],
|
||||||
|
host: "localhost"
|
||||||
|
})
|
||||||
|
|
|
@ -1,121 +1,137 @@
|
||||||
import { Router } from 'express'
|
import { Router } from "express"
|
||||||
import { genSalt, hash, compare } from "bcrypt"
|
import { genSalt, hash, compare } from "bcrypt"
|
||||||
import { User } from '@lib/models/User'
|
import { User } from "@lib/models/User"
|
||||||
import { sign } from 'jsonwebtoken'
|
import { sign } from "jsonwebtoken"
|
||||||
import config from '@lib/config'
|
import config from "@lib/config"
|
||||||
import jwt from '@lib/middleware/jwt'
|
import jwt from "@lib/middleware/jwt"
|
||||||
import { celebrate, Joi } from 'celebrate'
|
import { celebrate, Joi } from "celebrate"
|
||||||
|
|
||||||
const NO_EMPTY_SPACE_REGEX = /^\S*$/
|
const NO_EMPTY_SPACE_REGEX = /^\S*$/
|
||||||
|
|
||||||
export const requiresServerPassword = (process.env.MEMORY_DB || process.env.ENV === 'production') && !!process.env.REGISTRATION_PASSWORD
|
export const requiresServerPassword =
|
||||||
|
(process.env.MEMORY_DB || process.env.ENV === "production") &&
|
||||||
|
!!process.env.REGISTRATION_PASSWORD
|
||||||
console.log(`Registration password required: ${requiresServerPassword}`)
|
console.log(`Registration password required: ${requiresServerPassword}`)
|
||||||
|
|
||||||
export const auth = Router()
|
export const auth = Router()
|
||||||
|
|
||||||
const validateAuthPayload = (username: string, password: string, serverPassword?: string): void => {
|
const validateAuthPayload = (
|
||||||
if (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6) {
|
username: string,
|
||||||
throw new Error("Authentication data does not fulfill requirements")
|
password: string,
|
||||||
}
|
serverPassword?: string
|
||||||
|
): void => {
|
||||||
|
if (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6) {
|
||||||
|
throw new Error("Authentication data does not fulfill requirements")
|
||||||
|
}
|
||||||
|
|
||||||
if (requiresServerPassword) {
|
if (requiresServerPassword) {
|
||||||
if (!serverPassword || process.env.REGISTRATION_PASSWORD !== serverPassword) {
|
if (
|
||||||
throw new Error("Server password is incorrect. Please contact the server administrator.")
|
!serverPassword ||
|
||||||
}
|
process.env.REGISTRATION_PASSWORD !== serverPassword
|
||||||
}
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Server password is incorrect. Please contact the server administrator."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.post('/signup',
|
auth.post(
|
||||||
celebrate({
|
"/signup",
|
||||||
body: {
|
celebrate({
|
||||||
username: Joi.string().required(),
|
body: {
|
||||||
password: Joi.string().required(),
|
username: Joi.string().required(),
|
||||||
serverPassword: Joi.string().required().allow('', null),
|
password: Joi.string().required(),
|
||||||
}
|
serverPassword: Joi.string().required().allow("", null)
|
||||||
}),
|
}
|
||||||
async (req, res, next) => {
|
}),
|
||||||
try {
|
async (req, res, next) => {
|
||||||
validateAuthPayload(req.body.username, req.body.password, req.body.serverPassword)
|
try {
|
||||||
const username = req.body.username.toLowerCase();
|
validateAuthPayload(
|
||||||
|
req.body.username,
|
||||||
|
req.body.password,
|
||||||
|
req.body.serverPassword
|
||||||
|
)
|
||||||
|
const username = req.body.username.toLowerCase()
|
||||||
|
|
||||||
const existingUser = await User.findOne({
|
const existingUser = await User.findOne({
|
||||||
where: { username: username },
|
where: { username: username }
|
||||||
});
|
})
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new Error("Username already exists");
|
throw new Error("Username already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
const salt = await genSalt(10);
|
const salt = await genSalt(10)
|
||||||
const user = {
|
const user = {
|
||||||
username: username as string,
|
username: username as string,
|
||||||
password: await hash(req.body.password, salt),
|
password: await hash(req.body.password, salt)
|
||||||
};
|
}
|
||||||
|
|
||||||
const created_user = await User.create(user);
|
const created_user = await User.create(user)
|
||||||
|
|
||||||
const token = generateAccessToken(created_user.id);
|
const token = generateAccessToken(created_user.id)
|
||||||
|
|
||||||
res.status(201).json({ token: token, userId: created_user.id });
|
res.status(201).json({ token: token, userId: created_user.id })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
auth.post(
|
auth.post(
|
||||||
"/signin",
|
"/signin",
|
||||||
celebrate({
|
celebrate({
|
||||||
body: {
|
body: {
|
||||||
username: Joi.string().required(),
|
username: Joi.string().required(),
|
||||||
password: Joi.string().required(),
|
password: Joi.string().required(),
|
||||||
serverPassword: Joi.string().required().allow('', null),
|
serverPassword: Joi.string().required().allow("", null)
|
||||||
},
|
}
|
||||||
}),
|
}),
|
||||||
async (req, res, next) => {
|
async (req, res, next) => {
|
||||||
const error = "User does not exist or password is incorrect";
|
const error = "User does not exist or password is incorrect"
|
||||||
const errorToThrow = new Error(error);
|
const errorToThrow = new Error(error)
|
||||||
try {
|
try {
|
||||||
if (!req.body.username || !req.body.password) {
|
if (!req.body.username || !req.body.password) {
|
||||||
throw errorToThrow;
|
throw errorToThrow
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = req.body.username.toLowerCase();
|
const username = req.body.username.toLowerCase()
|
||||||
const user = await User.findOne({ where: { username: username } });
|
const user = await User.findOne({ where: { username: username } })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw errorToThrow;
|
throw errorToThrow
|
||||||
}
|
}
|
||||||
|
|
||||||
const password_valid = await compare(req.body.password, user.password);
|
const password_valid = await compare(req.body.password, user.password)
|
||||||
if (password_valid) {
|
if (password_valid) {
|
||||||
const token = generateAccessToken(user.id);
|
const token = generateAccessToken(user.id)
|
||||||
res.status(200).json({ token: token, userId: user.id });
|
res.status(200).json({ token: token, userId: user.id })
|
||||||
} else {
|
} else {
|
||||||
throw errorToThrow;
|
throw errorToThrow
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
auth.get("/requires-passcode", async (req, res, next) => {
|
auth.get("/requires-passcode", async (req, res, next) => {
|
||||||
if (requiresServerPassword) {
|
if (requiresServerPassword) {
|
||||||
res.status(200).json({ requiresPasscode: true });
|
res.status(200).json({ requiresPasscode: true })
|
||||||
} else {
|
} else {
|
||||||
res.status(200).json({ requiresPasscode: false });
|
res.status(200).json({ requiresPasscode: false })
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
function generateAccessToken(id: string) {
|
function generateAccessToken(id: string) {
|
||||||
return sign({ id: id }, config.jwt_secret, { expiresIn: "2d" });
|
return sign({ id: id }, config.jwt_secret, { expiresIn: "2d" })
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.get("/verify-token", jwt, async (req, res, next) => {
|
auth.get("/verify-token", jwt, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: "You are authenticated",
|
message: "You are authenticated"
|
||||||
});
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
|
@ -1,72 +1,72 @@
|
||||||
import { celebrate, Joi } from "celebrate";
|
import { celebrate, Joi } from "celebrate"
|
||||||
import { Router } from "express";
|
import { Router } from "express"
|
||||||
import { File } from "@lib/models/File";
|
import { File } from "@lib/models/File"
|
||||||
import secretKey from "@lib/middleware/secret-key";
|
import secretKey from "@lib/middleware/secret-key"
|
||||||
|
|
||||||
export const files = Router();
|
export const files = Router()
|
||||||
|
|
||||||
files.get("/raw/:id",
|
files.get(
|
||||||
celebrate({
|
"/raw/:id",
|
||||||
params: {
|
celebrate({
|
||||||
id: Joi.string().required(),
|
params: {
|
||||||
},
|
id: Joi.string().required()
|
||||||
}),
|
}
|
||||||
secretKey,
|
}),
|
||||||
async (req, res, next) => {
|
secretKey,
|
||||||
try {
|
async (req, res, next) => {
|
||||||
const file = await File.findOne({
|
try {
|
||||||
where: {
|
const file = await File.findOne({
|
||||||
id: req.params.id
|
where: {
|
||||||
},
|
id: req.params.id
|
||||||
attributes: ["title", "content"],
|
},
|
||||||
})
|
attributes: ["title", "content"]
|
||||||
|
})
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return res.status(404).json({ error: "File not found" })
|
return res.status(404).json({ error: "File not found" })
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: JWT-checkraw files
|
// TODO: JWT-checkraw files
|
||||||
if (file?.post?.visibility === "private") {
|
if (file?.post?.visibility === "private") {
|
||||||
// jwt(req as UserJwtRequest, res, () => {
|
// jwt(req as UserJwtRequest, res, () => {
|
||||||
// res.json(file);
|
// res.json(file);
|
||||||
// })
|
// })
|
||||||
res.json(file);
|
res.json(file)
|
||||||
} else {
|
} else {
|
||||||
res.json(file);
|
res.json(file)
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
next(e)
|
||||||
next(e);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
files.get(
|
||||||
|
"/html/:id",
|
||||||
|
celebrate({
|
||||||
|
params: {
|
||||||
|
id: Joi.string().required()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const file = await File.findOne({
|
||||||
|
where: {
|
||||||
|
id: req.params.id
|
||||||
|
},
|
||||||
|
attributes: ["html"]
|
||||||
|
})
|
||||||
|
|
||||||
files.get("/html/:id",
|
if (!file) {
|
||||||
celebrate({
|
return res.status(404).json({ error: "File not found" })
|
||||||
params: {
|
}
|
||||||
id: Joi.string().required(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const file = await File.findOne({
|
|
||||||
where: {
|
|
||||||
id: req.params.id
|
|
||||||
},
|
|
||||||
attributes: ["html"],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!file) {
|
res.setHeader("Content-Type", "text/plain")
|
||||||
return res.status(404).json({ error: "File not found" })
|
res.setHeader("Cache-Control", "public, max-age=4800")
|
||||||
}
|
res.status(200).write(file.html)
|
||||||
|
res.end()
|
||||||
res.setHeader('Content-Type', 'text/plain')
|
} catch (error) {
|
||||||
res.setHeader('Cache-Control', 'public, max-age=4800')
|
next(error)
|
||||||
res.status(200).write(file.html)
|
}
|
||||||
res.end()
|
}
|
||||||
} catch (error) {
|
|
||||||
next(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export { auth } from "./auth";
|
export { auth } from "./auth"
|
||||||
export { posts } from "./posts";
|
export { posts } from "./posts"
|
||||||
export { users } from "./users";
|
export { users } from "./users"
|
||||||
export { files } from "./files";
|
export { files } from "./files"
|
||||||
|
|
|
@ -1,209 +1,236 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express"
|
||||||
import { celebrate, Joi } from "celebrate";
|
import { celebrate, Joi } from "celebrate"
|
||||||
import { File } from '@lib/models/File'
|
import { File } from "@lib/models/File"
|
||||||
import { Post } from '@lib/models/Post';
|
import { Post } from "@lib/models/Post"
|
||||||
import jwt, { UserJwtRequest } from '@lib/middleware/jwt';
|
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto"
|
||||||
import { User } from '@lib/models/User';
|
import { User } from "@lib/models/User"
|
||||||
import secretKey from '@lib/middleware/secret-key';
|
import secretKey from "@lib/middleware/secret-key"
|
||||||
import markdown from '@lib/render-markdown';
|
import markdown from "@lib/render-markdown"
|
||||||
|
|
||||||
export const posts = Router();
|
export const posts = Router()
|
||||||
|
|
||||||
const postVisibilitySchema = (value: string) => {
|
const postVisibilitySchema = (value: string) => {
|
||||||
if (value === 'public' || value === 'private') {
|
if (value === "public" || value === "private") {
|
||||||
return value;
|
return value
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid post visibility');
|
throw new Error("Invalid post visibility")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
posts.post(
|
posts.post(
|
||||||
"/create",
|
"/create",
|
||||||
jwt,
|
jwt,
|
||||||
celebrate({
|
celebrate({
|
||||||
body: {
|
body: {
|
||||||
title: Joi.string().required(),
|
title: Joi.string().required().allow("", null),
|
||||||
files: Joi.any().required(),
|
files: Joi.any().required(),
|
||||||
visibility: Joi.string().custom(postVisibilitySchema, 'valid visibility').required(),
|
visibility: Joi.string()
|
||||||
userId: Joi.string().required(),
|
.custom(postVisibilitySchema, "valid visibility")
|
||||||
password: Joi.string().optional(),
|
.required(),
|
||||||
},
|
userId: Joi.string().required(),
|
||||||
}),
|
password: Joi.string().optional()
|
||||||
async (req, res, next) => {
|
}
|
||||||
try {
|
}),
|
||||||
let hashedPassword: string = ''
|
async (req, res, next) => {
|
||||||
if (req.body.visibility === 'protected') {
|
try {
|
||||||
hashedPassword = crypto.createHash('sha256').update(req.body.password).digest('hex');
|
let hashedPassword: string = ""
|
||||||
}
|
if (req.body.visibility === "protected") {
|
||||||
|
hashedPassword = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(req.body.password)
|
||||||
|
.digest("hex")
|
||||||
|
}
|
||||||
|
|
||||||
const newPost = new Post({
|
const newPost = new Post({
|
||||||
title: req.body.title,
|
title: req.body.title,
|
||||||
visibility: req.body.visibility,
|
visibility: req.body.visibility,
|
||||||
password: hashedPassword,
|
password: hashedPassword
|
||||||
})
|
})
|
||||||
|
|
||||||
await newPost.save()
|
await newPost.save()
|
||||||
await newPost.$add('users', req.body.userId);
|
await newPost.$add("users", req.body.userId)
|
||||||
const newFiles = await Promise.all(req.body.files.map(async (file) => {
|
const newFiles = await Promise.all(
|
||||||
const html = getHtmlFromFile(file);
|
req.body.files.map(async (file) => {
|
||||||
const newFile = new File({
|
const html = getHtmlFromFile(file)
|
||||||
title: file.title,
|
const newFile = new File({
|
||||||
content: file.content,
|
title: file.title || "",
|
||||||
sha: crypto.createHash('sha256').update(file.content).digest('hex').toString(),
|
content: file.content,
|
||||||
html
|
sha: crypto
|
||||||
})
|
.createHash("sha256")
|
||||||
|
.update(file.content)
|
||||||
|
.digest("hex")
|
||||||
|
.toString(),
|
||||||
|
html
|
||||||
|
})
|
||||||
|
|
||||||
await newFile.$set("user", req.body.userId);
|
await newFile.$set("user", req.body.userId)
|
||||||
await newFile.$set("post", newPost.id);
|
await newFile.$set("post", newPost.id)
|
||||||
await newFile.save();
|
await newFile.save()
|
||||||
return newFile;
|
return newFile
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
|
|
||||||
await Promise.all(newFiles.map((file) => {
|
await Promise.all(
|
||||||
newPost.$add("files", file.id);
|
newFiles.map((file) => {
|
||||||
newPost.save();
|
newPost.$add("files", file.id)
|
||||||
}))
|
newPost.save()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
res.json(newPost);
|
res.json(newPost)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
posts.get("/", secretKey, async (req, res, next) => {
|
posts.get("/", secretKey, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const posts = await Post.findAll({
|
const posts = await Post.findAll({
|
||||||
attributes: ["id", "title", "visibility", "createdAt"],
|
attributes: ["id", "title", "visibility", "createdAt"]
|
||||||
})
|
})
|
||||||
|
|
||||||
res.json(posts);
|
res.json(posts)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
posts.get("/mine", jwt, secretKey, async (req: UserJwtRequest, res, next) => {
|
posts.get("/mine", jwt, secretKey, async (req: UserJwtRequest, res, next) => {
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return res.status(401).json({ error: "Unauthorized" })
|
return res.status(401).json({ error: "Unauthorized" })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await User.findByPk(req.user.id, {
|
const user = await User.findByPk(req.user.id, {
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Post,
|
model: Post,
|
||||||
as: "posts",
|
as: "posts",
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: File,
|
model: File,
|
||||||
as: "files"
|
as: "files"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
})
|
})
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({ error: "User not found" })
|
return res.status(404).json({ error: "User not found" })
|
||||||
}
|
}
|
||||||
return res.json(user.posts?.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()))
|
return res.json(
|
||||||
} catch (error) {
|
user.posts?.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||||
next(error)
|
)
|
||||||
}
|
} catch (error) {
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
posts.get(
|
posts.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
celebrate({
|
celebrate({
|
||||||
params: {
|
params: {
|
||||||
id: Joi.string().required(),
|
id: Joi.string().required()
|
||||||
},
|
}
|
||||||
}),
|
}),
|
||||||
async (req: UserJwtRequest, res, next) => {
|
async (req: UserJwtRequest, res, next) => {
|
||||||
try {
|
try {
|
||||||
const post = await Post.findOne({
|
const post = await Post.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: req.params.id,
|
id: req.params.id
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: File,
|
model: File,
|
||||||
as: "files",
|
as: "files",
|
||||||
attributes: [
|
attributes: [
|
||||||
"id",
|
"id",
|
||||||
"title",
|
"title",
|
||||||
"content",
|
"content",
|
||||||
"sha",
|
"sha",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"updatedAt",
|
"updatedAt"
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: User,
|
model: User,
|
||||||
as: "users",
|
as: "users",
|
||||||
attributes: ["id", "username"],
|
attributes: ["id", "username"]
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return res.status(404).json({ error: "Post not found" })
|
return res.status(404).json({ error: "Post not found" })
|
||||||
}
|
}
|
||||||
|
|
||||||
// if public or unlisted, cache
|
// if public or unlisted, cache
|
||||||
if (post.visibility === 'public' || post.visibility === 'unlisted') {
|
if (post.visibility === "public" || post.visibility === "unlisted") {
|
||||||
res.set('Cache-Control', 'public, max-age=4800')
|
res.set("Cache-Control", "public, max-age=4800")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (post.visibility === 'public' || post?.visibility === 'unlisted') {
|
if (post.visibility === "public" || post?.visibility === "unlisted") {
|
||||||
secretKey(req, res, () => {
|
secretKey(req, res, () => {
|
||||||
res.json(post);
|
res.json(post)
|
||||||
})
|
})
|
||||||
} else if (post.visibility === 'private') {
|
} else if (post.visibility === "private") {
|
||||||
jwt(req as UserJwtRequest, res, () => {
|
jwt(req as UserJwtRequest, res, () => {
|
||||||
res.json(post);
|
res.json(post)
|
||||||
})
|
})
|
||||||
} else if (post.visibility === 'protected') {
|
} else if (post.visibility === "protected") {
|
||||||
const { password } = req.query
|
const { password } = req.query
|
||||||
if (!password || typeof password !== 'string') {
|
if (!password || typeof password !== "string") {
|
||||||
return jwt(req as UserJwtRequest, res, () => {
|
return jwt(req as UserJwtRequest, res, () => {
|
||||||
res.json(post);
|
res.json(post)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const hash = crypto.createHash('sha256').update(password).digest('hex').toString()
|
const hash = crypto
|
||||||
if (hash !== post.password) {
|
.createHash("sha256")
|
||||||
return res.status(400).json({ error: "Incorrect password." })
|
.update(password)
|
||||||
}
|
.digest("hex")
|
||||||
|
.toString()
|
||||||
|
if (hash !== post.password) {
|
||||||
|
return res.status(400).json({ error: "Incorrect password." })
|
||||||
|
}
|
||||||
|
|
||||||
res.json(post);
|
res.json(post)
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
next(e)
|
||||||
next(e);
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
);
|
|
||||||
|
|
||||||
function getHtmlFromFile(file: any) {
|
function getHtmlFromFile(file: any) {
|
||||||
const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', ''];
|
const renderAsMarkdown = [
|
||||||
const fileType = () => {
|
"markdown",
|
||||||
const pathParts = file.title.split(".");
|
"md",
|
||||||
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : "";
|
"mdown",
|
||||||
return language;
|
"mkdn",
|
||||||
};
|
"mkd",
|
||||||
const type = fileType();
|
"mdwn",
|
||||||
let contentToRender: string = (file.content || '');
|
"mdtxt",
|
||||||
|
"mdtext",
|
||||||
|
"text",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
const fileType = () => {
|
||||||
|
const pathParts = file.title.split(".")
|
||||||
|
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
const type = fileType()
|
||||||
|
let contentToRender: string = file.content || ""
|
||||||
|
|
||||||
if (!renderAsMarkdown.includes(type)) {
|
if (!renderAsMarkdown.includes(type)) {
|
||||||
contentToRender =
|
contentToRender = `~~~${type}
|
||||||
`~~~${type}
|
|
||||||
${file.content}
|
${file.content}
|
||||||
~~~`;
|
~~~`
|
||||||
} else {
|
} else {
|
||||||
contentToRender = '\n' + file.content;
|
contentToRender = "\n" + file.content
|
||||||
}
|
}
|
||||||
const html = markdown(contentToRender);
|
const html = markdown(contentToRender)
|
||||||
return html;
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express"
|
||||||
// import jwt from "@lib/middleware/jwt";
|
// import jwt from "@lib/middleware/jwt";
|
||||||
// import { User } from "@lib/models/User";
|
// import { User } from "@lib/models/User";
|
||||||
|
|
||||||
export const users = Router();
|
export const users = Router()
|
||||||
|
|
||||||
// users.get("/", jwt, async (req, res, next) => {
|
// users.get("/", jwt, async (req, res, next) => {
|
||||||
// try {
|
// try {
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { createServer } from 'http';
|
import { createServer } from "http"
|
||||||
import { app } from './app';
|
import { app } from "./app"
|
||||||
import config from './lib/config';
|
import config from "./lib/config"
|
||||||
import { sequelize } from './lib/sequelize';
|
import { sequelize } from "./lib/sequelize"
|
||||||
|
|
||||||
(async () => {
|
;(async () => {
|
||||||
await sequelize.sync({});
|
await sequelize.sync({})
|
||||||
createServer(app)
|
createServer(app).listen(config.port, () =>
|
||||||
.listen(
|
console.info(`Server running on port ${config.port}`)
|
||||||
config.port,
|
)
|
||||||
() => console.info(`Server running on port ${config.port}`)
|
})()
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
|
@ -2027,6 +2027,11 @@ prepend-http@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||||
|
|
||||||
|
prettier@^2.6.0:
|
||||||
|
version "2.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.0.tgz#12f8f504c4d8ddb76475f441337542fa799207d4"
|
||||||
|
integrity sha512-m2FgJibYrBGGgQXNzfd0PuDGShJgRavjUoRCw1mZERIWVSXF0iLzLm+aOqTAbLnC3n6JzUhAA8uZnFVghHJ86A==
|
||||||
|
|
||||||
prism-react-renderer@^1.3.1:
|
prism-react-renderer@^1.3.1:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d"
|
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d"
|
||||||
|
|
Loading…
Reference in a new issue