diff --git a/sql/users/details.sql b/sql/users/details.sql new file mode 100644 index 0000000..346d01e --- /dev/null +++ b/sql/users/details.sql @@ -0,0 +1 @@ +SELECT email FROM users WHERE id=$1; \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index 14a2508..7ba9809 100644 --- a/src/api.ts +++ b/src/api.ts @@ -14,7 +14,7 @@ // along with this program. If not, see . import { db } from "@kredens/db"; -import { ApolloServer, gql } from "apollo-server-express"; +import { ApolloServer, AuthenticationError, gql } from "apollo-server-express"; import { Kind } from "graphql/language"; import { GraphQLScalarType, GraphQLScalarTypeConfig } from "graphql/type"; import { DateTime } from "luxon"; @@ -67,6 +67,17 @@ const resolvers = { export function server() { return new ApolloServer({ + context: async req => { + const user = req.req.user; + + if (!user) { + throw new AuthenticationError("you must be logged in"); + } + + return { + user + }; + }, resolvers, typeDefs }); diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..a714457 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,40 @@ +// Copyright (C) 2019 ModZero +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { db } from "@kredens/db"; +import { User } from "@kredens/db/models"; +import express from "express"; +import { None } from "monet"; + +export const getUser = async (req: express.Request) => + req.session.userID ? db.users.details(req.session.userID) : None(); + +export const authMiddleware: () => express.Handler = () => async ( + req, + res, + next +) => { + if (req.session.userID) { + const user = await getUser(req); + + if (user.isSome()) { + req.user = user.some(); + } else { + delete req.session.userID; + } + } + + next(); +}; diff --git a/src/custom.d.ts b/src/custom.d.ts new file mode 100644 index 0000000..5f37445 --- /dev/null +++ b/src/custom.d.ts @@ -0,0 +1,24 @@ +// Copyright (C) 2019 ModZero +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { User } from "@kredens/db/models"; + +declare global { + namespace Express { + interface Request { + user?: User; + } + } +} diff --git a/src/db/index.ts b/src/db/index.ts index 0746025..96d0098 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -18,7 +18,7 @@ import { MigrationRepository, UserRepository } from "@kredens/db/repos"; -import monitor from "pg-monitor"; + import pgPromise, { IDatabase, IInitOptions } from "pg-promise"; type ExtendedProtocol = IDatabase & Extensions; @@ -31,6 +31,11 @@ const initOptions: IInitOptions = { }; const pgp: pgPromise.IMain = pgPromise(initOptions); -monitor.attach(initOptions); + +if (process.env.NODE_ENV !== "production") { + // tslint:disable-next-line:no-implicit-dependencies + import("pg-monitor").then(monitor => monitor.attach(initOptions)); +} + const db: ExtendedProtocol = pgp(process.env.PG_CONNECTION_STRING); export { db, pgp }; diff --git a/src/db/models.ts b/src/db/models.ts index d15ff61..4305c16 100644 --- a/src/db/models.ts +++ b/src/db/models.ts @@ -20,3 +20,8 @@ export interface Migration { name: string; applied_at: DateTime; } + +export interface User { + id: number; + email: string; +} diff --git a/src/db/repos/users.ts b/src/db/repos/users.ts index e10db52..0daee17 100644 --- a/src/db/repos/users.ts +++ b/src/db/repos/users.ts @@ -17,6 +17,7 @@ import { users as sql } from "@kredens/db/sql"; import argon2 from "argon2"; import { Maybe, None, Some } from "monet"; import { IDatabase, IMain } from "pg-promise"; +import { User } from "@kredens/db/models"; export class UserRepository { private db: IDatabase; @@ -47,4 +48,12 @@ export class UserRepository { .one(sql.create, [email, encryptedPassword]) .then((user: { id: number }) => +user.id); } + + public async details(id: number): Promise> { + return this.db + .oneOrNone(sql.details, [id]) + .then((user: { email: string }) => + user !== null ? Some({ ...user, id }) : None() + ); + } } diff --git a/src/db/sql/index.ts b/src/db/sql/index.ts index c3ca587..edbb650 100644 --- a/src/db/sql/index.ts +++ b/src/db/sql/index.ts @@ -37,6 +37,7 @@ const migrations = { const users = { create: sql("users/create.sql"), + details: sql("users/details.sql"), login: sql("users/login.sql") }; diff --git a/src/frontend/index.ts b/src/frontend/index.ts index 6e714f4..efd269c 100644 --- a/src/frontend/index.ts +++ b/src/frontend/index.ts @@ -1,3 +1,18 @@ +// Copyright (C) 2019 ModZero +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + import Vue from "vue"; import App from "./App.vue"; diff --git a/src/frontend/vue-shim.d.ts b/src/frontend/vue-shim.d.ts index 0660bd6..cdcc639 100644 --- a/src/frontend/vue-shim.d.ts +++ b/src/frontend/vue-shim.d.ts @@ -1,3 +1,18 @@ +// Copyright (C) 2019 ModZero +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + declare module "*.vue" { import Vue from "vue"; export default Vue; diff --git a/src/index.ts b/src/index.ts index 1afa74e..78a97c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ // along with this program. If not, see . import { server as graphqlServer } from "@kredens/api"; +import { authMiddleware } from "@kredens/auth"; import { db } from "@kredens/db"; import logger from "@kredens/logger"; import indexRouter from "@kredens/routes/"; @@ -25,18 +26,33 @@ import pinoExpress from "express-pino-logger"; import session, { SessionOptions } from "express-session"; import helmet from "helmet"; import createHttpError from "http-errors"; - async function main() { await db.tx(async t => { await t.migrations.create(); await t.migrations.apply(); }); - const server = graphqlServer(); const app = express(); const expressPino = pinoExpress({ logger }); app.use(helmet()); app.use(expressPino); + + if (app.settings.env === "development") { + const webpack = await import("webpack").then(p => p.default); // tslint:disable-line:no-implicit-dependencies + // tslint:disable-next-line:no-implicit-dependencies + const webpackDevMiddleware = await import("webpack-dev-middleware").then( + p => p.default + ); + const config = await import("../webpack.config").then(p => p.default); + + const compiler = webpack(config); + app.use( + webpackDevMiddleware(compiler, { + publicPath: "/assets/" + }) + ); + } + app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); @@ -52,25 +68,26 @@ async function main() { } app.use(session(sessionOptions)); app.use("/bootstrap", bootstrapRouter); + + app.use(authMiddleware()); + const apiServer = graphqlServer(); + apiServer.applyMiddleware({ + app, + path: "/graphql" + }); + app.use(csrf()); - if (app.settings.env === "development") { - const webpack = require("webpack"); // tslint:disable-line:no-implicit-dependencies - const webpackDevMiddleware = require("webpack-dev-middleware"); // tslint:disable-line:no-implicit-dependencies - const config = require("../webpack.config").default; - - const compiler = webpack(config); - app.use( - webpackDevMiddleware(compiler, { - publicPath: "/assets/" - }) - ); - } - app.set("view engine", "pug"); + app.use(async (req, res, next) => { + res.locals.csrfToken = req.csrfToken(); + res.locals.user = req.user; + + next(); + }); + app.use("/", indexRouter); - server.applyMiddleware({ app, path: "/graphql" }); app.use((req, res, next) => { next(createHttpError(404)); @@ -79,7 +96,7 @@ async function main() { const port = 3000; app.listen(port, () => logger.info("Example app listening", { - uri: `http://localhost:${port}${server.graphqlPath}` + uri: `http://localhost:${port}${apiServer.graphqlPath}` }) ); } diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 11f4529..3495419 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -19,17 +19,16 @@ import express from "express"; const router = express.Router(); router.get("/", async (req, res, next) => { - res.render("login", { - csrfToken: req.csrfToken() - }); + res.render("login"); }); router.post("/", async (req, res, next) => { const userID = await db.users.login(req.body.email, req.body.password); if (userID.isSome()) { - res.send(`Hi, ${userID.some()}`); + req.session.userID = userID.some(); + res.redirect("/"); } else { - res.send(`Go away.`); + res.redirect("/auth/"); } }); diff --git a/src/routes/bootstrap.ts b/src/routes/bootstrap.ts index 6250f1d..96a1b07 100644 --- a/src/routes/bootstrap.ts +++ b/src/routes/bootstrap.ts @@ -1,3 +1,18 @@ +// Copyright (C) 2019 ModZero +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + import { box, unbox } from "@kredens/crypto"; import { db } from "@kredens/db"; import express from "express"; diff --git a/src/routes/index.ts b/src/routes/index.ts index 25e6f8c..e858234 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,3 +1,18 @@ +// Copyright (C) 2019 ModZero +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + import express from "express"; import authRouter from "./auth"; import homeRouter from "./home"; diff --git a/tsconfig.json b/tsconfig.json index e10e8b3..9e56bba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "*" ] }, - "esModuleInterop": true, + "esModuleInterop": true }, "include": [ "./src/**/*" diff --git a/views/index.pug b/views/index.pug index d695bcc..5a71acb 100644 --- a/views/index.pug +++ b/views/index.pug @@ -1,8 +1,15 @@ -html - head - title= title - body - h1= message - div#body - p I am a placeholder - script(src="/assets/main.bundle.js") \ No newline at end of file +extends layout.pug + +block content + if user + p.auth Hi, #{ user.email } + else + p.auth + | Who are you? + | + a(href="/auth") Login you coward. + div#body + p I am a placeholder + +block scripts + script(src="/assets/main.bundle.js") \ No newline at end of file diff --git a/views/layout.pug b/views/layout.pug index befcb35..407669f 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -3,3 +3,4 @@ html title Kredens - #{title} body block content + block scripts \ No newline at end of file