Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate autograder to use Javalin #473

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7dd5955
feat: add Exception classes for HTTP status codes
ThanGerlek Oct 27, 2024
cbfa941
feat: add Spark.exception() handling
ThanGerlek Oct 27, 2024
be42e8e
chore: rmv now-redundant exception handling
ThanGerlek Oct 27, 2024
73896e1
migrate Server
ThanGerlek Oct 28, 2024
5ec2983
migrate: register Serializer with Javalin and use ctx.json()
ThanGerlek Oct 29, 2024
6faceab
remove unneeded code
ThanGerlek Nov 6, 2024
8be07f9
rmv unused code
ThanGerlek Nov 7, 2024
d4baa45
fix: update (and format) WebSocketController
ThanGerlek Nov 6, 2024
ece6093
minor ctx standardizations
ThanGerlek Nov 7, 2024
3484fc3
fix: update path param notation
ThanGerlek Nov 7, 2024
2d8a848
fix: update docstring
ThanGerlek Nov 7, 2024
99e1898
Merge branch 'extract-service-logic-from-controllers' into javalin-mi…
ThanGerlek Nov 8, 2024
1db89d1
rename /httpexception to /exception in preparation for merge
ThanGerlek Nov 14, 2024
98df175
Merge branch 'main' into javalin-migration
ThanGerlek Nov 14, 2024
6b91010
fix: update pom.xml
ThanGerlek Nov 14, 2024
e2adf91
remove duplicate file
ThanGerlek Nov 14, 2024
3eaa324
remove unused constructors
ThanGerlek Nov 14, 2024
5213997
Merge branch 'reduce-endpoint-dependencies' into javalin-migration
ThanGerlek Nov 15, 2024
a14767b
fix: merge conflicts
ThanGerlek Nov 15, 2024
16e914d
Merge branch 'reduce-endpoint-dependencies' into javalin-migration
ThanGerlek Nov 15, 2024
e31cec3
fix: add null checking for "user" session attribute
ThanGerlek Nov 16, 2024
34d26a4
fix: add null checking for error messages in thrown exceptions
ThanGerlek Nov 16, 2024
38c150b
Merge branch 'reduce-endpoint-dependencies' into javalin-migration
ThanGerlek Nov 17, 2024
e2b2431
fix: add .options() handler
ThanGerlek Nov 17, 2024
2d5479d
fix: incorrect parameter order
ThanGerlek Nov 17, 2024
4f43606
fix: enable automatic WebSocket pings to prevent IdleTimeout issues
ThanGerlek Nov 18, 2024
f4bb156
Merge branch 'reduce-endpoint-dependencies' into javalin-migration
ThanGerlek Nov 19, 2024
1a4fe96
fix: convert scheduleShutdown() to Javalin Handler
ThanGerlek Nov 19, 2024
237090e
minor cleanups
ThanGerlek Nov 19, 2024
164a809
fix: remove outdated TODO
ThanGerlek Nov 19, 2024
0c0ab8b
feat: add default latestSubmissions endpoint
ThanGerlek Nov 19, 2024
c6fdb95
rmv unneeded null handling
ThanGerlek Nov 19, 2024
3f40215
rmv unneeded throw
ThanGerlek Nov 19, 2024
547b618
Merge branch 'main' into javalin-migration
ThanGerlek Nov 19, 2024
ee4f698
make getConfig consistent public/private
ThanGerlek Nov 20, 2024
8066aaf
Merge branch 'main' into javalin-migration
ThanGerlek Nov 25, 2024
7e2a8fb
fix: migrate updatePenalties() to Javalin
ThanGerlek Nov 25, 2024
dc70b35
refactor: extract Serializer's JsonMapper into an explicit adapter class
ThanGerlek Dec 4, 2024
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
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
<version>6.9.0.202403050737-r</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.sparkjava/spark-core -->
<!-- https://mvnrepository.com/artifact/io.javalin/javalin -->
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>2.9.4</version>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>6.3.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
Expand Down
93 changes: 25 additions & 68 deletions src/main/java/edu/byu/cs/controller/AdminController.java
Original file line number Diff line number Diff line change
@@ -1,106 +1,63 @@
package edu.byu.cs.controller;

import edu.byu.cs.canvas.CanvasException;
import edu.byu.cs.canvas.model.CanvasSection;
import edu.byu.cs.dataAccess.DataAccessException;
import edu.byu.cs.dataAccess.ItemNotFoundException;
import edu.byu.cs.controller.exception.ResourceNotFoundException;
import edu.byu.cs.model.User;
import edu.byu.cs.service.AdminService;
import edu.byu.cs.util.Serializer;
import io.javalin.http.Handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Route;

import java.io.OutputStream;
import java.util.Collection;

import static edu.byu.cs.util.JwtUtils.generateToken;
import static spark.Spark.halt;

public class AdminController {

private static final Logger LOGGER = LoggerFactory.getLogger(AdminController.class);

public static final Route usersGet = (req, res) -> {
Collection<User> users;
try {
users = AdminService.getUsers();
} catch (DataAccessException e) {
halt(500);
return null;
}

res.type("application/json");
res.status(200);

return Serializer.serialize(users);

public static final Handler usersGet = ctx -> {
Collection<User> users = AdminService.getUsers();
ctx.json(users);
};

public static final Route testModeGet = (req, res) -> {
User testStudent;
try {
testStudent = AdminService.updateTestStudent();
} catch (CanvasException | DataAccessException e) {
halt(500);
return null;
}

res.cookie("/", "token", generateToken(testStudent.netId()), 14400, false, false);

res.status(200);

return null;
public static final Handler testModeGet = ctx -> {
User testStudent = AdminService.updateTestStudent();
ctx.cookie("token", generateToken(testStudent.netId()), 14400);
};

public static final Route commitAnalyticsGet = (req, res) -> {
String option = req.params(":option");
String data;

public static final Handler commitAnalyticsGet = ctx -> {
String option = ctx.pathParam("option");
try {
data = AdminService.getCommitAnalytics(option);
String data = AdminService.getCommitAnalytics(option);
ctx.result(data);
} catch (IllegalStateException e) {
LOGGER.error(e.getMessage());
throw new ResourceNotFoundException(e.getMessage(), e);
} catch (Exception e) {
LOGGER.error(e.getMessage());
if (e instanceof IllegalStateException) res.status(404);
else res.status(500);
return e.getMessage();
throw e;
}

res.status(200);

return data;
};

public static final Route honorCheckerZipGet = (req, res) -> {
String sectionStr = req.params(":section");
public static final Handler honorCheckerZipGet = ctx -> {
String sectionStr = ctx.pathParam("section");

try (OutputStream os = res.raw().getOutputStream()) {
try (OutputStream os = ctx.res().getOutputStream()) {
AdminService.streamHonorCheckerZip(sectionStr, os);

} catch (Exception e) {
LOGGER.error("Error compiling honor checker", e);
res.status(500);
return e.getMessage();
throw e;
}

res.status(200);

res.header("Content-Type", "application/zip");
res.header("Content-Disposition", "attachment; filename=" + "downloaded_file.zip");

return res.raw();

ctx.header("Content-Type", "application/zip");
ctx.header("Content-Disposition", "attachment; filename=" + "downloaded_file.zip");
};

public static Route sectionsGet = (req, res) -> {
try {
CanvasSection[] sections = AdminService.getAllSections();
res.type("application/json");
res.status(200);
return Serializer.serialize(sections);
} catch (CanvasException e) {
res.status(500);
return e.getMessage();
}
public static Handler sectionsGet = ctx -> {
CanvasSection[] sections = AdminService.getAllSections();
ctx.json(sections);
};
}
54 changes: 28 additions & 26 deletions src/main/java/edu/byu/cs/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
package edu.byu.cs.controller;

import edu.byu.cs.controller.exception.BadRequestException;
import edu.byu.cs.controller.exception.InternalServerException;
import edu.byu.cs.controller.exception.ResourceForbiddenException;
import edu.byu.cs.controller.exception.UnauthorizedException;
import edu.byu.cs.dataAccess.DaoService;
import edu.byu.cs.dataAccess.DataAccessException;
import edu.byu.cs.dataAccess.UserDao;
import edu.byu.cs.model.User;
import edu.byu.cs.util.Serializer;
import io.javalin.http.Handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Filter;
import spark.Route;

import static edu.byu.cs.util.JwtUtils.validateToken;
import static spark.Spark.halt;

public class AuthController {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthController.class);

/**
* A filter that verifies that the request has a valid JWT in the Authorization header.
* A handler that verifies that the request has a valid JWT in the Authorization header.
* If the request is valid, the netId is added to the session for later use.
*/
public static final Filter verifyAuthenticatedMiddleware = (req, res) -> {
String token = req.cookie("token");
public static final Handler verifyAuthenticatedMiddleware = ctx -> {
String token = ctx.cookie("token");

if (token == null) {
halt(401);
return;
throw new UnauthorizedException();
}
String netId = validateToken(token);

// token is expired or invalid
if (netId == null) {
res.cookie("/", "token", "", 0, false, false);
halt(401);
return;
ctx.cookie("token", "", 0);
throw new UnauthorizedException();
}

UserDao userDao = DaoService.getUserDao();
Expand All @@ -42,33 +41,36 @@ public class AuthController {
user = userDao.getUser(netId);
} catch (DataAccessException e) {
LOGGER.error("Error getting user from database", e);
halt(500);
return;
throw new InternalServerException("Error getting user from database", e);
}

if (user == null) {
LOGGER.error("Received request from unregistered user. This shouldn't be possible: {}", netId);
halt(400, "You must register first.");
return;
throw new BadRequestException("You must register first.");
}

req.session().attribute("user", user);
ctx.sessionAttribute("user", user);
};

public static final Filter verifyAdminMiddleware = (req, res) -> {
User user = req.session().attribute("user");
public static final Handler verifyAdminMiddleware = ctx -> {
User user = ctx.sessionAttribute("user");

if (user == null) {
throw new UnauthorizedException("No user credentials found");
}

if (user.role() != User.Role.ADMIN) {
halt(403);
throw new ResourceForbiddenException();
}
};

public static final Route meGet = (req, res) -> {
User user = req.session().attribute("user");

res.status(200);
res.type("application/json");
return Serializer.serialize(user);
public static final Handler meGet = ctx -> {
User user = ctx.sessionAttribute("user");
if (user == null) {
ctx.result("No user found.");
} else {
ctx.json(user);
}
};

}
51 changes: 19 additions & 32 deletions src/main/java/edu/byu/cs/controller/CasController.java
Original file line number Diff line number Diff line change
@@ -1,66 +1,53 @@
package edu.byu.cs.controller;

import edu.byu.cs.canvas.CanvasException;
import edu.byu.cs.controller.exception.BadRequestException;
import edu.byu.cs.controller.exception.InternalServerException;
import edu.byu.cs.dataAccess.DataAccessException;
import edu.byu.cs.model.User;
import edu.byu.cs.properties.ApplicationProperties;
import edu.byu.cs.service.CasService;
import spark.Route;
import io.javalin.http.Handler;
import io.javalin.http.HttpStatus;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

import static edu.byu.cs.util.JwtUtils.generateToken;
import static spark.Spark.halt;

public class CasController {
public static final Route callbackGet = (req, res) -> {
String ticket = req.queryParams("ticket");
public static final Handler callbackGet = ctx -> {
String ticket = ctx.queryParam("ticket");

User user;
try {
user = CasService.callback(ticket);
} catch (InternalServerException | DataAccessException e) {
halt(500);
return null;
} catch (BadRequestException e) {
halt(400);
return null;
} catch (CanvasException e) {
String errorUrlParam = URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8);
res.redirect(ApplicationProperties.frontendUrl() + "/login?error=" + errorUrlParam, 302);
halt(500);
return null;
ctx.redirect(ApplicationProperties.frontendUrl() + "/login?error=" + errorUrlParam, HttpStatus.FOUND);
return;
}

// FIXME: secure cookie with httpOnly
res.cookie("/", "token", generateToken(user.netId()), 14400, false, false);
res.redirect(ApplicationProperties.frontendUrl(), 302);
return null;
ctx.cookie("token", generateToken(user.netId()), 14400);
ctx.redirect(ApplicationProperties.frontendUrl(), HttpStatus.FOUND);
};

public static final Route loginGet = (req, res) -> {
public static final Handler loginGet = ctx -> {
// check if already logged in
if (req.cookie("token") != null) {
res.redirect(ApplicationProperties.frontendUrl(), 302);
return null;
if (ctx.cookie("token") != null) {
ctx.redirect(ApplicationProperties.frontendUrl(), HttpStatus.FOUND);
return;
}
res.redirect(CasService.BYU_CAS_URL + "/login" + "?service=" + ApplicationProperties.casCallbackUrl());
return null;
ctx.redirect(CasService.BYU_CAS_URL + "/login" + "?service=" + ApplicationProperties.casCallbackUrl());
};

public static final Route logoutPost = (req, res) -> {
if (req.cookie("token") == null) {
res.redirect(ApplicationProperties.frontendUrl(), 401);
return null;
public static final Handler logoutPost = ctx -> {
if (ctx.cookie("token") == null) {
ctx.redirect(ApplicationProperties.frontendUrl(), HttpStatus.UNAUTHORIZED);
return;
}

// TODO: call cas logout endpoint with ticket
res.removeCookie("/", "token");
res.redirect(ApplicationProperties.frontendUrl(), 200);
return null;
ctx.removeCookie("token", "/");
ctx.redirect(ApplicationProperties.frontendUrl(), HttpStatus.OK);
};

}
Loading
Loading