diff --git a/compose.yaml b/compose.yaml index 68d1912..ecec3f3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -27,6 +27,7 @@ services: MONGODB_URI: "mongodb://mongodb:27017/bot" MONGODB_USER: "root" MONGODB_PASS: "password" + JWTSECRET: "cooljwtsecret" volumes: - ./:/usr/src/app:ro diff --git a/mongo-init.js b/mongo-init.js index a7b191b..67b53c9 100644 --- a/mongo-init.js +++ b/mongo-init.js @@ -1,24 +1,30 @@ db.createUser({ - user: 'root', - pwd: 'password', - roles: [ - { - role: 'readWrite', - db: 'admin', - }, - { - role: 'readWrite', - db: 'bot', - }, - ], + user: "root", + pwd: "password", + roles: [ + { + role: "readWrite", + db: "admin", + }, + { + role: "readWrite", + db: "bot", + }, + ], }); db = new Mongo().getDB("bot"); -db.images.createIndex({ "status": 1 }); -db.images.createIndex({ "url": 1 }, { "unique": true }); +db.images.createIndex({ status: 1 }); +db.images.createIndex({ url: 1 }, { unique: true }); db.images.insert({ - url: "https://example.com", - status: "consumed", - tags: ["2girls", "sleeping"] -}); \ No newline at end of file + url: "https://example.com", + status: "consumed", + tags: ["2girls", "sleeping"], +}); + +db.authorizations.createIndex({ app: 1 }); +db.authorizations.insert({ + app: "tester", + secret: "test", +}); diff --git a/package.json b/package.json index 299b1a8..7a8021d 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,11 @@ "dependencies": { "@types/express": "^4.17.21", "@types/express-list-endpoints": "^6.0.3", + "@types/jsonwebtoken": "^9.0.5", "@types/mongoose": "^5.11.97", "express": "^4.18.2", "express-list-endpoints": "^6.0.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.3" } } \ No newline at end of file diff --git a/src/controllers/AuthControler.ts b/src/controllers/AuthControler.ts new file mode 100644 index 0000000..a524f56 --- /dev/null +++ b/src/controllers/AuthControler.ts @@ -0,0 +1,49 @@ +import jwt from "jsonwebtoken"; +import AuthService from "../services/AuthService"; +import { Request, Response, NextFunction } from "express"; + +const authTokenSecret = process.env.JWTSECRET || "badsecret"; + +class AuthControler { + async login(req: Request, res: Response) { + // Read app and secret from request body + const { app, secret } = req.body; + + // Filter app from the apps by app and secret + const authenticated = await AuthService.find(app, secret); + + if (authenticated) { + console.log("Authenticated app ", authenticated.app); + // Generate an access token + const accessToken = jwt.sign( + { app: authenticated.app }, + authTokenSecret, + { expiresIn: "1h" } + ); + + res.json({ + token: accessToken, + }); + } else { + res.status(403).send("Credentials incorrect"); + } + } + + authorize(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers.authorization; + if (authHeader) { + const token = authHeader.split(" ")[1]; + + jwt.verify(token, authTokenSecret, (err, app) => { + if (err) { + return res.status(403).json("Invalid token provided"); + } + next(); + }); + } else { + res.status(401).json("No Authorization header provided"); + } + } +} + +export default new AuthControler(); diff --git a/src/index.ts b/src/index.ts index 9dff064..f396445 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import express from "express"; import mongoose from "mongoose"; import listEndpoints from "express-list-endpoints"; import imageController from "./controllers/ImageController"; +import authControler from "./controllers/AuthControler"; export const app = express(); @@ -13,7 +14,8 @@ app.get("/", (_, res) => { }); app.get("/images", imageController.getAllImages); -app.post("/images", imageController.addImage); +app.post("/images", authControler.authorize, imageController.addImage); +app.post("/login", authControler.login) // Set the default port to 8080, or use the PORT environment variable diff --git a/src/models/AuthModel.ts b/src/models/AuthModel.ts new file mode 100644 index 0000000..394a64b --- /dev/null +++ b/src/models/AuthModel.ts @@ -0,0 +1,19 @@ +import mongoose, { Document } from "mongoose"; + +export interface Auth extends Document { + app: String, + secret: String +} + +const AuthSchema = new mongoose.Schema({ + app: { + type: String, + required: true, + }, + secret: { + type: String, + required: true, + }, +}); + +export default mongoose.model("authorizations", AuthSchema); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts new file mode 100644 index 0000000..aed8d12 --- /dev/null +++ b/src/services/AuthService.ts @@ -0,0 +1,10 @@ +import AuthModel, { Auth } from "../models/AuthModel"; + +class AuthService { + async find(app: String, secret: String): Promise { + const auth = await AuthModel.findOne({ app: app, secret: secret }).exec(); + return auth; + } +} + +export default new AuthService(); diff --git a/tests/app.test.ts b/tests/app.test.ts index 455b873..3d249b9 100644 --- a/tests/app.test.ts +++ b/tests/app.test.ts @@ -1,88 +1,135 @@ -import { afterEach, describe, expect, it, mock } from "bun:test"; +import { afterEach, beforeAll, describe, expect, it, mock } from "bun:test"; import request from "supertest"; import { app } from "../src"; import imageService from "../src/services/ImageService"; const imageServiceOriginal = imageService; +const tok = await request(app) + .post("/login") + .send({ app: "tester", secret: "test" }); +const token = tok.body.token; afterEach(() => { - mock.restore(); - mock.module("../src/services/ImageService", () => ({ default: imageServiceOriginal })); -}) + mock.restore(); + mock.module("../src/services/ImageService", () => ({ + default: imageServiceOriginal, + })); +}); describe("GET / shows all of the endpoints", async () => { - const res = await request(app).get("/"); + const res = await request(app).get("/"); - it("should be", async () => { - expect(res.body).toHaveProperty("endpoints"); - }); + it("should be", async () => { + expect(res.body).toHaveProperty("endpoints"); + }); - it("should be an array", () => { - expect(Array.isArray(res.body.endpoints)).toBeTrue(); - }) -}) + it("should be an array", () => { + expect(Array.isArray(res.body.endpoints)).toBeTrue(); + }); +}); describe("GET /images works properly", async () => { - const res = await request(app).get("/images"); + const res = await request(app).get("/images"); - it("should be an array", () => { - expect(Array.isArray(res.body.images)).toBeTrue(); - }); + it("should be an array", () => { + expect(Array.isArray(res.body.images)).toBeTrue(); + }); - it("should return a 200", async () => { - expect(res.statusCode).toBe(200); - }); + it("should return a 200", async () => { + expect(res.statusCode).toBe(200); + }); }); describe("POST /images works properly", () => { - it("should return 201 for new image", async () => { - const res = await request(app).post("/images").send({ - url: "https://test.url.com/1", + + it("should return 401 for unauthenticated requests", async () => { + const res = await request(app) + .post("/images") + .send({ + url: "https://test.url.com/0", status: "available", - tags: ["2girls", "touhou"] - }); - expect(res.status).toSatisfy(status => [201].includes(status)); - }); + tags: ["2girls", "touhou"], + }); + expect(res.status).toBe(401); + }); - it("should return 409 for a repeated images", async () => { - await request(app).post("/images").send({ - url: "https://test.url.com/2", + it("should return 403 for invalid tokens", async () => { + const res = await request(app) + .post("/images") + .set("authorization", `Bearer token`) + .send({ + url: "https://test.url.com/0", status: "available", - tags: ["2girls", "touhou"] - }); + tags: ["2girls", "touhou"], + }); + expect(res.status).toBe(403); + }); - const res = await request(app).post("/images").send({ - url: "https://test.url.com/2", - status: "available", - tags: ["2girls", "touhou"] - }); + it("should return 201 for new image", async () => { + const res = await request(app) + .post("/images") + .set("authorization", `Bearer ${token}`) + .send({ + url: "https://test.url.com/1", + status: "available", + tags: ["2girls", "touhou"], + }); + expect(res.status).toBe(201); + }); - expect(res.status).toBe(409); - }); + it("should return 409 for a repeated images", async () => { + await request(app) + .post("/images") + .set("authorization", `Bearer ${token}`) + .send({ + url: "https://test.url.com/2", + status: "available", + tags: ["2girls", "touhou"], + }); - it("should return 500 for an error on the service", async () => { - mock.module("../src/services/ImageService", () => ({ - default: { - add: () => { throw new Error("This is an expected testing error"); } - } - })); + const res = await request(app) + .post("/images") + .set("authorization", `Bearer ${token}`) + .send({ + url: "https://test.url.com/2", + status: "available", + tags: ["2girls", "touhou"], + }); - const res = await request(app).post("/images").send({ - url: "https://test.url.com/3", - status: "available", - tags: ["2girls", "touhou"] - }); - - expect(res.status).toBe(500); - }); + expect(res.status).toBe(409); + }); - it("should return 400 for malformed requests", async () => { - mock.restore(); - const res = await request(app).post("/images").send({ - url: "https://test.url.com/4", - status: "wrong", - tags: ["2girls", "touhou"] - }); - expect(res.status).toBe(400); - }); -}); \ No newline at end of file + it("should return 500 for an error on the service", async () => { + mock.module("../src/services/ImageService", () => ({ + default: { + add: () => { + throw new Error("This is an expected testing error"); + }, + }, + })); + + const res = await request(app) + .post("/images") + .set("authorization", `Bearer ${token}`) + .send({ + url: "https://test.url.com/3", + status: "available", + tags: ["2girls", "touhou"], + }); + + expect(res.status).toBe(500); + }); + + it("should return 400 for malformed requests", async () => { + mock.restore(); + const res = await request(app) + .post("/images") + .set("authorization", `Bearer ${token}`) + .send({ + url: "https://test.url.com/4", + status: "wrong", + tags: ["2girls", "touhou"], + }); + expect(res.status).toBe(400); + }); +}); diff --git a/tests/auth.test.ts b/tests/auth.test.ts new file mode 100644 index 0000000..6951875 --- /dev/null +++ b/tests/auth.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it, mock } from "bun:test"; +import request from "supertest"; +import { app } from "../src"; + +describe("/login", async () => { + const correctRespose = await request(app).post("/login").send({ + app: "tester", + secret: "test", + }); + it("should return 200 for correct login", () => { + expect(correctRespose.status).toBe(200); + }); + it("should contain a token", () => { + expect(correctRespose.body).toHaveProperty("token"); + }); + + it("should return 403 for invalid credentials", async () => { + const res = await request(app).post("/login").send({}); + expect(res.status).toBe(403); + }); +}); +