diff --git a/pom.xml b/pom.xml index e65fe9e94..7469f93e9 100644 --- a/pom.xml +++ b/pom.xml @@ -333,34 +333,74 @@ 1.9.0 - + - io.dropwizard - dropwizard-core - ${dropwizard.version} + io.ktor + ktor-server-core-jvm + ${ktor.version} - io.dropwizard - dropwizard-auth - ${dropwizard.version} + io.ktor + ktor-server-netty-jvm + ${ktor.version} - - io.dropwizard - dropwizard-assets - ${dropwizard.version} + io.ktor + ktor-server-sessions-jvm + ${ktor.version} - - - io.dropwizard - dropwizard-views-freemarker - ${dropwizard.version} + io.ktor + ktor-server-auth-jvm + ${ktor.version} + + + + io.ktor + ktor-server-freemarker-jvm + ${ktor.version} + + + + io.ktor + ktor-server-status-pages-jvm + ${ktor.version} + + + + io.ktor + ktor-server-call-logging-jvm + ${ktor.version} + + + + io.ktor + ktor-server-content-negotiation-jvm + ${ktor.version} + + + + io.ktor + ktor-serialization-jackson-jvm + ${ktor.version} + + + + io.ktor + ktor-server-partial-content-jvm + ${ktor.version} + + + + + javax.servlet + javax.servlet-api + 4.0.1 - + @@ -500,7 +540,7 @@ 1.1.1 4.4.4 - 2.1.5 + 3.0.3 5.1.0 2.21.0 0.8.5 diff --git a/src/main/kotlin/javabot/web/JavabotApplication.kt b/src/main/kotlin/javabot/web/JavabotApplication.kt index 6cfee5321..25b585f52 100644 --- a/src/main/kotlin/javabot/web/JavabotApplication.kt +++ b/src/main/kotlin/javabot/web/JavabotApplication.kt @@ -4,14 +4,19 @@ import com.google.inject.Guice import com.google.inject.Inject import com.google.inject.Injector import com.google.inject.Singleton -import io.dropwizard.Application -import io.dropwizard.assets.AssetsBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment -import io.dropwizard.views.ViewBundle +import freemarker.cache.ClassTemplateLoader +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.freemarker.* +import io.ktor.server.http.content.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.sessions.* import java.io.File import java.nio.file.Files -import java.util.EnumSet import javabot.Javabot import javabot.JavabotConfig import javabot.JavabotModule @@ -19,21 +24,12 @@ import javabot.dao.ApiDao import javabot.web.resources.AdminResource import javabot.web.resources.BotResource import javabot.web.resources.PublicOAuthResource -import javax.servlet.DispatcherType -import javax.servlet.Filter -import javax.servlet.FilterChain -import javax.servlet.FilterConfig -import javax.servlet.ServletRequest -import javax.servlet.ServletResponse -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse -import org.eclipse.jetty.server.session.SessionHandler import org.slf4j.LoggerFactory @Singleton -class JavabotApplication @Inject constructor(var injector: Injector) : - Application() { +class JavabotApplication @Inject constructor(var injector: Injector) { var running = false + lateinit var server: EmbeddedServer<*, *> companion object { private val LOG = LoggerFactory.getLogger(JavabotApplication::class.java) @@ -41,75 +37,97 @@ class JavabotApplication @Inject constructor(var injector: Injector) : @Throws(Exception::class) @JvmStatic fun main(args: Array) { - Guice.createInjector(JavabotModule()) - .getInstance(JavabotApplication::class.java) - .run(*arrayOf("server", "javabot.yml")) + val application = + Guice.createInjector(JavabotModule()).getInstance(JavabotApplication::class.java) + application.run() } } - override fun initialize(bootstrap: Bootstrap) { - bootstrap.addBundle(ViewBundle()) - bootstrap.addBundle(AssetsBundle("/assets", "/assets", null, "assets")) - bootstrap.addBundle( - AssetsBundle("/META-INF/resources/webjars", "/webjars", null, "webjars") - ) - } - - override fun run(configuration: JavabotConfiguration, environment: Environment) { - environment.applicationContext.isSessionsEnabled = true - environment.applicationContext.sessionHandler = SessionHandler() + fun run() { + val configuration = JavabotConfiguration() val bot = injector.getInstance(Javabot::class.java) bot.setUpThreads() - val oauth = injector.getInstance(PublicOAuthResource::class.java) - oauth.configuration = configuration - environment.jersey().register(oauth) + server = + embeddedServer(Netty, port = 8080, host = "0.0.0.0") { configureServer(configuration) } - environment.jersey().register(injector.getInstance(BotResource::class.java)) - environment.jersey().register(injector.getInstance(AdminResource::class.java)) - environment.jersey().register(RuntimeExceptionMapper(configuration)) + running = true + server.start(wait = true) + } + + private fun Application.configureServer(configuration: JavabotConfiguration) { + // Install FreeMarker for templating + install(FreeMarker) { + templateLoader = ClassTemplateLoader(this::class.java.classLoader, "/") + } - environment - .servlets() - .addFilter("javadoc", injector.getInstance(JavadocFilter::class.java)) - .addMappingForUrlPatterns( - EnumSet.allOf(DispatcherType::class.java), - false, - "/javadoc/*", - ) + // Install sessions + install(Sessions) { + cookie(JavabotConfiguration.SESSION_TOKEN_NAME) { + cookie.path = "/" + cookie.maxAgeInSeconds = 86400 * 30 + } + } - environment.healthChecks().register("javabot", JavabotHealthCheck()) + // Install status pages for error handling + install(StatusPages) { + exception { call, cause -> + LOG.error("Request failed", cause) + call.respond( + HttpStatusCode.InternalServerError, + "Internal Server Error: ${cause.message}", + ) + } - running = false - } + status(HttpStatusCode.NotFound) { call, status -> + call.respondText("404: Page Not Found", status = status) + } + } - class JavadocFilter @Inject constructor(var apiDao: ApiDao, var config: JavabotConfig) : - Filter { - override fun destroy() {} - - override fun doFilter( - request: ServletRequest, - response: ServletResponse, - chain: FilterChain, - ) { - request as HttpServletRequest - var filePath = request.requestURI.split("/").drop(2).joinToString("/") - if (!filePath.startsWith("/")) { - filePath = "/" + filePath + // Configure routing + routing { + // Static assets using static() which serves files from resources + static("/assets") { + resources("assets") + } + static("/webjars") { + resources("META-INF/resources/webjars") } - val path = File("javadoc$filePath").toPath() - if (Files.exists(path)) { - response.outputStream.use { stream -> - Files.copy(path, stream) - stream.flush() + // Javadoc filter + get("/javadoc/{...}") { + val apiDao = injector.getInstance(ApiDao::class.java) + val config = injector.getInstance(JavabotConfig::class.java) + + val pathAfterJavadoc = call.request.uri.substringAfter("/javadoc/") + val filePath = "/$pathAfterJavadoc" + val path = File("javadoc$filePath").toPath() + + if (Files.exists(path)) { + call.respondFile(path.toFile()) + } else { + call.respond(HttpStatusCode.NotFound) } - } else { - (response as HttpServletResponse).sendError(404) } - } - override fun init(filterConfig: FilterConfig?) {} + // Health check + get("/health") { call.respondText("OK", contentType = ContentType.Text.Plain) } + + // Register OAuth routes + val oauth = injector.getInstance(PublicOAuthResource::class.java) + oauth.configuration = configuration + oauth.configureRoutes(this) + + // Register Bot routes + val botResource = injector.getInstance(BotResource::class.java) + botResource.configureRoutes(this) + + // Register Admin routes + val adminResource = injector.getInstance(AdminResource::class.java) + adminResource.configureRoutes(this) + } } } + +data class UserSession(val sessionToken: String) diff --git a/src/main/kotlin/javabot/web/JavabotConfiguration.kt b/src/main/kotlin/javabot/web/JavabotConfiguration.kt index 2b1e3f873..1007f3e10 100644 --- a/src/main/kotlin/javabot/web/JavabotConfiguration.kt +++ b/src/main/kotlin/javabot/web/JavabotConfiguration.kt @@ -2,12 +2,11 @@ package javabot.web import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import io.dropwizard.Configuration import java.util.HashMap import java.util.Properties import javabot.web.model.OAuthConfig -class JavabotConfiguration : Configuration() { +class JavabotConfiguration { companion object { val SESSION_TOKEN_NAME: String = "JavabotSession" diff --git a/src/main/kotlin/javabot/web/JavabotHealthCheck.kt b/src/main/kotlin/javabot/web/JavabotHealthCheck.kt deleted file mode 100644 index 37309ca8b..000000000 --- a/src/main/kotlin/javabot/web/JavabotHealthCheck.kt +++ /dev/null @@ -1,10 +0,0 @@ -package javabot.web - -import com.codahale.metrics.health.HealthCheck - -class JavabotHealthCheck : HealthCheck() { - @Throws(Exception::class) - override fun check(): Result { - return Result.healthy() - } -} diff --git a/src/main/kotlin/javabot/web/RuntimeExceptionMapper.kt b/src/main/kotlin/javabot/web/RuntimeExceptionMapper.kt deleted file mode 100644 index e4e8eb6f8..000000000 --- a/src/main/kotlin/javabot/web/RuntimeExceptionMapper.kt +++ /dev/null @@ -1,69 +0,0 @@ -package javabot.web - -import java.net.URI -import java.net.URISyntaxException -import javabot.web.resources.PublicErrorResource -import javax.ws.rs.WebApplicationException -import javax.ws.rs.core.Context -import javax.ws.rs.core.Response -import javax.ws.rs.core.Response.Status.FORBIDDEN -import javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR -import javax.ws.rs.core.Response.Status.NOT_FOUND -import javax.ws.rs.core.Response.Status.TEMPORARY_REDIRECT -import javax.ws.rs.core.Response.Status.UNAUTHORIZED -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider -import org.apache.http.protocol.HttpContext -import org.slf4j.LoggerFactory - -@Provider -class RuntimeExceptionMapper(val configuration: JavabotConfiguration) : - ExceptionMapper { - - companion object { - private val LOG = LoggerFactory.getLogger(RuntimeExceptionMapper::class.java) - } - - @Context private var httpContext: HttpContext? = null - - override fun toResponse(runtime: RuntimeException): Response { - - if (runtime is WebApplicationException) { - return handleWebApplicationException(runtime) - } else { - LOG.error(runtime.message, runtime) - return Response.status(INTERNAL_SERVER_ERROR) - .entity(PublicErrorResource.view500()) - .build() - } - } - - private fun handleWebApplicationException(exception: RuntimeException): Response { - val webAppException = exception as WebApplicationException - - // No logging - val status = webAppException.response.status - if (status == UNAUTHORIZED.statusCode) { - try { - return Response.status(TEMPORARY_REDIRECT).location(URI("/auth/login")).build() - } catch (e: URISyntaxException) { - return Response.status(INTERNAL_SERVER_ERROR) - .entity(PublicErrorResource.view500()) - .build() - } - } else if (status == FORBIDDEN.statusCode) { - return Response.status(INTERNAL_SERVER_ERROR) - .entity(PublicErrorResource.view403()) - .build() - } else if (status == NOT_FOUND.statusCode) { - return Response.status(INTERNAL_SERVER_ERROR) - .entity(PublicErrorResource.view404()) - .build() - } else { - LOG.error(exception.message, exception) - return Response.status(INTERNAL_SERVER_ERROR) - .entity(PublicErrorResource.view500()) - .build() - } - } -} diff --git a/src/main/kotlin/javabot/web/resources/AdminResource.kt b/src/main/kotlin/javabot/web/resources/AdminResource.kt index a1c6da25b..e5e0d5436 100644 --- a/src/main/kotlin/javabot/web/resources/AdminResource.kt +++ b/src/main/kotlin/javabot/web/resources/AdminResource.kt @@ -1,6 +1,11 @@ package javabot.web.resources -import io.dropwizard.views.View +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.freemarker.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* import javabot.Javabot import javabot.JavabotConfig import javabot.dao.AdminDao @@ -11,24 +16,11 @@ import javabot.model.Admin import javabot.model.ApiEvent import javabot.model.Channel import javabot.model.javadoc.JavadocApi -import javabot.web.auth.Restricted -import javabot.web.model.Authority import javabot.web.model.User import javabot.web.views.ViewFactory import javax.inject.Inject -import javax.servlet.http.HttpServletRequest -import javax.ws.rs.Consumes -import javax.ws.rs.FormParam -import javax.ws.rs.GET -import javax.ws.rs.POST -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.WebApplicationException -import javax.ws.rs.core.Context -import javax.ws.rs.core.MediaType import org.bson.types.ObjectId -@Path("/admin") class AdminResource @Inject constructor( @@ -41,223 +33,404 @@ constructor( var config: JavabotConfig, ) { - @GET - fun index( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - ): View { - val current = adminDao.getAdminByEmailAddress(user.email) - return if (current == null) PublicErrorResource.view403() - else viewFactory.createAdminIndexView(request, current, Admin()) - } + fun configureRoutes(routing: Routing) { + with(routing) { + route("/admin") { + get { + val user = + getAuthenticatedUser(call) + ?: run { + val view = PublicErrorResource.view403() + call.respond(FreeMarkerContent(view.template, view.toModel())) + return@get + } - @GET - @Path("/config") - fun config( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - ): View { - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - return viewFactory.createConfigurationView(request) - } + val current = adminDao.getAdminByEmailAddress(user.email) + if (current == null) { + val view = PublicErrorResource.view403() + call.respond(FreeMarkerContent(view.template, view.toModel())) + } else { + val view = + viewFactory.createAdminIndexView( + KtorServletRequest(call), + current, + Admin(), + ) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + } - @GET - @Path("/javadoc") - fun javadoc( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - ): View { - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - return viewFactory.createJavadocAdminView(request) - } + get("/config") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val view = viewFactory.createConfigurationView(KtorServletRequest(call)) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } - @GET - @Path("/newChannel") - fun newChannel( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - ): View { - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - return viewFactory.createChannelEditView(request, Channel()) - } + get("/javadoc") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val view = viewFactory.createJavadocAdminView(KtorServletRequest(call)) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } - @GET - @Path("/editChannel/{channel}") - fun editChannel( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @PathParam("channel") channel: String, - ): View { - - // TODO redirect to / if channel is null - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - return viewFactory.createChannelEditView(request, channelDao.get(channel)!!) - } + get("/newChannel") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val view = + viewFactory.createChannelEditView(KtorServletRequest(call), Channel()) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } - @POST - @Path("/saveChannel") - fun saveChannel( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @FormParam("id") id: String?, - @FormParam("name") name: String, - @FormParam("key") key: String, - @FormParam("logged") logged: Boolean, - ): View { - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - val channel = - if (id == null) Channel(name, key, logged) else Channel(ObjectId(id), name, key, logged) - channelDao.save(channel) - return index(request, user) - } + get("/editChannel/{channel}") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val channelName = + call.parameters["channel"] + ?: run { + call.respond(HttpStatusCode.BadRequest) + return@get + } - @POST - @Path("/saveConfig") - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - fun saveConfig( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @FormParam("server") server: String, - @FormParam("url") url: String, - @FormParam("port") port: Int, - @FormParam("historyLength") historyLength: Int, - @FormParam("trigger") trigger: String, - @FormParam("nick") nick: String, - @FormParam("password") password: String, - @FormParam("throttleThreshold") throttleThreshold: Int, - @FormParam("minimumNickServAge") minimumNickServAge: Int, - ): View { - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - val config = configDao.get() - config.server = server - config.url = url - config.port = port - config.historyLength = historyLength - config.trigger = trigger - config.nick = nick - config.password = password - config.throttleThreshold = throttleThreshold - config.minimumNickServAge = minimumNickServAge - configDao.save(config) - return viewFactory.createConfigurationView(request) - } + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val channel = + channelDao.get(channelName) + ?: run { + call.respond(HttpStatusCode.NotFound) + return@get + } + val view = viewFactory.createChannelEditView(KtorServletRequest(call), channel) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } - @GET - @Path("/enableOperation/{name}") - fun enableOperation( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @PathParam("name") name: String, - ): View { - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - javabot.enableOperation(name) - return viewFactory.createConfigurationView(request) - } + post("/saveChannel") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@post + } + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@post + } - @GET - @Path("/disableOperation/{name}") - fun disableOperation( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @PathParam("name") name: String, - ): View { - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - javabot.disableOperation(name) - return viewFactory.createConfigurationView(request) - } + val params = call.receiveParameters() + val id = params["id"] + val name = params["name"] ?: "" + val key = params["key"] ?: "" + val logged = params["logged"]?.toBoolean() ?: false - @GET - @Path("/edit/{id}") - fun editAdmin( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @PathParam("id") id: String, - ): View { - val current = - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - - return viewFactory.createAdminIndexView(request, current, adminDao.find(ObjectId(id))) - } + val channel = + if (id == null) Channel(name, key, logged) + else Channel(ObjectId(id), name, key, logged) + channelDao.save(channel) - @GET - @Path("/delete/{id}") - fun deleteAdmin( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @PathParam("id") id: String, - ): View { - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - val admin = adminDao.find(ObjectId(id)) - if (admin != null && (!admin.botOwner)) { - adminDao.delete(admin) - } - return index(request, user) - } + val current = adminDao.getAdminByEmailAddress(user.email)!! + val view = + viewFactory.createAdminIndexView(KtorServletRequest(call), current, Admin()) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } - @POST - @Path("/add") - fun addAdmin( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @FormParam("ircName") ircName: String, - @FormParam("hostName") hostName: String, - @FormParam("emailAddress") emailAddress: String, - ): View { - var admin: Admin? = adminDao.getAdminByEmailAddress(emailAddress) - if (admin == null) { - admin = Admin(ircName, emailAddress, hostName, true) - } else { - admin.ircName = ircName - admin.hostName = hostName - admin.emailAddress = emailAddress - } - adminDao.save(admin) - return index(request, user) - } + post("/saveConfig") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@post + } + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@post + } - @POST - @Path("/addApi") - fun addApi( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @FormParam("name") name: String?, - @FormParam("groupId") groupId: String?, - @FormParam("artifactId") artifactId: String?, - @FormParam("version") version: String?, - ): View { - - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - version?.let { - val apiName = name ?: artifactId ?: throw WebApplicationException(400) - val api = JavadocApi(config, apiName, groupId ?: "", artifactId ?: "", version) - apiDao.save(api) - apiDao.save(ApiEvent.add(user.email, api)) - } + val params = call.receiveParameters() + val config = configDao.get() + config.server = params["server"] ?: config.server + config.url = params["url"] ?: config.url + config.port = params["port"]?.toIntOrNull() ?: config.port + config.historyLength = + params["historyLength"]?.toIntOrNull() ?: config.historyLength + config.trigger = params["trigger"] ?: config.trigger + config.nick = params["nick"] ?: config.nick + config.password = params["password"] ?: config.password + config.throttleThreshold = + params["throttleThreshold"]?.toIntOrNull() ?: config.throttleThreshold + config.minimumNickServAge = + params["minimumNickServAge"]?.toIntOrNull() ?: config.minimumNickServAge + configDao.save(config) - return javadoc(request, user) - } + val view = viewFactory.createConfigurationView(KtorServletRequest(call)) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + + get("/enableOperation/{name}") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val name = + call.parameters["name"] + ?: run { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + javabot.enableOperation(name) + val view = viewFactory.createConfigurationView(KtorServletRequest(call)) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + + get("/disableOperation/{name}") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val name = + call.parameters["name"] + ?: run { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + javabot.disableOperation(name) + val view = viewFactory.createConfigurationView(KtorServletRequest(call)) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + + get("/edit/{id}") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val id = + call.parameters["id"] + ?: run { + call.respond(HttpStatusCode.BadRequest) + return@get + } - @GET - @Path("/deleteApi/{id}") - fun deleteApi( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @PathParam("id") id: String, - ): View { - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - apiDao.delete(ObjectId(id)) - return javadoc(request, user) + val current = + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + + val view = + viewFactory.createAdminIndexView( + KtorServletRequest(call), + current, + adminDao.find(ObjectId(id)), + ) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + + get("/delete/{id}") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val id = + call.parameters["id"] + ?: run { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val admin = adminDao.find(ObjectId(id)) + if (admin != null && (!admin.botOwner)) { + adminDao.delete(admin) + } + + val current = adminDao.getAdminByEmailAddress(user.email)!! + val view = + viewFactory.createAdminIndexView(KtorServletRequest(call), current, Admin()) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + + post("/add") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@post + } + + val params = call.receiveParameters() + val ircName = params["ircName"] ?: "" + val hostName = params["hostName"] ?: "" + val emailAddress = params["emailAddress"] ?: "" + + var admin: Admin? = adminDao.getAdminByEmailAddress(emailAddress) + if (admin == null) { + admin = Admin(ircName, emailAddress, hostName, true) + } else { + admin.ircName = ircName + admin.hostName = hostName + admin.emailAddress = emailAddress + } + adminDao.save(admin) + + val current = adminDao.getAdminByEmailAddress(user.email)!! + val view = + viewFactory.createAdminIndexView(KtorServletRequest(call), current, Admin()) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + + post("/addApi") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@post + } + + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@post + } + + val params = call.receiveParameters() + val name = params["name"] + val groupId = params["groupId"] + val artifactId = params["artifactId"] + val version = params["version"] + + version?.let { + val apiName = + name + ?: artifactId + ?: run { + call.respond(HttpStatusCode.BadRequest) + return@post + } + val api = + JavadocApi(config, apiName, groupId ?: "", artifactId ?: "", version) + apiDao.save(api) + apiDao.save(ApiEvent.add(user.email, api)) + } + + val view = viewFactory.createJavadocAdminView(KtorServletRequest(call)) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + + get("/deleteApi/{id}") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val id = + call.parameters["id"] + ?: run { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + apiDao.delete(ObjectId(id)) + + val view = viewFactory.createJavadocAdminView(KtorServletRequest(call)) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + + get("/reloadApi/{id}") { + val user = + getAuthenticatedUser(call) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val id = + call.parameters["id"] + ?: run { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + adminDao.getAdminByEmailAddress(user.email) + ?: run { + call.respond(HttpStatusCode.Forbidden) + return@get + } + apiDao.find(ObjectId(id))?.let { apiDao.save(ApiEvent.reload(user.email, it)) } + + val view = viewFactory.createJavadocAdminView(KtorServletRequest(call)) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + } + } } - @GET - @Path("/reloadApi/{id}") - fun reloadApi( - @Context request: HttpServletRequest, - @Restricted(Authority.ROLE_ADMIN) user: User, - @PathParam("id") id: String, - ): View { - adminDao.getAdminByEmailAddress(user.email) ?: throw WebApplicationException(403) - apiDao.find(ObjectId(id))?.let { apiDao.save(ApiEvent.reload(user.email, it)) } - return javadoc(request, user) + private fun getAuthenticatedUser(call: ApplicationCall): User? { + // TODO: Implement proper authentication + return null } } diff --git a/src/main/kotlin/javabot/web/resources/BotResource.kt b/src/main/kotlin/javabot/web/resources/BotResource.kt index 81f94ffbc..3b1997ac4 100644 --- a/src/main/kotlin/javabot/web/resources/BotResource.kt +++ b/src/main/kotlin/javabot/web/resources/BotResource.kt @@ -1,6 +1,10 @@ package javabot.web.resources -import io.dropwizard.views.View +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.freemarker.* +import io.ktor.server.response.* +import io.ktor.server.routing.* import java.io.UnsupportedEncodingException import java.net.URLDecoder import java.time.LocalDate @@ -9,99 +13,76 @@ import java.time.format.DateTimeFormatter import javabot.model.Factoid import javabot.web.views.ViewFactory import javax.inject.Inject -import javax.servlet.http.HttpServletRequest -import javax.ws.rs.Consumes -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.Produces -import javax.ws.rs.QueryParam -import javax.ws.rs.core.Context -import javax.ws.rs.core.MediaType import org.slf4j.LoggerFactory -@Path("/") -@Consumes(MediaType.APPLICATION_JSON) -@Produces(MediaType.APPLICATION_JSON) class BotResource @Inject constructor(var viewFactory: ViewFactory) { - @GET - @Produces("text/html;charset=ISO-8859-1") - fun index(@Context request: HttpServletRequest): View { - if (request.getParameter("test.exception") != null) { - throw RuntimeException("Testing 500 pages") - } - return viewFactory.createIndexView(request) - } + fun configureRoutes(routing: Routing) { + with(routing) { + get("/") { + if (call.request.queryParameters["test.exception"] != null) { + throw RuntimeException("Testing 500 pages") + } + val view = viewFactory.createIndexView(KtorServletRequest(call)) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } - @GET - @Path("/index") - @Produces("text/html;charset=ISO-8859-1") - fun indexHtml(@Context request: HttpServletRequest): View { - return index(request) - } + get("/index") { + val view = viewFactory.createIndexView(KtorServletRequest(call)) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } - @GET - @Path("/factoids") - @Produces("text/html;charset=ISO-8859-1") - fun factoids( - @Context request: HttpServletRequest, - @QueryParam("page") page: Int?, - @QueryParam("name") name: String?, - @QueryParam("value") value: String?, - @QueryParam("userName") userName: String?, - ): View { - return viewFactory.createFactoidsView(request, page ?: 1, Factoid.of(name, value, userName)) - } + get("/factoids") { + val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1 + val name = call.request.queryParameters["name"] + val value = call.request.queryParameters["value"] + val userName = call.request.queryParameters["userName"] - @GET - @Path("/karma") - @Produces("text/html;charset=ISO-8859-1") - fun karma( - @Context request: HttpServletRequest, - @QueryParam("page") page: Int?, - @Suppress("UNUSED_PARAMETER") @QueryParam("name") name: String?, - @Suppress("UNUSED_PARAMETER") @QueryParam("value") value: Int?, - @Suppress("UNUSED_PARAMETER") @QueryParam("userName") userName: String?, - ): View { - return viewFactory.createKarmaView(request, page ?: 1) - } + val view = + viewFactory.createFactoidsView( + KtorServletRequest(call), + page, + Factoid.of(name, value, userName), + ) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } - @GET - @Path("/changes") - @Produces("text/html;charset=ISO-8859-1") - fun changes( - @Context request: HttpServletRequest, - @QueryParam("page") page: Int?, - @QueryParam("message") message: String?, - ): View { - return viewFactory.createChangesView(request, page ?: 1, message) - } + get("/karma") { + val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1 + val view = viewFactory.createKarmaView(KtorServletRequest(call), page) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } - @GET - @Path("/logs/{channel}/{date}") - @Produces("text/html;charset=ISO-8859-1") - fun logs( - @Context request: HttpServletRequest, - @PathParam("channel") channel: String?, - @PathParam("date") dateString: String?, - ): View { - val date: LocalDateTime = - try { - if ("today" == dateString) LocalDate.now().atStartOfDay() - else LocalDate.parse(dateString, FORMAT).atStartOfDay() - } catch (e: Exception) { - LocalDate.now().atStartOfDay() + get("/changes") { + val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1 + val message = call.request.queryParameters["message"] + val view = viewFactory.createChangesView(KtorServletRequest(call), page, message) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) } - val channelName: String - try { - channelName = URLDecoder.decode(channel, "UTF-8") - } catch (e: UnsupportedEncodingException) { - LOG.error(e.message, e) - throw RuntimeException(e.message, e) - } - return viewFactory.createLogsView(request, channelName, date) + get("/logs/{channel}/{date}") { + val channel = call.parameters["channel"] + val dateString = call.parameters["date"] + + val date: LocalDateTime = + try { + if ("today" == dateString) LocalDate.now().atStartOfDay() + else LocalDate.parse(dateString, FORMAT).atStartOfDay() + } catch (e: Exception) { + LocalDate.now().atStartOfDay() + } + val channelName: String + try { + channelName = URLDecoder.decode(channel, "UTF-8") + } catch (e: UnsupportedEncodingException) { + LOG.error(e.message, e) + throw RuntimeException(e.message, e) + } + + val view = viewFactory.createLogsView(KtorServletRequest(call), channelName, date) + call.respond(FreeMarkerContent("main.ftl", view.toModel())) + } + } } companion object { diff --git a/src/main/kotlin/javabot/web/resources/KtorServletRequest.kt b/src/main/kotlin/javabot/web/resources/KtorServletRequest.kt new file mode 100644 index 000000000..62972bd62 --- /dev/null +++ b/src/main/kotlin/javabot/web/resources/KtorServletRequest.kt @@ -0,0 +1,224 @@ +package javabot.web.resources + +import io.ktor.server.application.* +import io.ktor.server.request.* +import java.io.BufferedReader +import java.security.Principal +import java.util.* +import javax.servlet.* +import javax.servlet.http.* + +/** + * Adapter class to make Ktor ApplicationCall work like HttpServletRequest Only implements the + * methods used by the views + */ +class KtorServletRequest(private val call: ApplicationCall) : HttpServletRequest { + + override fun getCookies(): Array? { + val cookies = + call.request.cookies.rawCookies + .map { (name, value) -> Cookie(name, value) } + .toTypedArray() + return if (cookies.isEmpty()) null else cookies + } + + override fun getParameter(name: String?): String? { + return call.request.queryParameters[name] + } + + override fun getParameterMap(): MutableMap> { + return call.request.queryParameters + .entries() + .associate { it.key to it.value.toTypedArray() } + .toMutableMap() + } + + override fun getSession(): HttpSession { + return DummySession() + } + + override fun getSession(create: Boolean): HttpSession? { + return if (create) DummySession() else null + } + + // Minimal session implementation + private class DummySession : HttpSession { + private val attributes = mutableMapOf() + + override fun getAttribute(name: String?): Any? = attributes[name] + + override fun setAttribute(name: String?, value: Any?) { + attributes[name] = value + } + + override fun removeAttribute(name: String?) { + attributes.remove(name) + } + + override fun getAttributeNames(): Enumeration = + Collections.enumeration(attributes.keys) + + override fun getCreationTime(): Long = System.currentTimeMillis() + + override fun getId(): String = UUID.randomUUID().toString() + + override fun getLastAccessedTime(): Long = System.currentTimeMillis() + + override fun getMaxInactiveInterval(): Int = 3600 + + fun getServletContext(): ServletContext? = null + + override fun invalidate() {} + + override fun isNew(): Boolean = false + + override fun setMaxInactiveInterval(interval: Int) {} + + @Deprecated("Deprecated") override fun getSessionContext(): HttpSessionContext? = null + + @Deprecated("Deprecated") override fun getValue(name: String?): Any? = getAttribute(name) + + @Deprecated("Deprecated") + override fun getValueNames(): Array = attributes.keys.toTypedArray() + + @Deprecated("Deprecated") + override fun putValue(name: String?, value: Any?) = setAttribute(name, value) + + @Deprecated("Deprecated") override fun removeValue(name: String?) = removeAttribute(name) + } + + // Required methods - not all implemented + override fun getAuthType(): String? = null + + override fun getContextPath(): String = "" + + override fun getHeader(name: String?): String? = call.request.headers[name ?: ""] + + override fun getHeaderNames(): Enumeration = + Collections.enumeration(call.request.headers.names()) + + override fun getHeaders(name: String?): Enumeration = + Collections.enumeration(call.request.headers.getAll(name ?: "") ?: emptyList()) + + override fun getMethod(): String = call.request.httpMethod.value + + override fun getPathInfo(): String? = call.request.path() + + override fun getPathTranslated(): String? = null + + override fun getQueryString(): String? = call.request.queryString() + + override fun getRemoteUser(): String? = null + + override fun getRequestedSessionId(): String? = null + + override fun getRequestURI(): String = call.request.uri + + override fun getRequestURL(): StringBuffer = StringBuffer(call.request.origin.uri) + + override fun getServletPath(): String = "" + + override fun getUserPrincipal(): Principal? = null + + override fun isRequestedSessionIdFromCookie(): Boolean = false + + override fun isRequestedSessionIdFromURL(): Boolean = false + + @Deprecated("Deprecated") override fun isRequestedSessionIdFromUrl(): Boolean = false + + override fun isRequestedSessionIdValid(): Boolean = false + + override fun isUserInRole(role: String?): Boolean = false + + fun authenticate(response: HttpServletResponse?): Boolean = false + + fun changeSessionId(): String = "" + + override fun getIntHeader(name: String?): Int = -1 + + override fun getDateHeader(name: String?): Long = -1 + + fun login(username: String?, password: String?) {} + + fun logout() {} + + fun getParts(): MutableCollection = mutableListOf() + + fun getPart(name: String?): Part? = null + + override fun upgrade(handlerClass: Class?): T = TODO() + + override fun getAttribute(name: String?): Any? = null + + override fun getAttributeNames(): Enumeration = Collections.emptyEnumeration() + + override fun getCharacterEncoding(): String = "UTF-8" + + override fun getContentLength(): Int = -1 + + fun getContentLengthLong(): Long = -1 + + override fun getContentType(): String? = call.request.contentType().toString() + + override fun getInputStream(): ServletInputStream = TODO() + + fun getLocalAddr(): String = "" + + fun getLocalName(): String = "" + + fun getLocalPort(): Int = 0 + + override fun getLocale(): Locale = Locale.getDefault() + + override fun getLocales(): Enumeration = + Collections.enumeration(listOf(Locale.getDefault())) + + override fun getParameterNames(): Enumeration = + Collections.enumeration(call.request.queryParameters.names()) + + override fun getParameterValues(name: String?): Array? = + call.request.queryParameters.getAll(name)?.toTypedArray() + + override fun getProtocol(): String = "HTTP/1.1" + + override fun getReader(): BufferedReader = TODO() + + override fun getRealPath(path: String?): String? = null + + override fun getRemoteAddr(): String = call.request.local.remoteHost + + override fun getRemoteHost(): String = call.request.local.remoteHost + + fun getRemotePort(): Int = call.request.local.remotePort + + override fun getRequestDispatcher(path: String?): RequestDispatcher? = null + + override fun getScheme(): String = call.request.origin.scheme + + override fun getServerName(): String = call.request.local.serverHost + + override fun getServerPort(): Int = call.request.local.serverPort + + fun getServletContext(): ServletContext? = null + + fun isAsyncStarted(): Boolean = false + + fun isAsyncSupported(): Boolean = false + + override fun isSecure(): Boolean = call.request.origin.scheme == "https" + + override fun removeAttribute(name: String?) {} + + override fun setAttribute(name: String?, o: Any?) {} + + override fun setCharacterEncoding(env: String?) {} + + fun startAsync(): AsyncContext = TODO() + + fun startAsync( + servletRequest: ServletRequest?, + servletResponse: ServletResponse?, + ): AsyncContext = TODO() + + fun getDispatcherType(): DispatcherType = DispatcherType.REQUEST +} diff --git a/src/main/kotlin/javabot/web/resources/PublicErrorResource.kt b/src/main/kotlin/javabot/web/resources/PublicErrorResource.kt index 85c0678ea..13523f2b4 100644 --- a/src/main/kotlin/javabot/web/resources/PublicErrorResource.kt +++ b/src/main/kotlin/javabot/web/resources/PublicErrorResource.kt @@ -1,6 +1,5 @@ package javabot.web.resources -import io.dropwizard.views.View import java.util.Random import javabot.web.views.ErrorView @@ -10,15 +9,15 @@ class PublicErrorResource { private val IMAGE_404 = arrayOf("404_1.gif", "404_2.gif", "404_3.gif", "404_4.gif") private val IMAGE_500 = arrayOf("500.gif") - fun view403(): View { + fun view403(): ErrorView { return ErrorView("/error/403.ftl", getRandomImage(IMAGE_403)) } - fun view404(): View { + fun view404(): ErrorView { return ErrorView("/error/404.ftl", getRandomImage(IMAGE_404)) } - fun view500(): View { + fun view500(): ErrorView { return ErrorView("/error/500.ftl", getRandomImage(IMAGE_500)) } diff --git a/src/main/kotlin/javabot/web/resources/PublicOAuthResource.kt b/src/main/kotlin/javabot/web/resources/PublicOAuthResource.kt index 717dfff68..d03a8b7b6 100644 --- a/src/main/kotlin/javabot/web/resources/PublicOAuthResource.kt +++ b/src/main/kotlin/javabot/web/resources/PublicOAuthResource.kt @@ -1,112 +1,106 @@ package javabot.web.resources import com.antwerkz.sofia.Sofia -import com.codahale.metrics.annotation.Timed -import com.google.common.base.Optional +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.sessions.* import java.net.URI -import java.net.URISyntaxException import java.util.UUID import javabot.dao.AdminDao import javabot.model.Admin import javabot.web.JavabotConfiguration +import javabot.web.UserSession import javabot.web.model.Authority.ROLE_ADMIN import javabot.web.model.Authority.ROLE_PUBLIC import javabot.web.model.InMemoryUserCache.INSTANCE import javabot.web.model.User import javax.inject.Inject -import javax.servlet.http.HttpServletRequest -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.WebApplicationException -import javax.ws.rs.core.Context -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.NewCookie -import javax.ws.rs.core.Response -import javax.ws.rs.core.Response.Status.BAD_REQUEST -import javax.ws.rs.core.Response.Status.UNAUTHORIZED import org.brickred.socialauth.SocialAuthConfig import org.brickred.socialauth.SocialAuthManager -import org.brickred.socialauth.util.SocialAuthUtil import org.slf4j.LoggerFactory -@Path("/auth") -@Produces(MediaType.TEXT_HTML) class PublicOAuthResource @Inject constructor(var adminDao: AdminDao) { var configuration: JavabotConfiguration? = null - @GET - @Path("/login") - @Throws(URISyntaxException::class) - fun requestOAuth(@Context request: HttpServletRequest): Response { - val oauthCfg = configuration!!.OAuthCfg - if (oauthCfg != null) { - try { - val manager = getSocialAuthManager() - - request.session.setAttribute(AUTH_MANAGER, manager) - - val uri = - URI( - manager?.getAuthenticationUrl("googleplus", configuration!!.OAuthSuccessUrl) - ) - return Response.temporaryRedirect(uri).build() - } catch (e: Exception) { - log.error(e.message, e) + fun configureRoutes(routing: Routing) { + with(routing) { + get("/auth/login") { + val oauthCfg = configuration!!.OAuthCfg + if (oauthCfg != null) { + try { + val manager = getSocialAuthManager() + + // Store manager in session + call.sessions.set(UserSession(AUTH_MANAGER)) + + val uri = + URI( + manager?.getAuthenticationUrl( + "googleplus", + configuration!!.OAuthSuccessUrl, + ) + ) + call.respondRedirect(uri.toString()) + return@get + } catch (e: Exception) { + log.error(e.message, e) + } + } + call.respond(HttpStatusCode.BadRequest) } - } - throw WebApplicationException(BAD_REQUEST) - } - - /** - * Handles the OAuth server response to the earlier AuthRequest - * - * @return The OAuth identifier for this user if verification was successful - */ - @GET - @Timed - @Path("/verify") - fun verifyOAuthServerResponse(@Context request: HttpServletRequest): Response { - val manager = request.session.getAttribute(AUTH_MANAGER) as SocialAuthManager - - try { - val params = SocialAuthUtil.getRequestParametersMap(request) - val provider = manager.connect(params) - - val p = provider.userProfile - - Sofia.loggingInUser(p) - - var tempUser = User(UUID.randomUUID(), p.email, p.validatedId, provider.accessGrant) - tempUser.authorities.add(ROLE_PUBLIC) - val user = INSTANCE.getByOpenIDIdentifier(tempUser.openIDIdentifier) - if (user == null) { - val admin = adminDao.getAdminByEmailAddress(tempUser.email) - if (admin != null) { - tempUser.authorities.add(ROLE_ADMIN) - } else { - if (adminDao.count() == 0L) { - adminDao.save(Admin(tempUser.email)) - tempUser.authorities.add(ROLE_ADMIN) + get("/auth/verify") { + // TODO: Retrieve manager from session + val manager = + getSocialAuthManager() + ?: run { + call.respond(HttpStatusCode.Unauthorized) + return@get + } + + try { + val params = mutableMapOf() + call.request.queryParameters.entries().forEach { entry -> + params[entry.key] = entry.value.firstOrNull() ?: "" } + val provider = manager.connect(params) + + val p = provider.userProfile + + Sofia.loggingInUser(p) + + var tempUser = + User(UUID.randomUUID(), p.email, p.validatedId, provider.accessGrant) + tempUser.authorities.add(ROLE_PUBLIC) + + val user = INSTANCE.getByOpenIDIdentifier(tempUser.openIDIdentifier) + if (user == null) { + val admin = adminDao.getAdminByEmailAddress(tempUser.email) + if (admin != null) { + tempUser.authorities.add(ROLE_ADMIN) + } else { + if (adminDao.count() == 0L) { + adminDao.save(Admin(tempUser.email)) + tempUser.authorities.add(ROLE_ADMIN) + } + } + INSTANCE.put(tempUser) + } else { + tempUser = user + } + + call.sessions.set(UserSession(tempUser.sessionToken.toString())) + call.respondRedirect("/") + } catch (e: Exception) { + e.printStackTrace() + log.error(e.message, e) + call.respond(HttpStatusCode.Unauthorized) } - INSTANCE.put(tempUser) - } else { - tempUser = user } - - return Response.temporaryRedirect(URI("/")) - .cookie(replaceSessionTokenCookie(Optional.of(tempUser))) - .build() - } catch (e: Exception) { - e.printStackTrace() - log.error(e.message, e) } - - // Must have failed to be here - throw WebApplicationException(UNAUTHORIZED) } /** @return Get an initialized SocialAuthManager */ @@ -124,34 +118,6 @@ class PublicOAuthResource @Inject constructor(var adminDao: AdminDao) { return null } - protected fun replaceSessionTokenCookie(user: Optional): NewCookie { - if (user.isPresent) { - val value = user.get().sessionToken.toString() - log.debug("Replacing session token with {}", value) - return NewCookie( - JavabotConfiguration.SESSION_TOKEN_NAME, - value, - "/", - null, - null, - 86400 * 30, - false, - ) - } else { - // Remove the session token cookie - log.debug("Removing session token") - return NewCookie( - JavabotConfiguration.SESSION_TOKEN_NAME, - null, - null, - null, - null, - 0, - false, - ) - } - } - companion object { private val log = LoggerFactory.getLogger(PublicOAuthResource::class.java) diff --git a/src/main/kotlin/javabot/web/views/AdminIndexView.kt b/src/main/kotlin/javabot/web/views/AdminIndexView.kt index 51f644760..7ec57ec1c 100644 --- a/src/main/kotlin/javabot/web/views/AdminIndexView.kt +++ b/src/main/kotlin/javabot/web/views/AdminIndexView.kt @@ -28,4 +28,9 @@ constructor( override fun getChildView(): String { return "admin/index.ftl" } + + override fun toModel(): Map { + return super.toModel() + + mapOf("current" to current, "editing" to editing, "admins" to getAdmins()) + } } diff --git a/src/main/kotlin/javabot/web/views/ChangesView.kt b/src/main/kotlin/javabot/web/views/ChangesView.kt index b8e0d7a5f..927e3ef96 100644 --- a/src/main/kotlin/javabot/web/views/ChangesView.kt +++ b/src/main/kotlin/javabot/web/views/ChangesView.kt @@ -46,4 +46,8 @@ constructor( date, ) } + + override fun toModel(): Map { + return super.toModel() + mapOf("message" to message, "date" to date) + } } diff --git a/src/main/kotlin/javabot/web/views/ChannelEditView.kt b/src/main/kotlin/javabot/web/views/ChannelEditView.kt index 3139d1472..b272f5d9b 100644 --- a/src/main/kotlin/javabot/web/views/ChannelEditView.kt +++ b/src/main/kotlin/javabot/web/views/ChannelEditView.kt @@ -23,4 +23,8 @@ constructor( override fun getChildView(): String { return "admin/editChannel.ftl" } + + override fun toModel(): Map { + return super.toModel() + mapOf("channel" to channel) + } } diff --git a/src/main/kotlin/javabot/web/views/ConfigurationView.kt b/src/main/kotlin/javabot/web/views/ConfigurationView.kt index da91e42e2..8f88f3cd6 100644 --- a/src/main/kotlin/javabot/web/views/ConfigurationView.kt +++ b/src/main/kotlin/javabot/web/views/ConfigurationView.kt @@ -46,4 +46,13 @@ constructor( override fun getChildView(): String { return "admin/configuration.ftl" } + + override fun toModel(): Map { + return super.toModel() + + mapOf( + "configuration" to configuration, + "operations" to operations(), + "currentOps" to getCurrentOps(), + ) + } } diff --git a/src/main/kotlin/javabot/web/views/ErrorView.kt b/src/main/kotlin/javabot/web/views/ErrorView.kt index c26d4173d..40b3804e4 100644 --- a/src/main/kotlin/javabot/web/views/ErrorView.kt +++ b/src/main/kotlin/javabot/web/views/ErrorView.kt @@ -1,7 +1,7 @@ package javabot.web.views -import com.google.common.base.Charsets -import io.dropwizard.views.View - -class ErrorView(template: String, val image: String) : - View(template, com.google.common.base.Charsets.ISO_8859_1) +class ErrorView(val template: String, val image: String) { + fun toModel(): Map { + return mapOf("image" to image) + } +} diff --git a/src/main/kotlin/javabot/web/views/FactoidsView.kt b/src/main/kotlin/javabot/web/views/FactoidsView.kt index dba3e31a2..82e379fb1 100644 --- a/src/main/kotlin/javabot/web/views/FactoidsView.kt +++ b/src/main/kotlin/javabot/web/views/FactoidsView.kt @@ -71,6 +71,10 @@ constructor( return "/factoids.ftl" } + override fun toModel(): Map { + return super.toModel() + mapOf("filter" to getFilter()) + } + companion object { private val LOG = LoggerFactory.getLogger(FactoidsView::class.java) } diff --git a/src/main/kotlin/javabot/web/views/IndexView.kt b/src/main/kotlin/javabot/web/views/IndexView.kt index 399aa0696..58c40bdb1 100644 --- a/src/main/kotlin/javabot/web/views/IndexView.kt +++ b/src/main/kotlin/javabot/web/views/IndexView.kt @@ -21,4 +21,8 @@ constructor( override fun getChildView(): String { return "/index.ftl" } + + override fun toModel(): Map { + return super.toModel() + } } diff --git a/src/main/kotlin/javabot/web/views/JavadocAdminView.kt b/src/main/kotlin/javabot/web/views/JavadocAdminView.kt index da5480137..576055b03 100644 --- a/src/main/kotlin/javabot/web/views/JavadocAdminView.kt +++ b/src/main/kotlin/javabot/web/views/JavadocAdminView.kt @@ -26,4 +26,8 @@ constructor( fun apis(): List { return apiDao.findAll() } + + override fun toModel(): Map { + return super.toModel() + mapOf("apis" to apis()) + } } diff --git a/src/main/kotlin/javabot/web/views/LogsView.kt b/src/main/kotlin/javabot/web/views/LogsView.kt index 826be0a4e..a25fd43c8 100644 --- a/src/main/kotlin/javabot/web/views/LogsView.kt +++ b/src/main/kotlin/javabot/web/views/LogsView.kt @@ -52,4 +52,16 @@ constructor( override fun getChildView(): String { return "logs.ftl" } + + override fun toModel(): Map { + return super.toModel() + + mapOf( + "channel" to channel, + "date" to date, + "today" to today, + "yesterday" to yesterday, + "tomorrow" to tomorrow, + "logs" to logs(), + ) + } } diff --git a/src/main/kotlin/javabot/web/views/MainView.kt b/src/main/kotlin/javabot/web/views/MainView.kt index 29211ca50..b74414bab 100644 --- a/src/main/kotlin/javabot/web/views/MainView.kt +++ b/src/main/kotlin/javabot/web/views/MainView.kt @@ -1,7 +1,6 @@ package javabot.web.views import com.antwerkz.sofia.Sofia -import io.dropwizard.views.View import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.ArrayList @@ -22,7 +21,7 @@ abstract class MainView( var factoidDao: FactoidDao, var apiDao: ApiDao, val request: HttpServletRequest, -) : View("/main.ftl", com.google.common.base.Charsets.ISO_8859_1) { +) { companion object { val DATE_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd hh:mm") @@ -89,4 +88,20 @@ abstract class MainView( open fun format(date: LocalDateTime?): String { return if (date != null) DATE_TIME_FORMATTER.format(date) else "" } + + /** Convert this view to a model map for FreeMarker */ + open fun toModel(): Map { + return mapOf( + "sofia" to sofia(), + "factoidCount" to getFactoidCount(), + "loggedIn" to loggedIn(), + "isAdmin" to isAdmin(), + "currentChannel" to getCurrentChannel(), + "channels" to getChannels(), + "apis" to getAPIs(), + "errors" to getErrors(), + "hasErrors" to hasErrors(), + "childView" to getChildView(), + ) + } } diff --git a/src/main/kotlin/javabot/web/views/PagedView.kt b/src/main/kotlin/javabot/web/views/PagedView.kt index b78713d2c..b01374392 100644 --- a/src/main/kotlin/javabot/web/views/PagedView.kt +++ b/src/main/kotlin/javabot/web/views/PagedView.kt @@ -81,6 +81,22 @@ abstract class PagedView( abstract fun getPageItems(): List + override fun toModel(): Map { + return super.toModel() + + mapOf( + "page" to getPage(), + "pageCount" to getPageCount(), + "itemsPerPage" to getItemsPerPage(), + "itemCount" to itemCount, + "nextPage" to getNextPage(), + "previousPage" to getPreviousPage(), + "startRange" to getStartRange(), + "endRange" to getEndRange(), + "pageItems" to getPageItems(), + "pagedView" to getPagedView(), + ) + } + companion object { val ITEMS_PER_PAGE: Int = 50 } diff --git a/src/main/resources/main.ftl b/src/main/resources/main.ftl index 3ce8a3522..5000af4c4 100644 --- a/src/main/resources/main.ftl +++ b/src/main/resources/main.ftl @@ -30,12 +30,12 @@ - <#if !loggedIn()> + <#if !loggedIn> - <#if isAdmin()> + <#if isAdmin>

Admin

    @@ -50,7 +50,7 @@ - <#if isAdmin()> + <#if isAdmin> @@ -61,11 +61,11 @@
    Channels +
    - <#list getChannels() as channel> + <#list channels as channel>
    - + class='current' >${channel.name} @@ -79,7 +79,7 @@
    - <#list getAPIs() as api> + <#list apis as api>
    ${api.name} @@ -107,7 +107,11 @@
    - <#include getChildView() > + <#if pagedView??> + <#include pagedView > + <#elseif childView??> + <#include childView > +