More authentication

This commit is contained in:
Gender Shrapnel 2019-10-07 23:31:51 +02:00
parent 78c35b2ac3
commit 409b8af6a3
17 changed files with 214 additions and 34 deletions

1
sql/users/details.sql Normal file
View File

@ -0,0 +1 @@
SELECT email FROM users WHERE id=$1;

View File

@ -14,7 +14,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import { db } from "@kredens/db"; 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 { Kind } from "graphql/language";
import { GraphQLScalarType, GraphQLScalarTypeConfig } from "graphql/type"; import { GraphQLScalarType, GraphQLScalarTypeConfig } from "graphql/type";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
@ -67,6 +67,17 @@ const resolvers = {
export function server() { export function server() {
return new ApolloServer({ return new ApolloServer({
context: async req => {
const user = req.req.user;
if (!user) {
throw new AuthenticationError("you must be logged in");
}
return {
user
};
},
resolvers, resolvers,
typeDefs typeDefs
}); });

40
src/auth.ts Normal file
View File

@ -0,0 +1,40 @@
// Copyright (C) 2019 ModZero <modzero@modzero.xyz>
//
// 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 <http://www.gnu.org/licenses/>.
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<User>();
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();
};

24
src/custom.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
// Copyright (C) 2019 ModZero <modzero@modzero.xyz>
//
// 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 <http://www.gnu.org/licenses/>.
import { User } from "@kredens/db/models";
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}

View File

@ -18,7 +18,7 @@ import {
MigrationRepository, MigrationRepository,
UserRepository UserRepository
} from "@kredens/db/repos"; } from "@kredens/db/repos";
import monitor from "pg-monitor";
import pgPromise, { IDatabase, IInitOptions } from "pg-promise"; import pgPromise, { IDatabase, IInitOptions } from "pg-promise";
type ExtendedProtocol = IDatabase<Extensions> & Extensions; type ExtendedProtocol = IDatabase<Extensions> & Extensions;
@ -31,6 +31,11 @@ const initOptions: IInitOptions<Extensions> = {
}; };
const pgp: pgPromise.IMain = pgPromise(initOptions); 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); const db: ExtendedProtocol = pgp(process.env.PG_CONNECTION_STRING);
export { db, pgp }; export { db, pgp };

View File

@ -20,3 +20,8 @@ export interface Migration {
name: string; name: string;
applied_at: DateTime; applied_at: DateTime;
} }
export interface User {
id: number;
email: string;
}

View File

@ -17,6 +17,7 @@ import { users as sql } from "@kredens/db/sql";
import argon2 from "argon2"; import argon2 from "argon2";
import { Maybe, None, Some } from "monet"; import { Maybe, None, Some } from "monet";
import { IDatabase, IMain } from "pg-promise"; import { IDatabase, IMain } from "pg-promise";
import { User } from "@kredens/db/models";
export class UserRepository { export class UserRepository {
private db: IDatabase<any>; private db: IDatabase<any>;
@ -47,4 +48,12 @@ export class UserRepository {
.one(sql.create, [email, encryptedPassword]) .one(sql.create, [email, encryptedPassword])
.then((user: { id: number }) => +user.id); .then((user: { id: number }) => +user.id);
} }
public async details(id: number): Promise<Maybe<User>> {
return this.db
.oneOrNone(sql.details, [id])
.then((user: { email: string }) =>
user !== null ? Some({ ...user, id }) : None()
);
}
} }

View File

@ -37,6 +37,7 @@ const migrations = {
const users = { const users = {
create: sql("users/create.sql"), create: sql("users/create.sql"),
details: sql("users/details.sql"),
login: sql("users/login.sql") login: sql("users/login.sql")
}; };

View File

@ -1,3 +1,18 @@
// Copyright (C) 2019 ModZero <modzero@modzero.xyz>
//
// 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 <http://www.gnu.org/licenses/>.
import Vue from "vue"; import Vue from "vue";
import App from "./App.vue"; import App from "./App.vue";

View File

@ -1,3 +1,18 @@
// Copyright (C) 2019 ModZero <modzero@modzero.xyz>
//
// 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 <http://www.gnu.org/licenses/>.
declare module "*.vue" { declare module "*.vue" {
import Vue from "vue"; import Vue from "vue";
export default Vue; export default Vue;

View File

@ -14,6 +14,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import { server as graphqlServer } from "@kredens/api"; import { server as graphqlServer } from "@kredens/api";
import { authMiddleware } from "@kredens/auth";
import { db } from "@kredens/db"; import { db } from "@kredens/db";
import logger from "@kredens/logger"; import logger from "@kredens/logger";
import indexRouter from "@kredens/routes/"; import indexRouter from "@kredens/routes/";
@ -25,18 +26,33 @@ import pinoExpress from "express-pino-logger";
import session, { SessionOptions } from "express-session"; import session, { SessionOptions } from "express-session";
import helmet from "helmet"; import helmet from "helmet";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
async function main() { async function main() {
await db.tx(async t => { await db.tx(async t => {
await t.migrations.create(); await t.migrations.create();
await t.migrations.apply(); await t.migrations.apply();
}); });
const server = graphqlServer();
const app = express(); const app = express();
const expressPino = pinoExpress({ logger }); const expressPino = pinoExpress({ logger });
app.use(helmet()); app.use(helmet());
app.use(expressPino); 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.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
app.use(cookieParser()); app.use(cookieParser());
@ -52,25 +68,26 @@ async function main() {
} }
app.use(session(sessionOptions)); app.use(session(sessionOptions));
app.use("/bootstrap", bootstrapRouter); app.use("/bootstrap", bootstrapRouter);
app.use(authMiddleware());
const apiServer = graphqlServer();
apiServer.applyMiddleware({
app,
path: "/graphql"
});
app.use(csrf()); 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.set("view engine", "pug");
app.use(async (req, res, next) => {
res.locals.csrfToken = req.csrfToken();
res.locals.user = req.user;
next();
});
app.use("/", indexRouter); app.use("/", indexRouter);
server.applyMiddleware({ app, path: "/graphql" });
app.use((req, res, next) => { app.use((req, res, next) => {
next(createHttpError(404)); next(createHttpError(404));
@ -79,7 +96,7 @@ async function main() {
const port = 3000; const port = 3000;
app.listen(port, () => app.listen(port, () =>
logger.info("Example app listening", { logger.info("Example app listening", {
uri: `http://localhost:${port}${server.graphqlPath}` uri: `http://localhost:${port}${apiServer.graphqlPath}`
}) })
); );
} }

View File

@ -19,17 +19,16 @@ import express from "express";
const router = express.Router(); const router = express.Router();
router.get("/", async (req, res, next) => { router.get("/", async (req, res, next) => {
res.render("login", { res.render("login");
csrfToken: req.csrfToken()
});
}); });
router.post("/", async (req, res, next) => { router.post("/", async (req, res, next) => {
const userID = await db.users.login(req.body.email, req.body.password); const userID = await db.users.login(req.body.email, req.body.password);
if (userID.isSome()) { if (userID.isSome()) {
res.send(`Hi, ${userID.some()}`); req.session.userID = userID.some();
res.redirect("/");
} else { } else {
res.send(`Go away.`); res.redirect("/auth/");
} }
}); });

View File

@ -1,3 +1,18 @@
// Copyright (C) 2019 ModZero <modzero@modzero.xyz>
//
// 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 <http://www.gnu.org/licenses/>.
import { box, unbox } from "@kredens/crypto"; import { box, unbox } from "@kredens/crypto";
import { db } from "@kredens/db"; import { db } from "@kredens/db";
import express from "express"; import express from "express";

View File

@ -1,3 +1,18 @@
// Copyright (C) 2019 ModZero <modzero@modzero.xyz>
//
// 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 <http://www.gnu.org/licenses/>.
import express from "express"; import express from "express";
import authRouter from "./auth"; import authRouter from "./auth";
import homeRouter from "./home"; import homeRouter from "./home";

View File

@ -11,7 +11,7 @@
"*" "*"
] ]
}, },
"esModuleInterop": true, "esModuleInterop": true
}, },
"include": [ "include": [
"./src/**/*" "./src/**/*"

View File

@ -1,8 +1,15 @@
html extends layout.pug
head
title= title block content
body if user
h1= message p.auth Hi, #{ user.email }
div#body else
p I am a placeholder p.auth
script(src="/assets/main.bundle.js") | Who are you?
|
a(href="/auth") Login you coward.
div#body
p I am a placeholder
block scripts
script(src="/assets/main.bundle.js")

View File

@ -3,3 +3,4 @@ html
title Kredens - #{title} title Kredens - #{title}
body body
block content block content
block scripts