diff --git a/.gitignore b/.gitignore index 982d25a..9f38007 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dist config.yaml sqz-force greenvironment.db +logs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3edaa34..1aa8a45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,3 +15,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - session management - Sequelize models and integration - Sequelize-typescript integration +- error pages +- pagination for most list types diff --git a/package-lock.json b/package-lock.json index 6ccf67c..bf178c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2387,6 +2387,14 @@ "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" }, + "file-stream-rotator": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.5.5.tgz", + "integrity": "sha512-XzvE1ogpxUbARtZPZLICaDRAeWxoQLFMKS3ZwADoCQmurKEwuDD2jEfDVPm/R1HeKYsRYEl9PzVIezjQ3VTTPQ==", + "requires": { + "moment": "^2.11.2" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -2651,7 +2659,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2672,12 +2681,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2692,17 +2703,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2819,7 +2833,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2831,6 +2846,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2845,6 +2861,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2852,12 +2869,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2876,6 +2895,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2956,7 +2976,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2968,6 +2989,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3053,7 +3075,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3089,6 +3112,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3108,6 +3132,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3151,12 +3176,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -5042,6 +5069,11 @@ } } }, + "object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==" + }, "object-is": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", @@ -7685,6 +7717,17 @@ "winston-transport": "^4.3.0" } }, + "winston-daily-rotate-file": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.2.1.tgz", + "integrity": "sha512-ETNkdkMsf05HMg0kgkmTkA9GC6u6fFrat4mUVmx9XLCdgBoQL+iLuzbNUTWQxCVhlJ/w7MzsQfkU7bGf49NDbA==", + "requires": { + "file-stream-rotator": "^0.5.5", + "object-hash": "^1.3.0", + "triple-beam": "^1.3.0", + "winston-transport": "^4.2.0" + } + }, "winston-transport": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", diff --git a/package.json b/package.json index 94d36ad..294c6aa 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "sequelize-typescript": "^1.0.0", "socket.io": "^2.2.0", "sqlite3": "^4.1.0", - "winston": "^3.2.1" + "winston": "^3.2.1", + "winston-daily-rotate-file": "^4.2.1" } } diff --git a/src/app.ts b/src/app.ts index 0a565a6..0acacdf 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ import * as compression from "compression"; import * as cookieParser from "cookie-parser"; import * as cors from "cors"; +import {Request, Response} from "express"; import * as express from "express"; import * as graphqlHTTP from "express-graphql"; import * as session from "express-session"; @@ -9,6 +10,7 @@ import * as fsx from "fs-extra"; import {buildSchema} from "graphql"; import {importSchema} from "graphql-import"; import * as http from "http"; +import * as httpStatus from "http-status"; import * as path from "path"; import {Sequelize} from "sequelize-typescript"; import * as socketIo from "socket.io"; @@ -84,6 +86,14 @@ class App { schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))), }; })); + this.app.use((req: any, res: Response) => { + res.status(httpStatus.NOT_FOUND); + res.render("errors/404.pug", {url: req.url}); + }); + this.app.use((err, req: Request, res: Response) => { + res.status(httpStatus.INTERNAL_SERVER_ERROR); + res.render("errors/500.pug"); + }); } /** diff --git a/src/default-config.yaml b/src/default-config.yaml index 0c24242..0254d11 100644 --- a/src/default-config.yaml +++ b/src/default-config.yaml @@ -7,13 +7,16 @@ server: port: 8080 cors: false +# configuration of sessions session: secret: REPLACE WITH SAFE RANDOM GENERATED SECRET cookieMaxAge: 604800000‬ # 7 days +# configuration of the markdown parser markdown: plugins: - 'markdown-it-emoji' +# configuration for logging logging: level: info diff --git a/src/graphql/schema.graphql b/src/graphql/schema.graphql index fc0f156..93ee885 100644 --- a/src/graphql/schema.graphql +++ b/src/graphql/schema.graphql @@ -21,7 +21,7 @@ type Query { findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post] "find a user by user name or handle" - findUser(first: Int, offset: Int, name: String!, handle: String!): [User] + findUser(first: Int, offset: Int, name: String, handle: String): [User] "returns the post filtered by the sort type with pagination." getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post] @@ -62,16 +62,16 @@ type Mutation { sendMessage(chatId: ID!, content: String!): ChatMessage "create the post" - createPost(content: String!): Post + createPost(content: String!): Post! "delete the post for a given post id" - deletePost(postId: ID!): Boolean + deletePost(postId: ID!): Boolean! "Creates a chat between the user (and optional an other user)" - createChat(members: [ID!]): ChatRoom + createChat(members: [ID!]): ChatRoom! "Creates a new group with a given name and additional members" - createGroup(name: String!, members: [ID!]): Group + createGroup(name: String!, members: [ID!]): Group! "Joins a group with the given id" joinGroup(id: ID!): Group @@ -108,23 +108,35 @@ interface UserData { "Id of the User" id: ID! - "the total number of posts the user posted" - numberOfPosts: Int + "DEPRECATED! the total number of posts the user posted" + numberOfPosts: Int! + + "the number of posts the user has created" + postCount: Int! "returns a given number of posts of a user" - posts(first: Int=10, offset: Int): [Post] + posts(first: Int=10, offset: Int=0): [Post] "creation date of the user account" joinedAt: String! "all friends of the user" - friends: [User] + friends(first: Int=10, offset: Int=0): [User] + + "The number of friends the user has" + friendCount: Int! + + "The groups the user has joined" + groups(first: Int=10, offset: Int=0): [Group] + + "The numbef of groups the user has joined" + groupCount: Int! "the points of the user" - points: Int + points: Int! "the levels of the user depending on the points" - level: Int + level: Int! } "represents a single user account" @@ -142,25 +154,34 @@ type User implements UserData{ id: ID! "the total number of posts the user posted" - numberOfPosts: Int + numberOfPosts: Int! "returns a given number of posts of a user" posts(first: Int=10, offset: Int): [Post] + "the number of posts the user has created" + postCount: Int! + "creation date of the user account" joinedAt: String! "all friends of the user" - friends: [User] + friends(first: Int=10, offset: Int=0): [User] + + "The number of friends the user has" + friendCount: Int! "the points of the user" - points: Int + points: Int! "the groups the user has joined" - groups: [Group] + groups(first: Int=10, offset: Int=0): [Group] + + "The numbef of groups the user has joined" + groupCount: Int! "the levels of the user depending on the points" - level: Int + level: Int! } type Profile implements UserData { @@ -176,6 +197,9 @@ type Profile implements UserData { "returns the chatrooms the user joined." chats(first: Int=10, offset: Int): [ChatRoom] + "the count of the users chats" + chatCount: Int! + "unique identifier name from the User" handle: String! @@ -183,37 +207,46 @@ type Profile implements UserData { id: ID! "the total number of posts the user posted" - numberOfPosts: Int + numberOfPosts: Int! + + "the number of posts the user has created" + postCount: Int! "returns a given number of posts of a user" - posts(first: Int=10, offset: Int): [Post] + posts(first: Int=10, offset: Int): [Post!]! "creation date of the user account" joinedAt: String! "all friends of the user" - friends: [User] + friends(first: Int=10, offset: Int=0): [User!]! + + "The number of friends the user has" + friendCount: Int! "all sent request for groupChats/friends/events" - sentRequests: [Request] + sentRequests: [Request!]! "all received request for groupChats/friends/events" - receivedRequests: [Request] + receivedRequests: [Request!]! "all groups the user is an admin of" - administratedGroups: [Group] + administratedGroups: [Group!]! "all groups the user has created" - createdGroups: [Group] + createdGroups: [Group!]! "all groups the user has joined" - groups: [Group] + groups(first: Int=10, offset: Int=0): [Group!]! + + "The numbef of groups the user has joined" + groupCount: Int! "the points of the user" - points: Int + points: Int! "the levels of the user depending on the points" - level: Int + level: Int! } "represents a single user post" @@ -266,7 +299,7 @@ type ChatRoom { namespace: String "the members of the chatroom" - members: [User!] + members(first: Int=10, offset: Int=0): [User!] "return a specfic range of messages posted in the chat" messages(first: Int = 10, offset: Int, containing: String): [ChatMessage]! @@ -306,7 +339,7 @@ type Group { creator: User "all admins of the group" - admins: [User]! + admins(first: Int=10, offset: Int=0): [User]! "the members of the group with pagination" members(first: Int = 10, offset: Int = 0): [User]! @@ -315,7 +348,7 @@ type Group { chat: ChatRoom "the events of the group" - events: [Event!]! + events(first: Int=10, offset: Int=0): [Event!]! } type Event { @@ -332,7 +365,7 @@ type Event { group: Group! "The participants of the event." - participants: [User!]! + participants(first: Int=10, offset: Int=0): [User!]! } "represents the type of vote performed on a post" diff --git a/src/lib/globals.ts b/src/lib/globals.ts index 6fe2899..8e8c30b 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -9,6 +9,7 @@ import {EventEmitter} from "events"; import * as fsx from "fs-extra"; import * as yaml from "js-yaml"; import * as winston from "winston"; +require('winston-daily-rotate-file'); const configPath = "config.yaml"; const defaultConfig = __dirname + "/../default-config.yaml"; @@ -27,6 +28,7 @@ if (!(fsx.pathExistsSync(configPath))) { */ namespace globals { export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8")); + // @ts-ignore export const logger = winston.createLogger({ transports: [ new winston.transports.Console({ @@ -39,6 +41,20 @@ namespace globals { ), level: config.logging.level, }), + // @ts-ignore + new (winston.transports.DailyRotateFile)({ + dirname: "logs", + filename: "gv-%DATE%.log", + format: winston.format.combine( + winston.format.timestamp(), + winston.format.printf(({level, message, timestamp}) => { + return `${timestamp} ${level}: ${message}`; + }), + ), + json: false, + maxFiles: "7d", + zippedArchive: true, + }), ], }); export const internalEmitter = new EventEmitter(); diff --git a/src/lib/models/Event.ts b/src/lib/models/Event.ts index 49aede3..47f5106 100644 --- a/src/lib/models/Event.ts +++ b/src/lib/models/Event.ts @@ -29,6 +29,8 @@ export class Event extends Model { } public async participants({first, offset}: {first: number, offset: number}): Promise { - return await this.$get("rParticipants") as User[]; + const limit = first || 10; + offset = offset || 0; + return await this.$get("rParticipants", {limit, offset}) as User[]; } } diff --git a/src/lib/models/Group.ts b/src/lib/models/Group.ts index c07544f..52def92 100644 --- a/src/lib/models/Group.ts +++ b/src/lib/models/Group.ts @@ -40,8 +40,10 @@ export class Group extends Model { return await this.$get("rCreator") as User; } - public async admins(): Promise { - return await this.$get("rAdmins") as User[]; + public async admins({first, offset}: { first: number, offset: number }): Promise { + const limit = first || 10; + offset = offset || 0; + return await this.$get("rAdmins", {limit, offset}) as User[]; } public async members({first, offset}: { first: number, offset: number }): Promise { @@ -54,7 +56,9 @@ export class Group extends Model { return await this.$get("rChat") as ChatRoom; } - public async events(): Promise { - return await this.$get("rEvents") as Event[]; + public async events({first, offset}: { first: number, offset: number }): Promise { + const limit = first || 10; + offset = offset || 0; + return await this.$get("rEvents", {limit, offset}) as Event[]; } } diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts index 3150803..a63b41c 100644 --- a/src/lib/models/User.ts +++ b/src/lib/models/User.ts @@ -92,62 +92,156 @@ export class User extends Model { @UpdatedAt public readonly updatedAt!: Date; + /** + * The name of the user + */ public get name(): string { return this.getDataValue("username"); } + /** + * The date the user joined the network + */ public get joinedAt(): Date { return this.getDataValue("createdAt"); } + /** + * The points of the user + */ public get points(): number { return this.rankpoints; } + /** + * The level of the user which is the points divided by 100 + */ public get level(): number { return Math.ceil(this.rankpoints / 100); } - public async friends(): Promise { - return await this.$get("rFriendOf") as User[]; + /** + * All friends of the user + * @param first + * @param offset + */ + public async friends({first, offset}: {first: number, offset: number}): Promise { + const limit = first || 10; + offset = offset || 0; + return await this.$get("rFriendOf", {limit, offset}) as User[]; } - public async chats(): Promise { - return await this.$get("rChats") as ChatRoom[]; + /** + * The total number of the users friends. + */ + public async friendCount(): Promise { + return this.$count("rFriends"); } + /** + * The chats the user has joined + * @param first + * @param offset + */ + public async chats({first, offset}: {first: number, offset: number}): Promise { + const limit = first || 10; + offset = offset || 0; + return await this.$get("rChats", {limit, offset}) as ChatRoom[]; + } + + /** + * the number of chats the user has + */ + public async chatCount(): Promise { + return this.$count("rChats"); + } + + /** + * All active requests the user ha ssent + */ public async sentRequests(): Promise { return await this.$get("rSentRequests") as Request[]; } + /** + * All requests the user has received + */ public async receivedRequests(): Promise { return await this.$get("rReceivedRequests") as Request[]; } public async posts({first, offset}: {first: number, offset: number}): Promise { - return await this.$get("rPosts", {limit: first, offset}) as Post[]; + const limit = first || 10; + offset = offset || 0; + return await this.$get("rPosts", {limit, offset}) as Post[]; } + /** + * @deprecated + * use {@link postCount} instead + */ public async numberOfPosts(): Promise { + return this.postCount(); + } + + /** + * number of posts the user created + */ + public async postCount(): Promise { return this.$count("rPosts"); } + /** + * Groups the user is the admin of + */ public async administratedGroups(): Promise { return await this.$get("rAdministratedGroups") as Group[]; } + /** + * Groups the user has created + */ public async createdGroups(): Promise { return await this.$get("rCreatedGroups") as Group[]; } - public async groups(): Promise { - return await this.$get("rGroups") as Group[]; + /** + * Groups the user is a member of + * @param first + * @param offset + */ + public async groups({first, offset}: {first: number, offset: number}): Promise { + const limit = first || 10; + offset = offset || 0; + return await this.$get("rGroups", {limit, offset}) as Group[]; + } + + /** + * The number of groups the user has joined + */ + public async groupCount(): Promise { + return this.$count("rGroups"); } + /** + * Events the user has joined + */ public async events(): Promise { return await this.$get("rEvents") as Event[]; } + /** + * The number of events the user is participating in. + */ + public async eventCount(): Promise { + return this.$count("rEvents"); + } + + /** + * Denys a request the user has received + * @param sender + * @param type + */ public async denyRequest(sender: number, type: RequestType) { const request = await this.$get("rReceivedRequests", {where: {senderId: sender, requestType: type}}) as Request[]; @@ -156,6 +250,11 @@ export class User extends Model { } } + /** + * Accepts a request the user has received + * @param sender + * @param type + */ public async acceptRequest(sender: number, type: RequestType) { const requests = await this.$get("rReceivedRequests", {where: {senderId: sender, requestType: type}}) as Request[]; @@ -173,6 +272,10 @@ export class User extends Model { } } + /** + * Removes a user from the users friends + * @param friendId + */ public async removeFriend(friendId: number) { const friend = await User.findByPk(friendId); if (friend) { diff --git a/src/public/stylesheets/sass/mixins.sass b/src/public/stylesheets/sass/mixins.sass deleted file mode 100644 index 14bca84..0000000 --- a/src/public/stylesheets/sass/mixins.sass +++ /dev/null @@ -1,5 +0,0 @@ -@mixin gridPosition($rowStart, $rowEnd, $columnStart, $columnEnd) - grid-row-start: $rowStart - grid-row-end: $rowEnd - grid-column-start: $columnStart - grid-column-end: $columnEnd diff --git a/src/public/stylesheets/sass/style.sass b/src/public/stylesheets/sass/style.sass index 5474776..c0308d3 100644 --- a/src/public/stylesheets/sass/style.sass +++ b/src/public/stylesheets/sass/style.sass @@ -1,108 +1,10 @@ -@import "vars" -@import "mixins" - body font-family: Arial, serif -button - border: 2px solid $cPrimary - margin-top: 0.125em - padding: 0.125em - background-color: $cPrimary - color: $cPrimarySurface - font-weight: bold - transition-duration: 0.25s - -button:hover - background-color: lighten($cPrimary, 10%) - cursor: pointer - -button:active - background-color: darken($cPrimary, 5%) - box-shadow: inset 0.25em 0.25em 0.1em rgba(0, 0, 0, 0.25) - -.stylebar - @include gridPosition(1, 2, 1, 4) - display: grid - grid-template: 100% /25% 50% 25% - background-color: $cPrimary - color: $cPrimarySurface - - h1 - @include gridPosition(1, 2, 1, 2) +#server-error + * + margin-left: auto + margin-right: auto text-align: center - margin: auto - -#content - grid-template: 7.5% 92.5% / 25% 50% 25% - display: grid - width: 100% - height: 100% - -#friendscontainer - @include gridPosition(2, 3, 1, 2) - background-color: $cPrimaryBackground - -#input-login - margin-top: 1em - @include gridPosition(2,3,2,3) - grid-template: 7.5% 7.5% 7.5% 7.5% 72%/ 100% - display: grid - background-color: $cPrimaryBackground - - input - margin: 0.25em - - .loginButton - margin: 0.25em - -#input-register - margin-top: 1em - @include gridPosition(2,3,2,3) - grid-template: 7.5% 7.5% 7.5% 7.5% 7.5% 7.5% 58%/ 100% - display: grid - background-color: $cPrimaryBackground - - input - margin: 0.25em - - .registerButton - margin: 0.25em - -#feedcontainer - @include gridPosition(2, 3, 2, 3) - background-color: $cSecondaryBackground - .postinput - margin: 0.5em - input - width: 100% - border-radius: 0.25em - border: 1px solid $cPrimary - padding: 0.125em - height: 2em - button.submitbutton - border-radius: 0.25em - height: 2em - - .feeditem - background-color: $cPrimaryBackground - min-height: 2em - margin: 0.5em - padding: 0.25em - border-radius: 0.25em - .itemhead - align-items: flex-start - - .title, .handle, .date - margin: 0.125em - .title - font-weight: bold - - .handle, .date - color: $cInactiveText - .handle a - text-decoration: none - color: $cInactiveText - font-style: normal - .handle a:hover - text-decoration: underline + code + font-size: 2em diff --git a/src/public/stylesheets/sass/vars.sass b/src/public/stylesheets/sass/vars.sass deleted file mode 100644 index 871a831..0000000 --- a/src/public/stylesheets/sass/vars.sass +++ /dev/null @@ -1,5 +0,0 @@ -$cPrimaryBackground: #fff -$cSecondaryBackground: #ddd -$cInactiveText: #555 -$cPrimary: #0d6b14 -$cPrimarySurface: #fff diff --git a/src/routes/home.ts b/src/routes/home.ts index 102b88f..88a15bb 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -1,9 +1,9 @@ import {Router} from "express"; import {Namespace, Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; -import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/models"; import globals from "../lib/globals"; import {InternalEvents} from "../lib/InternalEvents"; +import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/models"; import Route from "../lib/Route"; /** diff --git a/src/views/errors/404.pug b/src/views/errors/404.pug new file mode 100644 index 0000000..af1fc68 --- /dev/null +++ b/src/views/errors/404.pug @@ -0,0 +1,12 @@ +html + head + link(href="/stylesheets/style.css" rel="stylesheet" type="text/css") + body + div#server-error + div + h1 Page not found! + div + code 404 + div + h1 The page "#{url}" was not found. + diff --git a/src/views/errors/500.pug b/src/views/errors/500.pug new file mode 100644 index 0000000..586f68b --- /dev/null +++ b/src/views/errors/500.pug @@ -0,0 +1,13 @@ +html + head + link(href="/stylesheets/style.css" rel="stylesheet" type="text/css") + body + div#server-error + div + h1 Internal server error! + div + code 500 + div + h2 Oops the server couldn't handle that. + div + p You might want to report this.