diff --git a/package.json b/package.json index 5cc112e..831c96a 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "typescript": "^3.7.2" }, "dependencies": { + "@types/uuid": "^3.4.6", "compression": "^1.7.4", "connect-session-sequelize": "^6.0.0", "cookie-parser": "^1.4.4", @@ -77,6 +78,7 @@ "socket.io": "^2.2.0", "socket.io-redis": "^5.2.0", "sqlite3": "^4.1.0", + "uuid": "^3.3.3", "winston": "^3.2.1", "winston-daily-rotate-file": "^4.2.1" } diff --git a/src/app.ts b/src/app.ts index c21518a..c5b039f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -80,6 +80,18 @@ class App { if (globals.config.server?.cors) { this.app.use(cors()); } + // handle authentification via bearer in the Authorization header + this.app.use(async (req, res, next) => { + if (!req.session.userId && req.headers.authorization) { + const bearer = req.headers.authorization.split("Bearer ")[1]; + if (bearer) { + const user = await dataaccess.getUserByToken(bearer); + // @ts-ignore + req.session.userId = user.id; + } + } + next(); + }); this.app.use((req, res, next) => { logger.verbose(`${req.method} ${req.url}`); process.send({cmd: "notifyRequest"}); diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index eec84e4..b99fcd4 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -101,6 +101,23 @@ export function resolver(req: any, res: any): any { return new NotLoggedInGqlError(); } }, + async getToken({email, passwordHash}: {email: string, passwordHash: string}) { + if (email && passwordHash) { + try { + const user = await dataaccess.getUserByLogin(email, passwordHash); + return { + expires: Number(user.authExpire), + value: user.token(), + }; + } catch (err) { + res.status(400); + return err.graphqlError; + } + } else { + res.status(400); + return new GraphQLError("No email or password specified."); + } + }, async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) { if (username && email && passwordHash) { if (!is.email(email)) { diff --git a/src/graphql/schema.graphql b/src/graphql/schema.graphql index e061f49..6bf3408 100644 --- a/src/graphql/schema.graphql +++ b/src/graphql/schema.graphql @@ -25,6 +25,9 @@ type Query { "returns the post filtered by the sort type with pagination." getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post] + + "Returns an access token for the user that can be used in requests. To user the token in requests, it has to be set in the HTTP header 'Authorization' with the format Bearer ." + getToken(email: String!, passwordHash: String!): Token! } type Mutation { @@ -32,7 +35,7 @@ type Mutation { acceptCookies: Boolean "Login of the user. The passwordHash should be a sha512 hash of the password." - login(email: String, passwordHash: String): Profile + login(email: String!, passwordHash: String!): Profile "Registers the user." register(username: String, email: String, passwordHash: String): Profile @@ -374,6 +377,12 @@ type Event { participants(first: Int=10, offset: Int=0): [User!]! } +"respresents an access token entry with the value as the acutal token and expires as the date the token expires." +type Token { + value: String! + expires: String! +} + "represents the type of vote performed on a post" enum VoteType { UPVOTE diff --git a/src/lib/dataAccess.ts b/src/lib/dataAccess.ts index 1ffcd42..4c67ea8 100644 --- a/src/lib/dataAccess.ts +++ b/src/lib/dataAccess.ts @@ -93,6 +93,14 @@ namespace dataaccess { } } + /** + * Returns the user by auth token. + * @param token + */ + export async function getUserByToken(token: string): Promise { + return models.User.findOne({where: {authToken: token}}); + } + /** * Registers a user with a username and password returning a user * @param username diff --git a/src/lib/models/Post.ts b/src/lib/models/Post.ts index b7412a5..0feade3 100644 --- a/src/lib/models/Post.ts +++ b/src/lib/models/Post.ts @@ -24,22 +24,37 @@ export class Post extends Model { @CreatedAt public readonly createdAt!: Date; + /** + * Returns the author of a post + */ public async author(): Promise { return await this.$get("rAuthor") as User; } + /** + * Returns the votes on a post + */ public async votes(): Promise> { return await this.$get("rVotes") as Array; } + /** + * Returns the markdown-rendered html content of the post + */ public get htmlContent() { return markdown.render(this.getDataValue("content")); } + /** + * Returns the number of upvotes on the post + */ public async upvotes() { return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.UPVOTE).length; } + /** + * Returns the number of downvotes on the post + */ public async downvotes() { return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.DOWNVOTE).length; } diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts index 9b1026a..0ebb547 100644 --- a/src/lib/models/User.ts +++ b/src/lib/models/User.ts @@ -10,6 +10,7 @@ import { Unique, UpdatedAt, } from "sequelize-typescript"; +import * as uuidv4 from "uuid/v4"; import {RequestNotFoundError} from "../errors/RequestNotFoundError"; import {UserNotFoundError} from "../errors/UserNotFoundError"; import {ChatMember} from "./ChatMember"; @@ -53,6 +54,13 @@ export class User extends Model { @Column({defaultValue: {}, allowNull: false, type: sqz.JSON}) public frontendSettings: any; + @Unique + @Column({defaultValue: uuidv4, unique: true}) + public authToken: string; + + @Column({defaultValue: () => Date.now() + 7200000}) + public authExpire: Date; + @BelongsToMany(() => User, () => Friendship, "userId") public rFriends: User[]; @@ -130,6 +138,18 @@ export class User extends Model { return JSON.stringify(this.getDataValue("frontendSettings")); } + /** + * Returns the token for the user that can be used as a bearer in requests + */ + public async token(): Promise { + if (this.getDataValue("authExpire") < new Date(Date.now())) { + this.authToken = null; + this.authExpire = null; + await this.save(); + } + return this.getDataValue("authToken"); + } + /** * All friends of the user * @param first diff --git a/yarn.lock b/yarn.lock index 31ead05..b5a9623 100644 --- a/yarn.lock +++ b/yarn.lock @@ -232,6 +232,13 @@ dependencies: "@types/node" "*" +"@types/uuid@^3.4.6": + version "3.4.6" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.6.tgz#d2c4c48eb85a757bf2927f75f939942d521e3016" + integrity sha512-cCdlC/1kGEZdEglzOieLDYBxHsvEOIg7kp/2FYyVR9Pxakq+Qf/inL3RKQ+PA8gOlI/NnL+fXmQH12nwcGzsHw== + dependencies: + "@types/node" "*" + "@types/validator@*", "@types/validator@^10.11.3": version "10.11.3" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-10.11.3.tgz#945799bef24a953c5bc02011ca8ad79331a3ef25"