More authentication
This commit is contained in:
		
							
								
								
									
										1
									
								
								sql/users/details.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								sql/users/details.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | SELECT email FROM users WHERE id=$1;  | ||||||
							
								
								
									
										13
									
								
								src/api.ts
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/api.ts
									
									
									
									
									
								
							| @@ -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
									
								
							
							
						
						
									
										40
									
								
								src/auth.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										24
									
								
								src/custom.d.ts
									
									
									
									
										vendored
									
									
										Normal 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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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 }; | ||||||
|   | |||||||
| @@ -20,3 +20,8 @@ export interface Migration { | |||||||
|   name: string; |   name: string; | ||||||
|   applied_at: DateTime; |   applied_at: DateTime; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface User { | ||||||
|  |   id: number; | ||||||
|  |   email: string; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -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() | ||||||
|  |       ); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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") | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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"; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								src/frontend/vue-shim.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								src/frontend/vue-shim.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -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; | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								src/index.ts
									
									
									
									
									
								
							| @@ -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}` | ||||||
|     }) |     }) | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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/"); | ||||||
|   } |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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"; | ||||||
|   | |||||||
| @@ -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"; | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
|         "*" |         "*" | ||||||
|       ] |       ] | ||||||
|     }, |     }, | ||||||
|     "esModuleInterop": true, |     "esModuleInterop": true | ||||||
|   }, |   }, | ||||||
|   "include": [ |   "include": [ | ||||||
|     "./src/**/*" |     "./src/**/*" | ||||||
|   | |||||||
| @@ -1,8 +1,15 @@ | |||||||
| html | extends layout.pug | ||||||
|   head |  | ||||||
|     title= title | block content | ||||||
|   body |   if user | ||||||
|     h1= message |     p.auth Hi, #{ user.email } | ||||||
|  |   else | ||||||
|  |     p.auth  | ||||||
|  |       | Who are you? | ||||||
|  |       | | ||||||
|  |       a(href="/auth") Login you coward. | ||||||
|   div#body |   div#body | ||||||
|     p I am a placeholder |     p I am a placeholder | ||||||
|  |  | ||||||
|  | block scripts | ||||||
|   script(src="/assets/main.bundle.js") |   script(src="/assets/main.bundle.js") | ||||||
| @@ -3,3 +3,4 @@ html | |||||||
|     title Kredens - #{title} |     title Kredens - #{title} | ||||||
|   body |   body | ||||||
|     block content |     block content | ||||||
|  |     block scripts | ||||||
		Reference in New Issue
	
	Block a user