server: add basic is-admin tests and bug fixes
This commit is contained in:
parent
06d847dfa3
commit
e5b9b65b55
9 changed files with 182 additions and 106 deletions
|
@ -49,9 +49,9 @@ const ExpirationBadge = ({
|
||||||
return (
|
return (
|
||||||
<Badge type={isExpired ? "error" : "warning"}>
|
<Badge type={isExpired ? "error" : "warning"}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
hideArrow
|
||||||
text={`${expirationDate.toLocaleDateString()} ${expirationDate.toLocaleTimeString()}`}>
|
text={`${expirationDate.toLocaleDateString()} ${expirationDate.toLocaleTimeString()}`}>
|
||||||
{isExpired ? "Expired" : `Expires ${timeUntilString}`}
|
{isExpired ? "Expired" : `Expires ${timeUntilString}`}
|
||||||
hideArrow
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import * as request from 'supertest'
|
import * as request from "supertest"
|
||||||
import { app } from '../app'
|
import { app } from "../app"
|
||||||
|
|
||||||
describe('GET /health', () => {
|
describe("GET /health", () => {
|
||||||
it('should return 200 and a status up', (done) => {
|
it("should return 200 and a status up", (done) => {
|
||||||
request(app)
|
request(app)
|
||||||
.get(`/health`)
|
.get(`/health`)
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
if (err) return done(err)
|
if (err) return done(err)
|
||||||
expect(res.body).toMatchObject({ 'status': 'UP' })
|
expect(res.body).toMatchObject({ status: "UP" })
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,62 +1,64 @@
|
||||||
import { config } from "../config"
|
import { config } from "../config"
|
||||||
|
|
||||||
describe("Config", () => {
|
describe("Config", () => {
|
||||||
it("should build a valid development config when no environment is set", () => {
|
it("should build a valid development config when no environment is set", () => {
|
||||||
const emptyEnv = {};
|
const emptyEnv = {}
|
||||||
const result = config(emptyEnv);
|
const result = config(emptyEnv)
|
||||||
|
|
||||||
expect(result).toHaveProperty("is_production", false)
|
expect(result).toHaveProperty("is_production", false)
|
||||||
expect(result).toHaveProperty("port")
|
expect(result).toHaveProperty("port")
|
||||||
expect(result).toHaveProperty("jwt_secret")
|
expect(result).toHaveProperty("jwt_secret")
|
||||||
expect(result).toHaveProperty("drift_home")
|
expect(result).toHaveProperty("drift_home")
|
||||||
expect(result).toHaveProperty("memory_db")
|
expect(result).toHaveProperty("memory_db")
|
||||||
expect(result).toHaveProperty("enable_admin")
|
expect(result).toHaveProperty("enable_admin")
|
||||||
expect(result).toHaveProperty("secret_key")
|
expect(result).toHaveProperty("secret_key")
|
||||||
expect(result).toHaveProperty("registration_password")
|
expect(result).toHaveProperty("registration_password")
|
||||||
expect(result).toHaveProperty("welcome_content")
|
expect(result).toHaveProperty("welcome_content")
|
||||||
expect(result).toHaveProperty("welcome_title")
|
expect(result).toHaveProperty("welcome_title")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should fail when building a prod environment without SECRET_KEY", () => {
|
it("should fail when building a prod environment without SECRET_KEY", () => {
|
||||||
expect(() => config({ NODE_ENV: "production" }))
|
expect(() => config({ NODE_ENV: "production" })).toThrow(
|
||||||
.toThrow(new Error("Missing environment variable: SECRET_KEY"))
|
new Error("Missing environment variable: SECRET_KEY")
|
||||||
})
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("should build a prod config with a SECRET_KEY", () => {
|
it("should build a prod config with a SECRET_KEY", () => {
|
||||||
const result = config({ NODE_ENV: "production", SECRET_KEY: "secret" })
|
const result = config({ NODE_ENV: "production", SECRET_KEY: "secret" })
|
||||||
|
|
||||||
expect(result).toHaveProperty("is_production", true)
|
expect(result).toHaveProperty("is_production", true)
|
||||||
expect(result).toHaveProperty("secret_key", "secret")
|
expect(result).toHaveProperty("secret_key", "secret")
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("jwt_secret", () => {
|
describe("jwt_secret", () => {
|
||||||
it("should use default jwt_secret when environment is blank string", () => {
|
it("should use default jwt_secret when environment is blank string", () => {
|
||||||
const result = config({ JWT_SECRET: "" })
|
const result = config({ JWT_SECRET: "" })
|
||||||
|
|
||||||
expect(result).toHaveProperty("is_production", false)
|
expect(result).toHaveProperty("is_production", false)
|
||||||
expect(result).toHaveProperty("jwt_secret", "myjwtsecret")
|
expect(result).toHaveProperty("jwt_secret", "myjwtsecret")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("booleans", () => {
|
describe("booleans", () => {
|
||||||
it("should parse 'true' as true", () => {
|
it("should parse 'true' as true", () => {
|
||||||
const result = config({ MEMORY_DB: "true" })
|
const result = config({ MEMORY_DB: "true" })
|
||||||
|
|
||||||
expect(result).toHaveProperty("memory_db", true)
|
expect(result).toHaveProperty("memory_db", true)
|
||||||
})
|
})
|
||||||
it("should parse 'false' as false", () => {
|
it("should parse 'false' as false", () => {
|
||||||
const result = config({ MEMORY_DB: "false" })
|
const result = config({ MEMORY_DB: "false" })
|
||||||
|
|
||||||
expect(result).toHaveProperty("memory_db", false)
|
expect(result).toHaveProperty("memory_db", false)
|
||||||
})
|
})
|
||||||
it("should fail when it is not parseable", () => {
|
it("should fail when it is not parseable", () => {
|
||||||
expect(() => config({ MEMORY_DB: "foo" }))
|
expect(() => config({ MEMORY_DB: "foo" })).toThrow(
|
||||||
.toThrow(new Error("Invalid boolean value: foo"))
|
new Error("Invalid boolean value: foo")
|
||||||
})
|
)
|
||||||
it("should default to false when the string is empty", () => {
|
})
|
||||||
const result = config({ MEMORY_DB: "" })
|
it("should default to false when the string is empty", () => {
|
||||||
|
const result = config({ MEMORY_DB: "" })
|
||||||
|
|
||||||
expect(result).toHaveProperty("memory_db", false)
|
expect(result).toHaveProperty("memory_db", false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
17
server/src/lib/__tests__/get-html-from-drift-file.ts
Normal file
17
server/src/lib/__tests__/get-html-from-drift-file.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import getHtmlFromFile from "@lib/get-html-from-drift-file"
|
||||||
|
|
||||||
|
describe("get-html-from-drift-file", () => {
|
||||||
|
it("should not wrap markdown in code blocks", () => {
|
||||||
|
const markdown = `## My Markdown`
|
||||||
|
const html = getHtmlFromFile({ content: markdown, title: "my-markdown.md" })
|
||||||
|
// the string is <h2><a href=\"#my-markdown\" id=\"my-markdown\" style=\"color:inherit\">My Markdown</a></h2>,
|
||||||
|
// but we dont wan't to be too strict in case markup changes
|
||||||
|
expect(html).toMatch(/<h2><a.*<\/a><\/h2>/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should wrap code in code blocks", () => {
|
||||||
|
const code = `const foo = "bar"`
|
||||||
|
const html = getHtmlFromFile({ content: code, title: "my-code.js" })
|
||||||
|
expect(html).toMatch(/<pre><code class="prism-code language-js">/)
|
||||||
|
})
|
||||||
|
})
|
50
server/src/lib/__tests__/middleware/is-admin.ts
Normal file
50
server/src/lib/__tests__/middleware/is-admin.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// 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
|
||||||
|
})
|
|
@ -6,12 +6,12 @@ type Config = {
|
||||||
memory_db: boolean
|
memory_db: boolean
|
||||||
enable_admin: boolean
|
enable_admin: boolean
|
||||||
secret_key: string
|
secret_key: string
|
||||||
registration_password: string,
|
registration_password: string
|
||||||
welcome_content: string | undefined,
|
welcome_content: string | undefined
|
||||||
welcome_title: string | undefined,
|
welcome_title: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnvironmentValue = string | undefined;
|
type EnvironmentValue = string | undefined
|
||||||
type Environment = { [key: string]: EnvironmentValue }
|
type Environment = { [key: string]: EnvironmentValue }
|
||||||
|
|
||||||
export const config = (env: Environment): Config => {
|
export const config = (env: Environment): Config => {
|
||||||
|
@ -34,7 +34,10 @@ export const config = (env: Environment): Config => {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultIfUndefined = (str: EnvironmentValue, defaultValue: string): string => {
|
const defaultIfUndefined = (
|
||||||
|
str: EnvironmentValue,
|
||||||
|
defaultValue: string
|
||||||
|
): string => {
|
||||||
if (str === undefined) {
|
if (str === undefined) {
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
@ -52,11 +55,15 @@ export const config = (env: Environment): Config => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const is_production = env.NODE_ENV === "production";
|
const is_production = env.NODE_ENV === "production"
|
||||||
|
|
||||||
const developmentDefault = (str: EnvironmentValue, name: string, defaultValue: string): string => {
|
const developmentDefault = (
|
||||||
if (is_production) return throwIfUndefined(str, name);
|
str: EnvironmentValue,
|
||||||
return defaultIfUndefined(str, defaultValue);
|
name: string,
|
||||||
|
defaultValue: string
|
||||||
|
): string => {
|
||||||
|
if (is_production) return throwIfUndefined(str, name)
|
||||||
|
return defaultIfUndefined(str, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
validNodeEnvs(env.NODE_ENV)
|
validNodeEnvs(env.NODE_ENV)
|
||||||
|
@ -72,7 +79,6 @@ export const config = (env: Environment): Config => {
|
||||||
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
||||||
welcome_content: env.WELCOME_CONTENT,
|
welcome_content: env.WELCOME_CONTENT,
|
||||||
welcome_title: env.WELCOME_TITLE
|
welcome_title: env.WELCOME_TITLE
|
||||||
|
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,37 +5,35 @@ import { File } from "@lib/models/File"
|
||||||
* returns rendered HTML from a Drift file
|
* returns rendered HTML from a Drift file
|
||||||
*/
|
*/
|
||||||
function getHtmlFromFile({ content, title }: Pick<File, "content" | "title">) {
|
function getHtmlFromFile({ content, title }: Pick<File, "content" | "title">) {
|
||||||
const renderAsMarkdown = [
|
const renderAsMarkdown = [
|
||||||
"markdown",
|
"markdown",
|
||||||
"md",
|
"md",
|
||||||
"mdown",
|
"mdown",
|
||||||
"mkdn",
|
"mkdn",
|
||||||
"mkd",
|
"mkd",
|
||||||
"mdwn",
|
"mdwn",
|
||||||
"mdtxt",
|
"mdtxt",
|
||||||
"mdtext",
|
"mdtext",
|
||||||
"text",
|
"text",
|
||||||
""
|
""
|
||||||
]
|
]
|
||||||
const fileType = () => {
|
const fileType = () => {
|
||||||
const pathParts = title.split(".")
|
const pathParts = title.split(".")
|
||||||
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
||||||
return language
|
return language
|
||||||
}
|
}
|
||||||
const type = fileType()
|
const type = fileType()
|
||||||
let contentToRender: string = content || ""
|
let contentToRender: string = content || ""
|
||||||
|
|
||||||
if (!renderAsMarkdown.includes(type)) {
|
if (!renderAsMarkdown.includes(type)) {
|
||||||
contentToRender = `~~~${type}
|
contentToRender = `~~~${type}
|
||||||
${content}
|
${content}
|
||||||
~~~`
|
~~~`
|
||||||
} else {
|
} else {
|
||||||
contentToRender = "\n" + content
|
contentToRender = "\n" + content
|
||||||
}
|
}
|
||||||
console.log(contentToRender.slice(0, 50))
|
const html = markdown(contentToRender)
|
||||||
const html = markdown(contentToRender)
|
return html
|
||||||
return html
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default getHtmlFromFile
|
||||||
export default getHtmlFromFile
|
|
||||||
|
|
|
@ -11,16 +11,20 @@ export interface UserJwtRequest extends Request {
|
||||||
user?: User
|
user?: User
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function authenticateToken(
|
export default function isAdmin(
|
||||||
req: UserJwtRequest,
|
req: UserJwtRequest,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
|
if (!req.headers?.authorization) {
|
||||||
|
return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
|
||||||
const authHeader = req.headers["authorization"]
|
const authHeader = req.headers["authorization"]
|
||||||
const token = authHeader && authHeader.split(" ")[1]
|
const token = authHeader && authHeader.split(" ")[1]
|
||||||
if (token == null) return res.sendStatus(401)
|
if (!token) return res.sendStatus(401)
|
||||||
|
console.log(config)
|
||||||
if (!config.enable_admin) return res.sendStatus(404)
|
if (!config.enable_admin) return res.sendStatus(404)
|
||||||
|
|
||||||
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, {
|
||||||
|
|
|
@ -357,4 +357,3 @@ posts.delete("/:id", jwt, async (req: UserJwtRequest, res, next) => {
|
||||||
next(e)
|
next(e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue