/* (C) 2025 Linnea Gräf - Licensed to everyone under the BSD 3 Clause License */ package moe.nea.prickly.server; import com.google.common.base.Preconditions; import gg.jte.ContentType; import gg.jte.TemplateEngine; import gg.jte.resolve.DirectoryCodeResolver; import io.javalin.Javalin; import io.javalin.config.JavalinConfig; import io.javalin.rendering.template.JavalinJte; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Objects; import lombok.extern.slf4j.Slf4j; import moe.nea.prickly.auth.Authorizations; import moe.nea.prickly.config.Config; import moe.nea.prickly.model.AuthorizationRequest; import moe.nea.prickly.model.AuthorizeAction; import moe.nea.prickly.util.BundleUtil; import moe.nea.prickly.util.JsonHelper; import moe.nea.prickly.util.OAuthUtil; import moe.nea.prickly.util.QueryParameterHelper; import org.jspecify.annotations.Nullable; @Slf4j public class Server { Javalin javalin; public Server() { log.info("creating server instance"); javalin = Javalin.create(this::configure); installRoutes(); } protected void installRoutes() { log.info("installing routes"); for (Config.Application application : Config.APPLICATIONS) { installApplication(application); } } protected void installApplication(Config.Application application) { log.debug("registering application {}", application); var prefix = "/app/" + application.SLUG; javalin.get(prefix + "/", ctx -> { ctx.redirect(application.HOMEPAGE); }); javalin.get(prefix + "/authorize", ctx -> { var responseType = OAuthUtil.parseResponseType(ctx.queryParam("response_type")); var redirectUri = OAuthUtil.verifyRedirectUrl(ctx.queryParam("redirect_uri"), application.REDIRECT_URI); var state = ctx.queryParam("state"); var clientId = ctx.queryParam("client_id"); Preconditions.checkArgument( Objects.equals(clientId, application.SLUG), "client_id does not match application slug"); var scope = OAuthUtil.parseScopes(ctx.queryParam("scope")); ctx.render( "authorize.jte", Map.of( "application", application, "authorizationRequest", new AuthorizationRequest(responseType, redirectUri, state, scope))); }); javalin.post(prefix + "/authorize", ctx -> { var authRequest = JsonHelper.parseFormParam(ctx.formParam("authRequest"), AuthorizationRequest.class); var action = AuthorizeAction.valueOf(ctx.formParam("action")); var map = new HashMap(); map.put("state", authRequest.state()); switch (action) { case ACCEPT: // TODO: differentiate authRequest.responseType() map.put("code", Authorizations.createToken(Objects.requireNonNull(ctx.formParam("username")))); break; case DENY: map.put("error", "access_denied"); map.put("error_description", "You have denied access to " + application.NAME); break; } ctx.redirect(QueryParameterHelper.appendQuery(authRequest.redirectUri(), map) .toString()); }); } protected void configure(JavalinConfig config) { log.info("configuring javalin"); var templateEngine = BundleUtil.IS_BUNDLED ? TemplateEngine.createPrecompiled(ContentType.Html) : TemplateEngine.create(new DirectoryCodeResolver(Path.of("src/main/jte")), ContentType.Html); config.fileRenderer(new JavalinJte(templateEngine)); } public void start() { var port = Config.PORT; var host = Config.HOST; log.info("starting on port http://{}:{}", host, port); javalin.start(host, port); } }