Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 71 additions & 14 deletions codewit/api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import express from 'express';
import express, { Router } from 'express';
import { sequelize } from './models';
import demoRouter from './routes/demo';
import exerciseRouter from './routes/exercise';
Expand All @@ -10,14 +10,28 @@ import userRouter from './routes/user';
import attemptRouter from './routes/attempt';
import passport from 'passport';
import session from 'express-session';
import { COOKIE_KEY, HOST, PORT } from './secrets';
import { ENABLE_ASSET_SERVING, COOKIE_KEY, HOST, PORT } from './secrets';
import './auth/passport';
import { checkAuth } from './middleware/auth';
import { catchError, asyncHandle } from "./middleware/catch";
import { log_request } from "./middleware/logging";
import { set_request_id } from "./middleware/id";
import { init } from "./utils/id_generator";
import handler from "serve-handler";
import { join } from "node:path";

const cwd = process.cwd();

// we need to serve data from "dist/apps/client"
const assets_dir = join(cwd, "dist/apps/client");

const app = express();

// ensure that a request id and uuid have been assigned to each inbound http
// request
app.use(set_request_id);
app.use(log_request);

app.use(
session({
secret: COOKIE_KEY,
Expand All @@ -35,22 +49,65 @@ app.use(passport.session());

app.use(express.json());

// there is a mix of no api prefix requests and api prefix requests that are
// being made when attempting to authorize a user with google. this is to help
// with this but it should be fixed so that the flow is more consistent
app.use('/oauth2', authrouter);

app.use('/users', checkAuth, userRouter);
app.use('/demos', checkAuth, demoRouter);
app.use('/exercises', checkAuth, exerciseRouter);
app.use('/modules', checkAuth, moduleRouter);
app.use('/resources', checkAuth, resourceRouter);
app.use('/courses', (req, res, next) => {
if (req.path === '/landing' || req.path === '/landing/') {
return next();
}
if (ENABLE_ASSET_SERVING) {
// start api configuration -----------------------------------------------------

const api_router = Router();

api_router.use('/oauth2', authrouter);

api_router.use('/users', checkAuth, userRouter);
api_router.use('/demos', checkAuth, demoRouter);
api_router.use('/exercises', checkAuth, exerciseRouter);
api_router.use('/modules', checkAuth, moduleRouter);
api_router.use('/resources', checkAuth, resourceRouter);
api_router.use('/attempts', checkAuth, attemptRouter);

api_router.use('/courses', (req, res, next) => {
if (req.path === '/landing' || req.path === '/landing/') {
return next();
}

return checkAuth(req, res, next);
}, courseRouter);

app.use("/api", api_router);


// handle all requests as if they are trying to retrieve UI assets
app.get("/*", async (req, res) => {
await handler(req, res, {
public: assets_dir,
rewrites: [
{ "source": "/**", "destination": "/index.html" }
]
});
});

// end api configuration -------------------------------------------------------
} else {
app.use('/users', checkAuth, userRouter);
app.use('/demos', checkAuth, demoRouter);
app.use('/exercises', checkAuth, exerciseRouter);
app.use('/modules', checkAuth, moduleRouter);
app.use('/resources', checkAuth, resourceRouter);
app.use('/attempts', checkAuth, attemptRouter);

app.use('/courses', (req, res, next) => {
if (req.path === '/landing' || req.path === '/landing/') {
return next();
}

return checkAuth(req, res, next);
}, courseRouter);
app.use('/attempts', checkAuth, attemptRouter);
return checkAuth(req, res, next);
}, courseRouter);
}

// catch all for dealing with errors
app.use(catchError);

init()
Expand Down
21 changes: 21 additions & 0 deletions codewit/api/src/middleware/id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Request, Response, NextFunction } from "express";
import { v4 as uuidv4 } from "uuid";

// a request counter to keep track of the total number of requests
let request_counter = 0;

// assigns an id and uuid to the provided request, used for tracing a request
// through the server and to other services
export function set_request_id(req: Request, res: Response, next: NextFunction) {
// the current request id count to assign
let id = request_counter += 1;
// the uuid to assign to the request
let uuid = uuidv4();

// going to make them functions as to prevent overwriting the values, will
// not stop from overwriting the functions but still
req.id = () => id;
req.uuid = () => uuid;

next();
}
47 changes: 47 additions & 0 deletions codewit/api/src/middleware/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Request, Response, NextFunction } from "express";

// logs any incoming requests once the request has finished
export function log_request(req: Request, res: Response, next: NextFunction) {
let start = process.hrtime.bigint();

// pull the current request id and uuid
let id = req.id();
let uuid = req.uuid();

let ip = req.socket.remoteAddress;
let port = req.socket.remotePort;
let version = req.httpVersion;
let method = req.method;
let path = req.path;

// when the "finish" event is triggered we will then log the request and
// response because we will have the status code by that time
res.once("finish", () => {
let now = new Date();
let finish = process.hrtime.bigint();
let duration = finish - start;
let code = res.statusCode;
let message = res.statusMessage;

console.log(`${now.toJSON()} ${id}:${uuid} ${ip}:${port} HTTP/${version} ${method} ${path} ${code} ${message} ${format_duration(duration)}`);
});

next();
}

const TIME_UNITS = ["ns", "μs", "ms", "s"];

// simple duration formatter that will convert nanoseconds to a larger unit
// for easier readability
function format_duration(duration: bigint): string {
let index = 0;
let while_check = BigInt(2000);
let div_assign = BigInt(1000);

while (index < TIME_UNITS.length && duration > while_check) {
duration /= div_assign;
index += 1;
}

return `${duration}${TIME_UNITS[index]}`;
}
19 changes: 8 additions & 11 deletions codewit/api/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,22 @@ import { ModuleDemos } from './moduleDemos';

require('dotenv').config();

if (
!process.env.DB_HOST ||
!process.env.DB_NAME ||
!process.env.DB_USER ||
!process.env.DB_PASSWORD ||
!process.env.DB_PORT
) {
throw new Error(
'Please provide the DB_HOST, DB_NAME, DB_USER, DB_PASSWORD, and DB_PORT environment variables'
);
const required_keys = ["DB_HOST", "DB_NAME", "DB_USER"];

for (let key of required_keys) {
if (!(key in process.env)) {
throw new Error(`Missing required database value: "${key}"`);
}
}

const sequelize = new Sequelize({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: Number(process.env.DB_PORT),
port: Number(process.env.DB_PORT ?? 5432),
dialect: 'postgres',
logging: false,
pool: {
max: 10,
min: 0,
Expand Down
10 changes: 10 additions & 0 deletions codewit/api/src/secrets.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
function check_if_boolean(value: string): boolean {
switch (value.toLowerCase()) {
case "true": return true;
case "false": return false;
default: return false
}
}

const HOST = process.env.API_HOST ?? '0.0.0.0';
const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const GOOGLE_REDIRECT_URL = process.env.GOOGLE_REDIRECT_URL;
const COOKIE_KEY = process.env.COOKIE_KEY as string;
const FRONTEND_URL = process.env.FRONTEND_URL;
const ENABLE_ASSET_SERVING = check_if_boolean(process.env.ENABLE_ASSET_SERVING ?? "");

export {
HOST,
Expand All @@ -14,4 +23,5 @@ export {
GOOGLE_REDIRECT_URL,
COOKIE_KEY,
FRONTEND_URL,
ENABLE_ASSET_SERVING,
};
12 changes: 12 additions & 0 deletions codewit/api/src/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,17 @@ declare global {
namespace Express {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface User extends UserDocument {}

export interface Request {
// retrieves the current request id
//
// represents the total number of requests handled
id: () => number,

// retrieves the current request uuid
//
// used for tracing requests to other servers (eg codeval)
uuid: () => string,
}
}
}
3 changes: 0 additions & 3 deletions codewit/client/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ export default defineConfig(({ mode }) => ({
'/api': {
target: API_TARGET,
changeOrigin: true,
rewrite: usingGatewayProxy
? undefined
: (path) => path.replace(/^\/api/, ''),
},
'/codeeval': {
target: CODEEVAL_TARGET,
Expand Down
1 change: 1 addition & 0 deletions codewit/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ services:
- PORT=3000
- FRONTEND_URL=http://localhost:3001
- NODE_ENV=dev
- ENABLE_ASSET_SERVING=true
env_file:
- .env
restart: unless-stopped
Expand Down
Loading
Loading