Skip to content

Commit

Permalink
implemented create task and list tasks in api + app
Browse files Browse the repository at this point in the history
  • Loading branch information
vuvincent committed Sep 13, 2023
1 parent f5f4b89 commit 39a6c19
Show file tree
Hide file tree
Showing 55 changed files with 324 additions and 3,406 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CD

on:
push:
branches:
- main

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: 18

- name: Install dependencies
run: npm ci

- name: Run Prisma migrations
run: npx prisma migrate dev

- name: Apply migrations to prod DB
run: npx prisma db push --preview-feature
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: 18

- name: Install dependencies
run: npm ci

- name: Lint code
run: npm run lint

- name: Build
run: npm run build

- name: Test
run: npm run test
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE `Task` DROP FOREIGN KEY `Task_userId_fkey`;

-- RenameIndex
ALTER TABLE `Task` RENAME INDEX `Task_userId_fkey` TO `owner_id_idx`;
3 changes: 1 addition & 2 deletions apps/api-v2/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ model User {
id String @id @default(uuid())
email String @unique
name String?
tasks Task[]
}

model Task {
Expand All @@ -24,5 +23,5 @@ model Task {
startAt DateTime?
expectedDurationSeconds Int?
userId String
user User @relation(fields: [userId], references: [id])
@@index([userId], name: "owner_id_idx") // planetscale doesn't support foreign keys
}
37 changes: 4 additions & 33 deletions apps/api-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@ import express from "express";
import passport from "passport";
import config from "./config";
import PinoHTTP from "pino-http";
import {
EncodedProfileTokenClaims,
IAuthCtx,
IAuthPassportCallbackCtx,
SupernovaResponse,
} from "./types";
import { ISupernovaTask } from "@supernova/types";
import { EncodedProfileTokenClaims, IAuthPassportCallbackCtx } from "./types";
import cors from "cors";
import cookieParser from "cookie-parser";
import { authenticateJWTMiddleware } from "./mws";
import { prisma } from "./db";
import { buildAuthRouter } from "./routers/auth";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import jwt from "jsonwebtoken";
import { buildTasksRouter } from "./routers/tasks";

export const createApp = () => {
const logger = PinoHTTP();
Expand All @@ -29,6 +22,7 @@ export const createApp = () => {
})
);
app.use(cookieParser()); // parses the cookies because apparently express doesn't do this by default
app.use(express.json()); // parses the request body

passport.use(
new GoogleStrategy(
Expand All @@ -53,32 +47,9 @@ export const createApp = () => {
)
);

// get all tasks belonging to the user
app.get("/tasks", authenticateJWTMiddleware, async (req, res) => {
const userId = (req.user as IAuthCtx).sub;
const tasks = await prisma.task.findMany({
where: {
userId,
},
});
res.json(
new SupernovaResponse<ISupernovaTask[]>({
data: tasks.map((t) => ({
id: t.id,
originalBuildText: t.title, // TODO: incorporate this into the database schema
title: t.title,
isComplete: t.done,
description: t.description ?? undefined,
startAt: t.startAt ?? undefined,
expectedDurationSeconds: t.expectedDurationSeconds ?? undefined,
userId: t.userId,
})),
})
);
});

// use auth router
app.use(buildAuthRouter());
app.use(buildTasksRouter());

return { app };
};
Expand Down
49 changes: 45 additions & 4 deletions apps/api-v2/src/mws.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import express from "express";
import { IAuthCtx, SupernovaResponse } from "./types";
import express, { Request, Response, NextFunction } from "express";
import {
IAuthCtx,
SupernovaRequestValidationSchema,
SupernovaResponse,
} from "./types";
import jwt from "jsonwebtoken";
import config from "./config";
import { redis } from "./db";
import { AnyZodObject, z } from "zod";

// TODO: redirect to the web app on errors instead of sending a JSON response
export const authenticateJWTMiddleware = async (
Expand All @@ -16,7 +21,7 @@ export const authenticateJWTMiddleware = async (
// verify if authHeader is valid
if (token) {
try {
const user = jwt.verify(token, config.JWT_SECRET) as IAuthCtx;
const user = jwt.verify(token, config.JWT_SECRET);
if (user.sub === undefined) {
return res.status(403).send(
new SupernovaResponse({
Expand Down Expand Up @@ -48,7 +53,7 @@ export const authenticateJWTMiddleware = async (
);
}
// set the user in the request
req.user = user;
req.user = user as IAuthCtx;
next();
} catch (err) {
return res.status(403).send(
Expand Down Expand Up @@ -77,3 +82,39 @@ export const authenticateJWTMiddleware = async (
);
}
};

/**
* Validates the request body, query and params against the schema
* @param schema s
* @returns
*/
export const validateRequestSchema =
(schema: SupernovaRequestValidationSchema) =>
async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
return next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json(
new SupernovaResponse({
message: "Invalid request; failed schema validation",
error: error.toString(),
})
);
}
// log the error
console.error(error);
return res.status(500).json(
new SupernovaResponse({
message: "Internal Server Error",
error:
"An error occurred while validating the request; this is on us, not you. Please try again later.",
})
);
}
};
74 changes: 74 additions & 0 deletions apps/api-v2/src/routers/tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Router } from "express";
import { authenticateJWTMiddleware, validateRequestSchema } from "../mws";
import { getAuthContext } from "../utils";
import { prisma } from "../db";
import { SupernovaResponse } from "../types";
import { createTaskRequestSchema } from "@supernova/types";

export const buildTasksRouter = () => {
const router = Router();

router.get("/tasks", authenticateJWTMiddleware, async (req, res) => {
try {
const authCtx = getAuthContext(req);
// get all tasks for the user
const tasks = await prisma.task.findMany({
where: {
userId: authCtx.sub,
},
});
return res.status(200).json(
new SupernovaResponse({
message: "Tasks fetched successfully",
data: tasks,
})
);
} catch (err) {
let e = err as Error;
return res.status(500).json(
new SupernovaResponse({
message: e.message,
error: "Internal Server Error",
})
);
}
});

// create task
router.post(
"/tasks",
authenticateJWTMiddleware,
validateRequestSchema(createTaskRequestSchema),
async (req, res) => {
try {
const authCtx = getAuthContext(req);
const task = await prisma.task.create({
data: {
title: req.body.title,
description: req.body.description,
startAt: req.body.startAt,
expectedDurationSeconds: req.body.expectedDurationSeconds,
userId: authCtx.sub,
},
});
return res.status(200).json(
new SupernovaResponse({
message: "Task created successfully",
data: task,
})
);
} catch (e) {
if (e instanceof Error) {
console.error(e);
return res.status(500).json(
new SupernovaResponse({
message: e.message,
error: "Internal Server Error",
})
);
}
}
}
);
return router;
};
12 changes: 11 additions & 1 deletion apps/api-v2/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Profile } from "passport-google-oauth20";
import jwt from "jsonwebtoken";
import { AnyZodObject, z } from "zod";

export interface ISupernovaResponse<T extends any> {
data?: T;
Expand Down Expand Up @@ -36,4 +37,13 @@ export interface IAuthPassportCallbackCtx {
export type EncodedProfileTokenClaims = { user: Profile } & jwt.JwtPayload;

// for storing auth context for the typical request protected by our JWT middleware
export type IAuthCtx = jwt.JwtPayload;
// sub is the user ID which will be string since the @authenticateJWTMiddleware middleware
// will verify that the token is valid and has a sub claim
export type IAuthCtx = jwt.JwtPayload & { sub: string };

// any validation schema
export type SupernovaRequestValidationSchema = z.ZodObject<{
body?: AnyZodObject;
query?: AnyZodObject;
params?: AnyZodObject;
}>;
6 changes: 6 additions & 0 deletions apps/api-v2/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Request } from "express";
import { IAuthCtx } from "./types";

export const getAuthContext = (req: Request): IAuthCtx => {
return req.user as IAuthCtx;
};
6 changes: 0 additions & 6 deletions apps/api/.dockerignore

This file was deleted.

11 changes: 0 additions & 11 deletions apps/api/.env.example

This file was deleted.

4 changes: 0 additions & 4 deletions apps/api/.gitignore

This file was deleted.

17 changes: 0 additions & 17 deletions apps/api/Dockerfile

This file was deleted.

Loading

0 comments on commit 39a6c19

Please sign in to comment.