mirror of https://github.com/MaxLeiter/Drift
rm server code, add markdown rendering, html saving, visibility updating
parent
096cf41eee
commit
c41cf7c5ef
@ -1,11 +1,10 @@
|
|||||||
import Auth from "../components"
|
import Auth from "../components"
|
||||||
import Header from "@components/header"
|
import PageWrapper from "@components/page-wrapper"
|
||||||
|
|
||||||
export default function SignInPage() {
|
export default function SignInPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper>
|
||||||
<Header />
|
|
||||||
<Auth page="signin" />
|
<Auth page="signin" />
|
||||||
</>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,12 @@
|
|||||||
import Header from "@components/header"
|
import Header from "@components/header"
|
||||||
import NewPost from "app/(posts)/new/components/new"
|
import NewPost from "app/(posts)/new/components/new"
|
||||||
import "@styles/react-datepicker.css"
|
import "@styles/react-datepicker.css"
|
||||||
|
import PageWrapper from "@components/page-wrapper"
|
||||||
|
|
||||||
const New = () => <>
|
const New = () => (
|
||||||
<Header signedIn />
|
<PageWrapper signedIn>
|
||||||
<NewPost />
|
<NewPost />
|
||||||
</>
|
</PageWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
export default New
|
export default New
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { MDXRemote, MDXRemoteProps } from "next-mdx-remote";
|
||||||
|
|
||||||
|
export default function MDXRemoteWrapper(props: MDXRemoteProps) {
|
||||||
|
return <MDXRemote {...props} />;
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
'use client';
|
||||||
|
import Page from "@geist-ui/core/dist/page"
|
||||||
|
import Header from "./header"
|
||||||
|
|
||||||
|
export default function PageWrapper({
|
||||||
|
children,
|
||||||
|
signedIn
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
signedIn?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Page.Header>
|
||||||
|
<Header signedIn={signedIn} />
|
||||||
|
</Page.Header>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingPageWrapper() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Page.Header>
|
||||||
|
<Header signedIn={false} />
|
||||||
|
</Page.Header>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
import useSWR from "swr"
|
|
||||||
|
|
||||||
// https://2020.paco.me/blog/shared-hook-state-with-swr
|
|
||||||
const useSharedState = <T>(key: string, initial?: T) => {
|
|
||||||
const { data: state, mutate: setState } = useSWR(key, {
|
|
||||||
fallbackData: initial
|
|
||||||
})
|
|
||||||
return [state, setState] as const
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useSharedState
|
|
@ -1,33 +0,0 @@
|
|||||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
|
||||||
import { getCookie, setCookie } from "cookies-next"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
import useSharedState from "./use-shared-state"
|
|
||||||
|
|
||||||
const useSignedIn = () => {
|
|
||||||
const token = getCookie(TOKEN_COOKIE_NAME)
|
|
||||||
|
|
||||||
const [signedIn, setSignedIn] = useSharedState(
|
|
||||||
"signedIn",
|
|
||||||
typeof window === "undefined" ? false : !!token
|
|
||||||
)
|
|
||||||
|
|
||||||
const signin = (token: string) => {
|
|
||||||
setSignedIn(true)
|
|
||||||
// TODO: investigate SameSite / CORS cookie security
|
|
||||||
setCookie(TOKEN_COOKIE_NAME, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (token) {
|
|
||||||
// setSignedIn(true)
|
|
||||||
// } else {
|
|
||||||
// setSignedIn(false)
|
|
||||||
// }
|
|
||||||
// }, [setSignedIn, token])
|
|
||||||
|
|
||||||
console.log("signed in", signedIn)
|
|
||||||
|
|
||||||
return { signedIn, signin, token }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useSignedIn
|
|
@ -0,0 +1,117 @@
|
|||||||
|
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||||
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
|
import { getPostById } from "@lib/server/prisma"
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
import { getSession } from "next-auth/react"
|
||||||
|
import { prisma } from "lib/server/prisma"
|
||||||
|
import * as crypto from "crypto"
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
if (req.method === "GET") return handleGet(req, res)
|
||||||
|
else if (req.method === "PUT") return handlePut(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMethods(["GET", "PUT"], handler)
|
||||||
|
|
||||||
|
async function handleGet(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
|
const id = parseQueryParam(req.query.id)
|
||||||
|
const files = req.query.files ? parseQueryParam(req.query.files) : true
|
||||||
|
|
||||||
|
console.log("post id is", id)
|
||||||
|
if (!id) {
|
||||||
|
return res.status(400).json({ error: "Missing id" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = await getPostById(id, Boolean(files))
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return res.status(404).json({ message: "Post not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.visibility === "public") {
|
||||||
|
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate")
|
||||||
|
return res.json(post)
|
||||||
|
} else if (post.visibility === "unlisted") {
|
||||||
|
res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getSession({ req })
|
||||||
|
|
||||||
|
// the user can always go directly to their own post
|
||||||
|
if (session?.user.id === post.authorId) {
|
||||||
|
return res.json(post)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.visibility === "protected") {
|
||||||
|
const password = parseQueryParam(req.query.password)
|
||||||
|
const hash = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(password?.toString() || "")
|
||||||
|
.digest("hex")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
if (hash === post.password) {
|
||||||
|
return res.json(post)
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
isProtected: true,
|
||||||
|
post: {
|
||||||
|
id: post.id,
|
||||||
|
visibility: post.visibility,
|
||||||
|
title: post.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(404).json({ message: "Post not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT is for adjusting visibility and password
|
||||||
|
async function handlePut(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
|
const { password, visibility } = req.body
|
||||||
|
const id = parseQueryParam(req.query.id)
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return res.status(400).json({ error: "Missing id" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = await getPostById(id, false)
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return res.status(404).json({ message: "Post not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getSession({ req })
|
||||||
|
|
||||||
|
const isAuthor = session?.user.id === post.authorId
|
||||||
|
|
||||||
|
if (!isAuthor) {
|
||||||
|
return res.status(403).json({ message: "Unauthorized" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibility === "protected" && !password) {
|
||||||
|
return res.status(400).json({ message: "Missing password" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(password?.toString() || "")
|
||||||
|
.digest("hex")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
const updatedPost = await prisma.post.update({
|
||||||
|
where: {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
visibility,
|
||||||
|
password: visibility === "protected" ? hashedPassword : null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: updatedPost.id,
|
||||||
|
visibility: updatedPost.visibility
|
||||||
|
})
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,2 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
__tests__/
|
|
@ -1,4 +0,0 @@
|
|||||||
JWT_SECRET=secret-jwt
|
|
||||||
MEMORY_DB=true
|
|
||||||
REGISTRATION_PASSWORD=password
|
|
||||||
SECRET_KEY=secret
|
|
@ -1,4 +0,0 @@
|
|||||||
.env
|
|
||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
drift.sqlite
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"singleQuote": false,
|
|
||||||
"printWidth": 80,
|
|
||||||
"useTabs": true
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
const path = require("path")
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
// config: path.resolve("config", "config.js"),
|
|
||||||
"models-path": path.resolve("src", "lib", "models"),
|
|
||||||
// "seeders-path": path.resolve("src", "seeders"),
|
|
||||||
"migrations-path": path.resolve("src", "migrations")
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
FROM node:17-alpine AS deps
|
|
||||||
|
|
||||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
|
||||||
RUN apk add --no-cache libc6-compat git
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json yarn.lock tsconfig.json tslint.json ./
|
|
||||||
|
|
||||||
RUN yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
FROM node:17-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ARG NODE_ENV
|
|
||||||
|
|
||||||
ENV NODE_ENV=${NODE_ENV:-production}
|
|
||||||
|
|
||||||
RUN apk add --no-cache git
|
|
||||||
RUN yarn build:docker
|
|
||||||
|
|
||||||
FROM node:17-alpine AS runner
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ARG NODE_ENV
|
|
||||||
|
|
||||||
ENV NODE_ENV=${NODE_ENV:-production}
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
|
||||||
RUN adduser --system --uid 1001 drift
|
|
||||||
|
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
|
||||||
|
|
||||||
USER drift
|
|
||||||
|
|
||||||
ENV PORT=3000
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
CMD ["node", "dist/index.js"]
|
|
@ -1,11 +0,0 @@
|
|||||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
|
||||||
module.exports = {
|
|
||||||
preset: "ts-jest",
|
|
||||||
testEnvironment: "node",
|
|
||||||
setupFiles: ["<rootDir>/test/setup-tests.ts"],
|
|
||||||
moduleNameMapper: {
|
|
||||||
"@lib/(.*)": "<rootDir>/src/lib/$1",
|
|
||||||
"@routes/(.*)": "<rootDir>/src/routes/$1"
|
|
||||||
},
|
|
||||||
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/dist/"]
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
require("dotenv").config()
|
|
||||||
require("./src/database").umzug.runAsCLI()
|
|
@ -1,65 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "sequelize-typescript-starter",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.ts",
|
|
||||||
"scripts": {
|
|
||||||
"start": "cross-env NODE_ENV=production node dist/index.js",
|
|
||||||
"dev": "cross-env NODE_ENV=development nodemon index.ts",
|
|
||||||
"build": "mkdir -p ./dist && cp .env ./dist/.env && tsc -p ./tsconfig.json && tsc-alias -p ./tsconfig.json && yarn post-build",
|
|
||||||
"build:docker": "mkdir -p ./dist && cp .env.test ./dist/.env && tsc -p ./tsconfig.json && tsc-alias -p ./tsconfig.json && yarn post-build",
|
|
||||||
"post-build": "cp package.json ./dist/package.json && cp yarn.lock ./dist/yarn.lock && cd dist && env NODE_ENV=production yarn install",
|
|
||||||
"migrate:up": "ts-node migrate up",
|
|
||||||
"migrate:down": "ts-node migrate down",
|
|
||||||
"migrate": "ts-node migrate",
|
|
||||||
"lint": "prettier --config .prettierrc 'src/**/*.ts' 'index.ts' --write",
|
|
||||||
"test": "jest --silent"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"bcryptjs": "^2.4.3",
|
|
||||||
"body-parser": "^1.18.2",
|
|
||||||
"celebrate": "^15.0.1",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dotenv": "^16.0.0",
|
|
||||||
"express": "^4.16.2",
|
|
||||||
"express-jwt": "^6.1.1",
|
|
||||||
"jsonwebtoken": "^8.5.1",
|
|
||||||
"marked": "^4.0.12",
|
|
||||||
"nodemon": "^2.0.15",
|
|
||||||
"prism-react-renderer": "^1.3.1",
|
|
||||||
"react": "^18.0.0",
|
|
||||||
"react-dom": "^18.0.0",
|
|
||||||
"reflect-metadata": "^0.1.10",
|
|
||||||
"sequelize": "^6.17.0",
|
|
||||||
"sequelize-typescript": "^2.1.3",
|
|
||||||
"sqlite3": "^5.1.2",
|
|
||||||
"strong-error-handler": "^4.0.0",
|
|
||||||
"umzug": "^3.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bcryptjs": "2.4.2",
|
|
||||||
"@types/cors": "2.8.12",
|
|
||||||
"@types/express": "4.17.13",
|
|
||||||
"@types/express-jwt": "6.0.4",
|
|
||||||
"@types/jest": "27.5.0",
|
|
||||||
"@types/jsonwebtoken": "8.5.8",
|
|
||||||
"@types/marked": "4.0.3",
|
|
||||||
"@types/node": "17.0.21",
|
|
||||||
"@types/node-fetch": "2.6.1",
|
|
||||||
"@types/react-dom": "17.0.16",
|
|
||||||
"@types/supertest": "2.0.12",
|
|
||||||
"@types/validator": "^13.7.10",
|
|
||||||
"cross-env": "7.0.3",
|
|
||||||
"jest": "27.5.1",
|
|
||||||
"prettier": "2.6.2",
|
|
||||||
"supertest": "6.2.3",
|
|
||||||
"ts-jest": "27.1.4",
|
|
||||||
"ts-node": "10.7.0",
|
|
||||||
"tsc-alias": "1.6.7",
|
|
||||||
"tsconfig-paths": "3.14.1",
|
|
||||||
"tslint": "6.1.3",
|
|
||||||
"typescript": "4.6.4"
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,43 +0,0 @@
|
|||||||
import * as express from "express"
|
|
||||||
import * as bodyParser from "body-parser"
|
|
||||||
import * as errorhandler from "strong-error-handler"
|
|
||||||
import { posts, user, auth, files, admin, health } from "@routes/index"
|
|
||||||
import { errors } from "celebrate"
|
|
||||||
import secretKey from "@lib/middleware/secret-key"
|
|
||||||
import markdown from "@lib/render-markdown"
|
|
||||||
import config from "@lib/config"
|
|
||||||
|
|
||||||
export const app = express()
|
|
||||||
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }))
|
|
||||||
app.use(bodyParser.json({ limit: "5mb" }))
|
|
||||||
|
|
||||||
app.use("/auth", auth)
|
|
||||||
app.use("/posts", posts)
|
|
||||||
app.use("/user", user)
|
|
||||||
app.use("/files", files)
|
|
||||||
app.use("/admin", admin)
|
|
||||||
app.use("/health", health)
|
|
||||||
|
|
||||||
app.get("/welcome", secretKey, (req, res) => {
|
|
||||||
const introContent = config.welcome_content
|
|
||||||
const introTitle = config.welcome_title
|
|
||||||
if (!introContent || !introTitle) {
|
|
||||||
return res.status(500).json({ error: "Missing welcome content" })
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
title: introTitle,
|
|
||||||
content: introContent,
|
|
||||||
rendered: markdown(introContent)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(errors())
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
errorhandler({
|
|
||||||
debug: !config.is_production,
|
|
||||||
log: true
|
|
||||||
})
|
|
||||||
)
|
|
@ -1,48 +0,0 @@
|
|||||||
import config from "@lib/config"
|
|
||||||
import databasePath from "@lib/get-database-path"
|
|
||||||
import { Sequelize } from "sequelize-typescript"
|
|
||||||
import { SequelizeStorage, Umzug } from "umzug"
|
|
||||||
|
|
||||||
export const sequelize = new Sequelize({
|
|
||||||
dialect: "sqlite",
|
|
||||||
database: "drift",
|
|
||||||
storage: config.memory_db ? ":memory:" : databasePath,
|
|
||||||
models: [__dirname + "/lib/models"],
|
|
||||||
logging: console.log
|
|
||||||
})
|
|
||||||
|
|
||||||
if (config.memory_db) {
|
|
||||||
console.log("Using in-memory database")
|
|
||||||
} else {
|
|
||||||
console.log(`Database path: ${databasePath}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const umzug = new Umzug({
|
|
||||||
migrations: {
|
|
||||||
glob: config.is_production
|
|
||||||
? __dirname + "/migrations/*.js"
|
|
||||||
: __dirname + "/migrations/*.ts"
|
|
||||||
},
|
|
||||||
context: sequelize.getQueryInterface(),
|
|
||||||
storage: new SequelizeStorage({ sequelize }),
|
|
||||||
logger: console
|
|
||||||
})
|
|
||||||
|
|
||||||
export type Migration = typeof umzug._types.migration
|
|
||||||
|
|
||||||
// If you're in a development environment, you can manually migrate with `yarn migrate:{up,down}` in the `server` folder
|
|
||||||
if (config.is_production) {
|
|
||||||
;(async () => {
|
|
||||||
// Checks migrations and run them if they are not already applied. To keep
|
|
||||||
// track of the executed migrations, a table (and sequelize model) called SequelizeMeta
|
|
||||||
// will be automatically created (if it doesn't exist already) and parsed.
|
|
||||||
console.log("Checking migrations...")
|
|
||||||
const migrations = await umzug.up()
|
|
||||||
if (migrations.length > 0) {
|
|
||||||
console.log("Migrations applied:")
|
|
||||||
console.log(migrations)
|
|
||||||
} else {
|
|
||||||
console.log("No migrations applied.")
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
type Config = {
|
|
||||||
port: number
|
|
||||||
jwt_secret: string
|
|
||||||
drift_home: string
|
|
||||||
is_production: boolean
|
|
||||||
memory_db: boolean
|
|
||||||
enable_admin: boolean
|
|
||||||
secret_key: string
|
|
||||||
registration_password: string
|
|
||||||
welcome_content: string | undefined
|
|
||||||
welcome_title: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
type EnvironmentValue = string | undefined
|
|
||||||
type Environment = { [key: string]: EnvironmentValue }
|
|
||||||
|
|
||||||
export const config = (env: Environment): Config => {
|
|
||||||
const stringToBoolean = (str: EnvironmentValue): boolean => {
|
|
||||||
if (str === "true") {
|
|
||||||
return true
|
|
||||||
} else if (str === "false") {
|
|
||||||
return false
|
|
||||||
} else if (str) {
|
|
||||||
throw new Error(`Invalid boolean value: ${str}`)
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const throwIfUndefined = (str: EnvironmentValue, name: string): string => {
|
|
||||||
if (str === undefined) {
|
|
||||||
throw new Error(`Missing environment variable: ${name}`)
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultIfUndefined = (
|
|
||||||
str: EnvironmentValue,
|
|
||||||
defaultValue: string
|
|
||||||
): string => {
|
|
||||||
if (str === undefined) {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
const validNodeEnvs = (str: EnvironmentValue) => {
|
|
||||||
const valid = ["development", "production", "test"]
|
|
||||||
if (str && !valid.includes(str)) {
|
|
||||||
throw new Error(`Invalid NODE_ENV set: ${str}`)
|
|
||||||
} else if (!str) {
|
|
||||||
console.warn("No NODE_ENV specified, defaulting to development")
|
|
||||||
} else {
|
|
||||||
console.log(`Using NODE_ENV: ${str}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const is_production = env.NODE_ENV === "production"
|
|
||||||
|
|
||||||
const developmentDefault = (
|
|
||||||
str: EnvironmentValue,
|
|
||||||
name: string,
|
|
||||||
defaultValue: string
|
|
||||||
): string => {
|
|
||||||
if (is_production) return throwIfUndefined(str, name)
|
|
||||||
return defaultIfUndefined(str, defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
validNodeEnvs(env.NODE_ENV)
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
port: env.PORT ? parseInt(env.PORT) : 3000,
|
|
||||||
jwt_secret: env.JWT_SECRET || "myjwtsecret",
|
|
||||||
drift_home: env.DRIFT_HOME || "~/.drift",
|
|
||||||
is_production,
|
|
||||||
memory_db: stringToBoolean(env.MEMORY_DB),
|
|
||||||
enable_admin: stringToBoolean(env.ENABLE_ADMIN),
|
|
||||||
secret_key: developmentDefault(env.SECRET_KEY, "SECRET_KEY", "secret"),
|
|
||||||
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
|
||||||
welcome_content: env.WELCOME_CONTENT,
|
|
||||||
welcome_title: env.WELCOME_TITLE
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config(process.env)
|
|
@ -1,16 +0,0 @@
|
|||||||
// https://github.com/thelounge/thelounge/blob/0fb6dae8a68627cd7747ea6164ebe93390fe90f2/src/helper.js#L224
|
|
||||||
|
|
||||||
import * as os from "os"
|
|
||||||
import * as path from "path"
|
|
||||||
import config from "./config"
|
|
||||||
// Expand ~ into the current user home dir.
|
|
||||||
// This does *not* support `~other_user/tmp` => `/home/other_user/tmp`.
|
|
||||||
function getDatabasePath() {
|
|
||||||
const fileName = "drift.sqlite"
|
|
||||||
const databasePath = `${config.drift_home}/${fileName}`
|
|
||||||
|
|
||||||
const home = os.homedir().replace("$", "$$$$")
|
|
||||||
return path.resolve(databasePath.replace(/^~($|\/|\\)/, home + "$1"))
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getDatabasePath()
|
|
@ -1,40 +0,0 @@
|
|||||||
import markdown from "./render-markdown"
|
|
||||||
import { File } from "@lib/models/File"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns rendered HTML from a Drift file
|
|
||||||
*/
|
|
||||||
function getHtmlFromFile({ content, title }: Pick<File, "content" | "title">) {
|
|
||||||
const renderAsMarkdown = [
|
|
||||||
"markdown",
|
|
||||||
"md",
|
|
||||||
"mdown",
|
|
||||||
"mkdn",
|
|
||||||
"mkd",
|
|
||||||
"mdwn",
|
|
||||||
"mdtxt",
|
|
||||||
"mdtext",
|
|
||||||
"text",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
const fileType = () => {
|
|
||||||
const pathParts = title.split(".")
|
|
||||||
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
|
||||||
return language
|
|
||||||
}
|
|
||||||
const type = fileType()
|
|
||||||
let contentToRender: string = content || ""
|
|
||||||
|
|
||||||
if (!renderAsMarkdown.includes(type)) {
|
|
||||||
contentToRender = `~~~${type}
|
|
||||||
${content}
|
|
||||||
~~~`
|
|
||||||
} else {
|
|
||||||
contentToRender = "\n" + content
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = markdown(contentToRender)
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getHtmlFromFile
|
|
@ -1,50 +0,0 @@
|
|||||||
// import * as request from 'supertest'
|
|
||||||
// import { app } from '../../../app'
|
|
||||||
import { NextFunction, Response } from "express"
|
|
||||||
import isAdmin from "@lib/middleware/is-admin"
|
|
||||||
import { UserJwtRequest } from "@lib/middleware/jwt"
|
|
||||||
|
|
||||||
describe("is-admin middlware", () => {
|
|
||||||
let mockRequest: Partial<UserJwtRequest>
|
|
||||||
let mockResponse: Partial<Response>
|
|
||||||
let nextFunction: NextFunction = jest.fn()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockRequest = {}
|
|
||||||
mockResponse = {
|
|
||||||
sendStatus: jest.fn()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return 401 if no authorization header", async () => {
|
|
||||||
const res = mockResponse as Response
|
|
||||||
isAdmin(mockRequest as UserJwtRequest, res, nextFunction)
|
|
||||||
expect(res.sendStatus).toHaveBeenCalledWith(401)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return 401 if no token is supplied", async () => {
|
|
||||||
const req = mockRequest as UserJwtRequest
|
|
||||||
req.headers = {
|
|
||||||
authorization: "Bearer"
|
|
||||||
}
|
|
||||||
isAdmin(req, mockResponse as Response, nextFunction)
|
|
||||||
expect(mockResponse.sendStatus).toBeCalledWith(401)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return 404 if config.enable_admin is false", async () => {
|
|
||||||
jest.mock("../../config", () => ({
|
|
||||||
enable_admin: false
|
|
||||||
}))
|
|
||||||
|
|
||||||
const req = mockRequest as UserJwtRequest
|
|
||||||
req.headers = {
|
|
||||||
authorization: "Bearer 123"
|
|
||||||
}
|
|
||||||
isAdmin(req, mockResponse as Response, nextFunction)
|
|
||||||
expect(mockResponse.sendStatus).toBeCalledWith(404)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: 403 if !isAdmin
|
|
||||||
// Verify it calls next() if admin
|
|
||||||
// Requires mocking config.enable_admin
|
|
||||||
})
|
|
@ -1,48 +0,0 @@
|
|||||||
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
|
|
||||||
import { NextFunction, Response } from "express"
|
|
||||||
|
|
||||||
describe("jwt middlware", () => {
|
|
||||||
let mockRequest: Partial<UserJwtRequest>
|
|
||||||
let mockResponse: Partial<Response>
|
|
||||||
let nextFunction: NextFunction = jest.fn()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockRequest = {}
|
|
||||||
mockResponse = {
|
|
||||||
sendStatus: jest.fn().mockReturnThis()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return 401 if no authorization header", () => {
|
|
||||||
const res = mockResponse as Response
|
|
||||||
jwt(mockRequest as UserJwtRequest, res, nextFunction)
|
|
||||||
expect(res.sendStatus).toHaveBeenCalledWith(401)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return 401 if no token is supplied", () => {
|
|
||||||
const req = mockRequest as UserJwtRequest
|
|
||||||
req.headers = {
|
|
||||||
authorization: "Bearer"
|
|
||||||
}
|
|
||||||
jwt(req, mockResponse as Response, nextFunction)
|
|
||||||
expect(mockResponse.sendStatus).toBeCalledWith(401)
|
|
||||||
})
|
|
||||||
|
|
||||||
// it("should return 401 if token is deleted", async () => {
|
|
||||||
// try {
|
|
||||||
// const tokenString = "123"
|
|
||||||
|
|
||||||
// const req = mockRequest as UserJwtRequest
|
|
||||||
// req.headers = {
|
|
||||||
// authorization: `Bearer ${tokenString}`
|
|
||||||
// }
|
|
||||||
// jwt(req, mockResponse as Response, nextFunction)
|
|
||||||
// expect(mockResponse.sendStatus).toBeCalledWith(401)
|
|
||||||
// expect(mockResponse.json).toBeCalledWith({
|
|
||||||
// message: "Token is no longer valid"
|
|
||||||
// })
|
|
||||||
// } catch (e) {
|
|
||||||
// console.log(e)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
})
|
|
@ -1,46 +0,0 @@
|
|||||||
// import * as request from 'supertest'
|
|
||||||
// import { app } from '../../../app'
|
|
||||||
import { NextFunction, Response } from "express"
|
|
||||||
import { UserJwtRequest } from "@lib/middleware/jwt"
|
|
||||||
import secretKey from "@lib/middleware/secret-key"
|
|
||||||
import config from "@lib/config"
|
|
||||||
|
|
||||||
describe("secret-key middlware", () => {
|
|
||||||
let mockRequest: Partial<UserJwtRequest>
|
|
||||||
let mockResponse: Partial<Response>
|
|
||||||
let nextFunction: NextFunction = jest.fn()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockRequest = {}
|
|
||||||
mockResponse = {
|
|
||||||
sendStatus: jest.fn()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return 401 if no x-secret-key header", async () => {
|
|
||||||
const res = mockResponse as Response
|
|
||||||
secretKey(mockRequest as UserJwtRequest, res, nextFunction)
|
|
||||||
expect(res.sendStatus).toHaveBeenCalledWith(401)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return 401 if x-secret-key does not match server", async () => {
|
|
||||||
const defaultSecretKey = config.secret_key
|
|
||||||
const req = mockRequest as UserJwtRequest
|
|
||||||
req.headers = {
|
|
||||||
authorization: "Bearer",
|
|
||||||
"x-secret-key": defaultSecretKey + "1"
|
|
||||||
}
|
|
||||||
secretKey(req, mockResponse as Response, nextFunction)
|
|
||||||
expect(mockResponse.sendStatus).toBeCalledWith(401)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should call next() if x-secret-key matches server", async () => {
|
|
||||||
const req = mockRequest as UserJwtRequest
|
|
||||||
req.headers = {
|
|
||||||
authorization: "Bearer",
|
|
||||||
"x-secret-key": config.secret_key
|
|
||||||
}
|
|
||||||
secretKey(req, mockResponse as Response, nextFunction)
|
|
||||||
expect(nextFunction).toBeCalled()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,43 +0,0 @@
|
|||||||
import { NextFunction, Request, Response } from "express"
|
|
||||||
import * as jwt from "jsonwebtoken"
|
|
||||||
import config from "../config"
|
|
||||||
import { User as UserModel } from "../models/User"
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserJwtRequest extends Request {
|
|
||||||
user?: User
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function isAdmin(
|
|
||||||
req: UserJwtRequest,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
if (!req.headers?.authorization) {
|
|
||||||
return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
const authHeader = req.headers["authorization"]
|
|
||||||
const token = authHeader && authHeader.split(" ")[1]
|
|
||||||
if (!token) return res.sendStatus(401)
|
|
||||||
if (!config.enable_admin) return res.sendStatus(404)
|
|
||||||
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
|
|
||||||
if (err) return res.sendStatus(403)
|
|
||||||
const userObj = await UserModel.findByPk(user.id, {
|
|
||||||
attributes: {
|
|
||||||
exclude: ["password"]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!userObj || userObj.role !== "admin") {
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = userObj
|
|
||||||
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
import { AuthToken } from "@lib/models/AuthToken"
|
|
||||||
import { NextFunction, Request, Response } from "express"
|
|
||||||
import * as jwt from "jsonwebtoken"
|
|
||||||
import config from "../config"
|
|
||||||
import { User as UserModel } from "../models/User"
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserJwtRequest extends Request {
|
|
||||||
user?: User
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function authenticateToken(
|
|
||||||
req: UserJwtRequest,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const authHeader = req.headers ? req.headers["authorization"] : undefined
|
|
||||||
const token = authHeader && authHeader.split(" ")[1]
|
|
||||||
|
|
||||||
if (token == null) return res.sendStatus(401)
|
|
||||||
|
|
||||||
const authToken = await AuthToken.findOne({ where: { token: token } })
|
|
||||||
if (authToken == null) {
|
|
||||||
return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authToken.deletedAt) {
|
|
||||||
return res.sendStatus(401).json({
|
|
||||||
message: "Token is no longer valid"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
|
|
||||||
if (err) return res.sendStatus(403)
|
|
||||||
const userObj = await UserModel.findByPk(user.id, {
|
|
||||||
attributes: {
|
|
||||||
exclude: ["password"]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!userObj) {
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
req.user = user
|
|
||||||
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
import config from "@lib/config"
|
|
||||||
import { NextFunction, Request, Response } from "express"
|
|
||||||
|
|
||||||
export default function authenticateToken(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
if (!(req.headers && req.headers["x-secret-key"])) {
|
|
||||||
return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestKey = req.headers["x-secret-key"]
|
|
||||||
if (requestKey !== config.secret_key) {
|
|
||||||
return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
import {
|
|
||||||
Model,
|
|
||||||
Column,
|
|
||||||
Table,
|
|
||||||
IsUUID,
|
|
||||||
PrimaryKey,
|
|
||||||
DataType,
|
|
||||||
CreatedAt,
|
|
||||||
UpdatedAt,
|
|
||||||
DeletedAt,
|
|
||||||
Unique,
|
|
||||||
BelongsTo,
|
|
||||||
ForeignKey
|
|
||||||
} from "sequelize-typescript"
|
|
||||||
import { User } from "./User"
|
|
||||||
|
|
||||||
@Table
|
|
||||||
export class AuthToken extends Model {
|
|
||||||
@IsUUID(4)
|
|
||||||
@PrimaryKey
|
|
||||||
@Unique
|
|
||||||
@Column({
|
|
||||||
type: DataType.UUID,
|
|
||||||
defaultValue: DataType.UUIDV4
|
|
||||||
})
|
|
||||||
id!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
token!: string
|
|
||||||
|
|
||||||
@BelongsTo(() => User, "userId")
|
|
||||||
user!: User
|
|
||||||
|
|
||||||
@ForeignKey(() => User)
|
|
||||||
@Column
|
|
||||||
userId!: number
|
|
||||||
|
|
||||||
@Column
|
|
||||||
expiredReason?: string
|
|
||||||
|
|
||||||
@CreatedAt
|
|
||||||
@Column
|
|
||||||
createdAt!: Date
|
|
||||||
|
|
||||||
@UpdatedAt
|
|
||||||
@Column
|
|
||||||
updatedAt!: Date
|
|
||||||
|
|
||||||
@DeletedAt
|
|
||||||
@Column
|
|
||||||
deletedAt?: Date
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
import {
|
|
||||||
BelongsTo,
|
|
||||||
Column,
|
|
||||||
CreatedAt,
|
|
||||||
DataType,
|
|
||||||
ForeignKey,
|
|
||||||
IsUUID,
|
|
||||||
Model,
|
|
||||||
PrimaryKey,
|
|
||||||
Scopes,
|
|
||||||
Table,
|
|
||||||
Unique
|
|
||||||
} from "sequelize-typescript"
|
|
||||||
import { Post } from "./Post"
|
|
||||||
import { User } from "./User"
|
|
||||||
|
|
||||||
@Scopes(() => ({
|
|
||||||
full: {
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
through: { attributes: [] }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Post,
|
|
||||||
through: { attributes: [] }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
@Table({
|
|
||||||
tableName: "files"
|
|
||||||
})
|
|
||||||
export class File extends Model {
|
|
||||||
@IsUUID(4)
|
|
||||||
@PrimaryKey
|
|
||||||
@Unique
|
|
||||||
@Column({
|
|
||||||
type: DataType.UUID,
|
|
||||||
defaultValue: DataType.UUIDV4
|
|
||||||
})
|
|
||||||
id!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
title!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
content!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
sha!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
html!: string
|
|
||||||
|
|
||||||
@BelongsTo(() => User, "userId")
|
|
||||||
user!: User
|
|
||||||
|
|
||||||
@BelongsTo(() => Post, "postId")
|
|
||||||
post!: Post
|
|
||||||
|
|
||||||
@ForeignKey(() => User)
|
|
||||||
@Column
|
|
||||||
userId!: number
|
|
||||||
|
|
||||||
@ForeignKey(() => Post)
|
|
||||||
@Column
|
|
||||||
postId!: number
|
|
||||||
|
|
||||||
@CreatedAt
|
|
||||||
@Column
|
|
||||||
createdAt!: Date
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
import {
|
|
||||||
BelongsToMany,
|
|
||||||
Column,
|
|
||||||
CreatedAt,
|
|
||||||
DataType,
|
|
||||||
HasMany,
|
|
||||||
HasOne,
|
|
||||||
IsUUID,
|
|
||||||
Model,
|
|
||||||
PrimaryKey,
|
|
||||||
Scopes,
|
|
||||||
Table,
|
|
||||||
Unique,
|
|
||||||
UpdatedAt
|
|
||||||
} from "sequelize-typescript"
|
|
||||||
import { PostAuthor } from "./PostAuthor"
|
|
||||||
import { User } from "./User"
|
|
||||||
import { File } from "./File"
|
|
||||||
|
|
||||||
@Scopes(() => ({
|
|
||||||
user: {
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
through: { attributes: [] }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
full: {
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
through: { attributes: [] }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: File,
|
|
||||||
through: { attributes: [] }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
@Table({
|
|
||||||
tableName: "posts"
|
|
||||||
})
|
|
||||||
export class Post extends Model {
|
|
||||||
@IsUUID(4)
|
|
||||||
@PrimaryKey
|
|
||||||
@Unique
|
|
||||||
@Column({
|
|
||||||
type: DataType.UUID,
|
|
||||||
defaultValue: DataType.UUIDV4
|
|
||||||
})
|
|
||||||
id!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
title!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
description?: string
|
|
||||||
|
|
||||||
@BelongsToMany(() => User, () => PostAuthor)
|
|
||||||
users?: User[]
|
|
||||||
|
|
||||||
@HasMany(() => File, { constraints: false })
|
|
||||||
files?: File[]
|
|
||||||
|
|
||||||
@CreatedAt
|
|
||||||
@Column
|
|
||||||
createdAt!: Date
|
|
||||||
|
|
||||||
@Column
|
|
||||||
visibility!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
password?: string
|
|
||||||
|
|
||||||
@UpdatedAt
|
|
||||||
@Column
|
|
||||||
updatedAt!: Date
|
|
||||||
|
|
||||||
@Column
|
|
||||||
deletedAt?: Date
|
|
||||||
|
|
||||||
@Column
|
|
||||||
expiresAt?: Date
|
|
||||||
|
|
||||||
@HasOne(() => Post, { foreignKey: "parentId", constraints: false })
|
|
||||||
parent?: Post
|
|
||||||
|
|
||||||
// TODO: deletedBy
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import {
|
|
||||||
Model,
|
|
||||||
Column,
|
|
||||||
Table,
|
|
||||||
ForeignKey,
|
|
||||||
IsUUID,
|
|
||||||
PrimaryKey,
|
|
||||||
DataType,
|
|
||||||
Unique
|
|
||||||
} from "sequelize-typescript"
|
|
||||||
import { Post } from "./Post"
|
|
||||||
import { User } from "./User"
|
|
||||||
|
|
||||||
@Table({
|
|
||||||
tableName: "post_authors"
|
|
||||||
})
|
|
||||||
export class PostAuthor extends Model {
|
|
||||||
@IsUUID(4)
|
|
||||||
@PrimaryKey
|
|
||||||
@Unique
|
|
||||||
@Column({
|
|
||||||
type: DataType.UUID,
|
|
||||||
defaultValue: DataType.UUIDV4
|
|
||||||
})
|
|
||||||
id!: string
|
|
||||||
|
|
||||||
@ForeignKey(() => Post)
|
|
||||||
@Column
|
|
||||||
postId!: number
|
|
||||||
|
|
||||||
@ForeignKey(() => User)
|
|
||||||
@Column
|
|
||||||
userId!: number
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
import {
|
|
||||||
Model,
|
|
||||||
Column,
|
|
||||||
Table,
|
|
||||||
BelongsToMany,
|
|
||||||
Scopes,
|
|
||||||
CreatedAt,
|
|
||||||
UpdatedAt,
|
|
||||||
IsUUID,
|
|
||||||
PrimaryKey,
|
|
||||||
DataType,
|
|
||||||
Unique
|
|
||||||
} from "sequelize-typescript"
|
|
||||||
import { Post } from "./Post"
|
|
||||||
import { PostAuthor } from "./PostAuthor"
|
|
||||||
|
|
||||||
@Scopes(() => ({
|
|
||||||
posts: {
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Post,
|
|
||||||
through: { attributes: [] }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
withoutPassword: {
|
|
||||||
attributes: {
|
|
||||||
exclude: ["password"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
@Table({
|
|
||||||
tableName: "users"
|
|
||||||
})
|
|
||||||
export class User extends Model {
|
|
||||||
@IsUUID(4)
|
|
||||||
@PrimaryKey
|
|
||||||
@Unique
|
|
||||||
@Column({
|
|
||||||
type: DataType.UUID,
|
|
||||||
defaultValue: DataType.UUIDV4
|
|
||||||
})
|
|
||||||
id!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
username!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
password!: string
|
|
||||||
|
|
||||||
@BelongsToMany(() => Post, () => PostAuthor)
|
|
||||||
posts?: Post[]
|
|
||||||
|
|
||||||
@CreatedAt
|
|
||||||
@Column
|
|
||||||
createdAt!: Date
|
|
||||||
|
|
||||||
@UpdatedAt
|
|
||||||
@Column
|
|
||||||
updatedAt!: Date
|
|
||||||
|
|
||||||
@Column
|
|
||||||
role!: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
email?: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
displayName?: string
|
|
||||||
|
|
||||||
@Column
|
|
||||||
bio?: string
|
|
||||||
}
|
|
@ -1,152 +0,0 @@
|
|||||||
import { marked } from 'marked'
|
|
||||||
import Highlight, { defaultProps, Language, } from 'prism-react-renderer'
|
|
||||||
import { renderToStaticMarkup } from 'react-dom/server'
|
|
||||||
|
|
||||||
// // image sizes. DDoS Safe?
|
|
||||||
// const imageSizeLink = /^!?\[((?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?)\]\(\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f()\\]*\)|[^\s\x00-\x1f()\\])*?(?:\s+=(?:[\w%]+)?x(?:[\w%]+)?)?)(?:\s+("(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)))?\s*\)/;
|
|
||||||
// //@ts-ignore
|
|
||||||
// Lexer.rules.inline.normal.link = imageSizeLink;
|
|
||||||
// //@ts-ignore
|
|
||||||
// Lexer.rules.inline.gfm.link = imageSizeLink;
|
|
||||||
// //@ts-ignore
|
|
||||||
// Lexer.rules.inline.breaks.link = imageSizeLink;
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
delete defaultProps.theme
|
|
||||||
// import linkStyles from '../components/link/link.module.css'
|
|
||||||
|
|
||||||
const renderer = new marked.Renderer()
|
|
||||||
|
|
||||||
renderer.heading = (text, level, _, slugger) => {
|
|
||||||
const id = slugger.slug(text)
|
|
||||||
const Component = `h${level}`
|
|
||||||
|
|
||||||
return renderToStaticMarkup(
|
|
||||||
//@ts-ignore
|
|
||||||
<Component>
|
|
||||||
<a href={`#${id}`} id={id} style={{ color: "inherit" }} dangerouslySetInnerHTML={{ __html: (text) }} >
|
|
||||||
</a>
|
|
||||||
</Component>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderer.link = (href, _, text) => {
|
|
||||||
// const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
|
|
||||||
// if (isHrefLocal) {
|
|
||||||
// return renderToStaticMarkup(
|
|
||||||
// <a href={href || ''}>
|
|
||||||
// {text}
|
|
||||||
// </a>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // dirty hack
|
|
||||||
// // if text contains elements, render as html
|
|
||||||
// return <a href={href || ""} target="_blank" rel="noopener noreferrer" dangerouslySetInnerHTML={{ __html: convertHtmlEntities(text) }} ></a>
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
renderer.image = function (href, _, text) {
|
|
||||||
return `<Image loading="lazy" src="${href}" alt="${text}" layout="fill" />`
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.checkbox = () => ''
|
|
||||||
renderer.listitem = (text, task, checked) => {
|
|
||||||
if (task) {
|
|
||||||
return `<li class="reset"><span class="check">​<input type="checkbox" disabled ${checked ? 'checked' : ''
|
|
||||||
} /></span><span>${text}</span></li>`
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<li>${text}</li>`
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.code = (code: string, language: string) => {
|
|
||||||
return renderToStaticMarkup(
|
|
||||||
<pre>
|
|
||||||
{/* {title && <code>{title} </code>} */}
|
|
||||||
{/* {language && title && <code style={{}}> {language} </code>} */}
|
|
||||||
<Code
|
|
||||||
language={language}
|
|
||||||
// title={title}
|
|
||||||
code={code}
|
|
||||||
// highlight={highlight}
|
|
||||||
/>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
marked.setOptions({
|
|
||||||
gfm: true,
|
|
||||||
breaks: true,
|
|
||||||
headerIds: true,
|
|
||||||
renderer,
|
|
||||||
})
|
|
||||||
|
|
||||||
const markdown = (markdown: string) => marked(markdown)
|
|
||||||
|
|
||||||
export default markdown
|
|
||||||
|
|
||||||
const Code = ({ code, language, highlight, title, ...props }: {
|
|
||||||
code: string,
|
|
||||||
language: string,
|
|
||||||
highlight?: string,
|
|
||||||
title?: string,
|
|
||||||
}) => {
|
|
||||||
if (!language)
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<code {...props} dangerouslySetInnerHTML={{ __html: code }} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
const highlightedLines = highlight
|
|
||||||
//@ts-ignore
|
|
||||||
? highlight.split(',').reduce((lines, h) => {
|
|
||||||
if (h.includes('-')) {
|
|
||||||
// Expand ranges like 3-5 into [3,4,5]
|
|
||||||
const [start, end] = h.split('-').map(Number)
|
|
||||||
const x = Array(end - start + 1)
|
|
||||||
.fill(undefined)
|
|
||||||
.map((_, i) => i + start)
|
|
||||||
return [...lines, ...x]
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...lines, Number(h)]
|
|
||||||
}, [])
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// https://mdxjs.com/guides/syntax-harkedighlighting#all-together
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Highlight {...defaultProps} code={code.trim()} language={language as Language} >
|
|
||||||
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
|
||||||
<code className={className} style={{ ...style }}>
|
|
||||||
{
|
|
||||||
tokens.map((line, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
{...getLineProps({ line, key: i })}
|
|
||||||
style={
|
|
||||||
//@ts-ignore
|
|
||||||
highlightedLines.includes((i + 1).toString())
|
|
||||||
? {
|
|
||||||
background: 'var(--highlight)',
|
|
||||||
margin: '0 -1rem',
|
|
||||||
padding: '0 1rem',
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
line.map((token, key) => (
|
|
||||||
<span key={key} {...getTokenProps({ token, key })} />
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</code>
|
|
||||||
)}
|
|
||||||
</Highlight>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
import { DataTypes } from "sequelize"
|
|
||||||
import type { Migration } from "../database"
|
|
||||||
|
|
||||||
export const up: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.createTable("users", {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
deletedAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const down: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.dropTable("users")
|
|
@ -1,12 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
import { DataTypes } from "sequelize"
|
|
||||||
import type { Migration } from "../database"
|
|
||||||
|
|
||||||
export const up: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.addColumn("users", "role", {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: "user"
|
|
||||||
})
|
|
||||||
|
|
||||||
export const down: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.removeColumn("users", "role")
|
|
@ -1,34 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
import { DataTypes } from "sequelize"
|
|
||||||
import type { Migration } from "../database"
|
|
||||||
|
|
||||||
export const up: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.createTable("posts", {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
},
|
|
||||||
visibility: {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
deletedAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const down: Migration = async ({ context: queryInterface }) =>
|
|
||||||
await queryInterface.dropTable("posts")
|
|
@ -1,58 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
import { DataTypes } from "sequelize"
|
|
||||||
import type { Migration } from "../database"
|
|
||||||
|
|
||||||
export const up: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.createTable("files", {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
},
|
|
||||||
sha: {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
},
|
|
||||||
html: {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
deletedAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: "users",
|
|
||||||
key: "id"
|
|
||||||
},
|
|
||||||
onDelete: "SET NULL",
|
|
||||||
onUpdate: "CASCADE"
|
|
||||||
},
|
|
||||||
postId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: "posts",
|
|
||||||
key: "id"
|
|
||||||
},
|
|
||||||
onDelete: "SET NULL",
|
|
||||||
onUpdate: "CASCADE"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const down: Migration = async ({ context: queryInterface }) =>
|
|
||||||
await queryInterface.dropTable("files")
|
|
@ -1,41 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
import { DataTypes } from "sequelize"
|
|
||||||
import type { Migration } from "../database"
|
|
||||||
|
|
||||||
export const up: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.createTable("post_authors", {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
postId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
references: {
|
|
||||||
model: "posts",
|
|
||||||
key: "id"
|
|
||||||
},
|
|
||||||
onDelete: "CASCADE",
|
|
||||||
onUpdate: "CASCADE"
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
references: {
|
|
||||||
model: "users",
|
|
||||||
key: "id"
|
|
||||||
},
|
|
||||||
onDelete: "CASCADE",
|
|
||||||
onUpdate: "CASCADE"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const down: Migration = async ({ context: queryInterface }) =>
|
|
||||||
await queryInterface.dropTable("post_authors")
|
|
@ -1,12 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
import { DataTypes } from "sequelize"
|
|
||||||
import type { Migration } from "../database"
|
|
||||||
|
|
||||||
export const up: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.addColumn("posts", "expiresAt", {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
})
|
|
||||||
|
|
||||||
export const down: Migration = async ({ context: queryInterface }) =>
|
|
||||||
await queryInterface.removeColumn("posts", "expiresAt")
|
|
@ -1,12 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
import { DataTypes } from "sequelize"
|
|
||||||
import type { Migration } from "../database"
|
|
||||||
|
|
||||||
export const up: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.addColumn("posts", "parentId", {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
})
|
|
||||||
|
|
||||||
export const down: Migration = async ({ context: queryInterface }) =>
|
|
||||||
await queryInterface.removeColumn("posts", "parentId")
|
|
@ -1,43 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
import { DataTypes } from "sequelize"
|
|
||||||
import type { Migration } from "../database"
|
|
||||||
|
|
||||||
export const up: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.createTable("AuthTokens", {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
token: {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
},
|
|
||||||
expiredReason: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE
|
|
||||||
},
|
|
||||||
deletedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: "users",
|
|
||||||
key: "id"
|
|
||||||
},
|
|
||||||
onUpdate: "CASCADE",
|
|
||||||
onDelete: "CASCADE"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const down: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.dropTable("AuthTokens")
|
|
@ -1,12 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
import { DataTypes } from "sequelize"
|
|
||||||
import type { Migration } from "../database"
|
|
||||||
|
|
||||||
export const up: Migration = async ({ context: queryInterface }) =>
|
|
||||||
queryInterface.addColumn("posts", "description", {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
})
|
|
||||||
|
|
||||||
export const down: Migration = async ({ context: queryInterface }) =>
|
|
||||||
await queryInterface.removeColumn("posts", "description")
|
|
@ -1,26 +0,0 @@
|
|||||||
"use strict"
|
|
||||||
import { DataTypes } from "sequelize"
|
|
||||||
import type { Migration } from "../database"
|
|
||||||
|
|
||||||
export const up: Migration = async ({ context: queryInterface }) =>
|
|
||||||
Promise.all([
|
|
||||||
queryInterface.addColumn("users", "email", {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
}),
|
|
||||||
queryInterface.addColumn("users", "displayName", {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
}),
|
|
||||||
queryInterface.addColumn("users", "bio", {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
export const down: Migration = async ({ context: queryInterface }) =>
|
|
||||||
Promise.all([
|
|
||||||
queryInterface.removeColumn("users", "email"),
|
|
||||||
queryInterface.removeColumn("users", "displayName"),
|
|
||||||
queryInterface.removeColumn("users", "bio")
|
|
||||||
])
|
|
@ -1,10 +0,0 @@
|
|||||||
import { createServer } from "http"
|
|
||||||
import { app } from "./app"
|
|
||||||
import config from "./lib/config"
|
|
||||||
import "./database"
|
|
||||||
;(async () => {
|
|
||||||
// await sequelize.sync()
|
|
||||||
createServer(app).listen(config.port, () =>
|
|
||||||
console.info(`Server running on port ${config.port}`)
|
|
||||||
)
|
|
||||||
})()
|
|
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"compileOnSave": false,
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es6",
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"declaration": false,
|
|
||||||
"pretty": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strictPropertyInitialization": true,
|
|
||||||
"outDir": "dist",
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@routes/*": ["./src/routes/*"],
|
|
||||||
"@lib/*": ["./src/lib/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ts-node": {
|
|
||||||
"require": ["tsconfig-paths/register"]
|
|
||||||
},
|
|
||||||
"include": ["index.ts", "src/**/*.ts"],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "tslint:latest",
|
|
||||||
"rules": {
|
|
||||||
"arrow-parens": false,
|
|
||||||
"class-name": false,
|
|
||||||
"no-empty": false,
|
|
||||||
"unified-signatures": false,
|
|
||||||
"no-trailing-whitespace": false,
|
|
||||||
"no-submodule-imports": false,
|
|
||||||
"object-literal-sort-keys": false,
|
|
||||||
"interface-name": false,
|
|
||||||
"no-consecutive-blank-lines": false,
|
|
||||||
"no-object-literal-type-assertion": false,
|
|
||||||
"no-unused-expression": false,
|
|
||||||
"trailing-comma": false,
|
|
||||||
"ordered-imports": false,
|
|
||||||
"no-unused-imports": [true, {
|
|
||||||
"ignoreExports": true,
|
|
||||||
"ignoreDeclarations": true,
|
|
||||||
"ignoreTypeReferences": true
|
|
||||||
}],
|
|
||||||
"max-line-length": [
|
|
||||||
true,
|
|
||||||
140
|
|
||||||
],
|
|
||||||
"member-access": false,
|
|
||||||
"no-string-literal": false,
|
|
||||||
"curly": false,
|
|
||||||
"only-arrow-functions": false,
|
|
||||||
"typedef": [
|
|
||||||
true
|
|
||||||
],
|
|
||||||
"no-var-requires": false,
|
|
||||||
"quotemark": [
|
|
||||||
"single"
|
|
||||||
],
|
|
||||||
"triple-equals": true,
|
|
||||||
"member-ordering": [
|
|
||||||
true,
|
|
||||||
"public-before-private",
|
|
||||||
"variables-before-functions"
|
|
||||||
],
|
|
||||||
"variable-name": [
|
|
||||||
true,
|
|
||||||
"ban-keywords",
|
|
||||||
"check-format",
|
|
||||||
"allow-leading-underscore",
|
|
||||||
"allow-pascal-case"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue