diff --git a/api/build.gradle.kts b/api/build.gradle.kts index d83c709..409bcd6 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -10,7 +10,6 @@ dependencies { api(project(":core")) implementation(project(":storage")) api("io.ktor:ktor-server-core:$ktorVersion") - api("io.ktor:ktor-auth:$ktorVersion") api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") diff --git a/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt b/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt new file mode 100644 index 0000000..c73667f --- /dev/null +++ b/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt @@ -0,0 +1,58 @@ +package com.wbrawner.twigs + +import com.wbrawner.twigs.model.Budget +import com.wbrawner.twigs.model.Permission +import com.wbrawner.twigs.storage.BudgetRepository +import com.wbrawner.twigs.storage.PermissionRepository +import com.wbrawner.twigs.storage.Session +import io.ktor.application.* +import io.ktor.auth.* +import io.ktor.http.* +import io.ktor.response.* +import io.ktor.util.pipeline.* + +suspend inline fun PipelineContext.requireBudgetWithPermission( + permissionRepository: PermissionRepository, + userId: String, + budgetId: String, + permission: Permission, + otherwise: () -> Unit +) { + permissionRepository.findAll( + userId = userId, + budgetIds = listOf(budgetId) + ).firstOrNull { + it.permission.isAtLeast(permission) + } ?: run { + errorResponse(HttpStatusCode.Forbidden, "Insufficient permissions on budget $budgetId") + otherwise() + } +} + +suspend fun PipelineContext.budgetWithPermission( + budgetRepository: BudgetRepository, + permissionRepository: PermissionRepository, + budgetId: String, + permission: Permission, + block: suspend (Budget) -> Unit +) { + val session = call.principal()!! + val userPermission = permissionRepository.findAll( + userId = session.userId, + budgetIds = listOf(budgetId) + ).firstOrNull() + if (userPermission?.permission?.isNotAtLeast(permission) != true) { + errorResponse(HttpStatusCode.Forbidden) + return + } + block(budgetRepository.findAllByIds(listOf(budgetId)).first()) +} + +suspend inline fun PipelineContext.errorResponse( + httpStatusCode: HttpStatusCode = HttpStatusCode.NotFound, + message: String? = null +) { + message?.let { + call.respond(httpStatusCode, ErrorResponse(message)) + }?: call.respond(httpStatusCode) +} diff --git a/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt index 5c64091..c9ca460 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt @@ -5,35 +5,18 @@ import com.wbrawner.twigs.model.Permission import com.wbrawner.twigs.model.UserPermission import com.wbrawner.twigs.storage.BudgetRepository import com.wbrawner.twigs.storage.PermissionRepository +import com.wbrawner.twigs.storage.Session import io.ktor.application.* import io.ktor.auth.* import io.ktor.http.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* -import io.ktor.util.pipeline.* fun Application.budgetRoutes( budgetRepository: BudgetRepository, permissionRepository: PermissionRepository ) { - suspend fun PipelineContext.budgetWithPermission( - budgetId: String, - permission: Permission, - block: suspend (Budget) -> Unit - ) { - val session = call.principal()!! - val userPermission = permissionRepository.findAll( - userId = session.userId, - budgetIds = listOf(budgetId) - ).firstOrNull() - if (userPermission?.permission?.isNotAtLeast(permission) != true) { - call.respond(HttpStatusCode.Forbidden) - return - } - block(budgetRepository.findAllByIds(listOf(budgetId)).first()) - } - routing { route("/api/budgets") { authenticate(optional = false) { @@ -47,7 +30,7 @@ fun Application.budgetRoutes( } get("/{id}") { - budgetWithPermission(budgetId = call.parameters["id"]!!, Permission.READ) { budget -> + budgetWithPermission(budgetRepository, permissionRepository, call.parameters["id"]!!, Permission.READ) { budget -> val users = permissionRepository.findAll(budgetIds = listOf(budget.id)) call.respond(BudgetResponse(budget, users)) } @@ -57,7 +40,7 @@ fun Application.budgetRoutes( val session = call.principal()!! val request = call.receive() if (request.name.isNullOrBlank()) { - call.respond(HttpStatusCode.BadRequest, "Name cannot be empty or null") + errorResponse(HttpStatusCode.BadRequest, "Name cannot be empty or null") return@post } val budget = budgetRepository.save( @@ -90,7 +73,7 @@ fun Application.budgetRoutes( } put("/{id}") { - budgetWithPermission(call.parameters["id"]!!, Permission.MANAGE) { budget -> + budgetWithPermission(budgetRepository, permissionRepository, call.parameters["id"]!!, Permission.MANAGE) { budget -> val request = call.receive() val name = request.name ?: budget.name val description = request.description ?: budget.description @@ -112,7 +95,7 @@ fun Application.budgetRoutes( } delete("/{id}") { - budgetWithPermission(budgetId = call.parameters["id"]!!, Permission.OWNER) { budget -> + budgetWithPermission(budgetRepository, permissionRepository, budgetId = call.parameters["id"]!!, Permission.OWNER) { budget -> budgetRepository.delete(budget) call.respond(HttpStatusCode.NoContent) } diff --git a/api/src/main/kotlin/com/wbrawner/twigs/CategoryApi.kt b/api/src/main/kotlin/com/wbrawner/twigs/CategoryApi.kt index 91f1c63..8990cce 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/CategoryApi.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/CategoryApi.kt @@ -25,7 +25,7 @@ data class CategoryResponse( category.title, category.description, category.amount, - category.budget!!.id, + category.budgetId!!, category.expense, category.archived ) diff --git a/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt index c109b93..96bec11 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt @@ -4,13 +4,13 @@ import com.wbrawner.twigs.model.Category import com.wbrawner.twigs.model.Permission import com.wbrawner.twigs.storage.CategoryRepository import com.wbrawner.twigs.storage.PermissionRepository +import com.wbrawner.twigs.storage.Session import io.ktor.application.* import io.ktor.auth.* import io.ktor.http.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* -import io.ktor.util.pipeline.* fun Application.categoryRoutes( categoryRepository: CategoryRepository, @@ -45,11 +45,11 @@ fun Application.categoryRoutes( val session = call.principal()!! val request = call.receive() if (request.title.isNullOrBlank()) { - call.respond(HttpStatusCode.BadRequest, ErrorResponse("Title cannot be null or empty")) + errorResponse(HttpStatusCode.BadRequest, "Title cannot be null or empty") return@post } if (request.budgetId.isNullOrBlank()) { - call.respond(HttpStatusCode.BadRequest, ErrorResponse("Budget ID cannot be null or empty")) + errorResponse(HttpStatusCode.BadRequest, "Budget ID cannot be null or empty") return@post } requireBudgetWithPermission( @@ -111,7 +111,7 @@ fun Application.categoryRoutes( val category = categoryRepository.findAll(ids = call.parameters.getAll("id")) .firstOrNull() ?: run { - call.respond(HttpStatusCode.NotFound) + errorResponse(HttpStatusCode.NotFound) return@delete } requireBudgetWithPermission( @@ -128,21 +128,3 @@ fun Application.categoryRoutes( } } } - -suspend inline fun PipelineContext.requireBudgetWithPermission( - permissionRepository: PermissionRepository, - userId: String, - budgetId: String, - permission: Permission, - otherwise: () -> Unit -) { - permissionRepository.findAll( - userId = userId, - budgetIds = listOf(budgetId) - ).firstOrNull { - it.permission.isAtLeast(permission) - } ?: run { - call.respond(HttpStatusCode.Forbidden, "Insufficient permissions on budget $budgetId") - otherwise() - } -} diff --git a/api/src/main/kotlin/com/wbrawner/twigs/UserApi.kt b/api/src/main/kotlin/com/wbrawner/twigs/UserApi.kt index cb6ce0c..e3f4483 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/UserApi.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/UserApi.kt @@ -2,25 +2,18 @@ package com.wbrawner.twigs import com.wbrawner.twigs.model.Permission import com.wbrawner.twigs.model.User +import com.wbrawner.twigs.storage.Session import java.util.* -data class NewUserRequest( - val username: String, - val password: String, - val email: String? = null -) - -data class UpdateUserRequest( +data class UserRequest( val username: String? = null, val password: String? = null, val email: String? = null ) -data class LoginRequest(val username: String? = null, val password: String? = null) +data class LoginRequest(val username: String, val password: String) -data class UserResponse(val id: String, val username: String, val email: String?) { - constructor(user: User) : this(user.id, user.name, user.email) -} +data class UserResponse(val id: String, val username: String, val email: String?) data class UserPermissionRequest( val user: String, @@ -29,13 +22,15 @@ data class UserPermissionRequest( data class UserPermissionResponse(val user: String, val permission: Permission?) -data class SessionResponse(val token: String, val expiration: String) { - constructor(session: Session) : this(session.token, session.expiration.toInstant().toString()) -} +data class SessionResponse(val token: String, val expiration: String) data class PasswordResetRequest( val userId: Long, val id: String = randomString(), private val date: Calendar = GregorianCalendar(), private val token: String = randomString() -) \ No newline at end of file +) + +fun User.asResponse(): UserResponse = UserResponse(id, name, email) + +fun Session.asResponse(): SessionResponse = SessionResponse(token, expiration.toInstant().toString()) \ No newline at end of file diff --git a/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt new file mode 100644 index 0000000..43607c1 --- /dev/null +++ b/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt @@ -0,0 +1,139 @@ +package com.wbrawner.twigs + +import com.wbrawner.twigs.model.User +import com.wbrawner.twigs.storage.PermissionRepository +import com.wbrawner.twigs.storage.Session +import com.wbrawner.twigs.storage.SessionRepository +import com.wbrawner.twigs.storage.UserRepository +import io.ktor.application.* +import io.ktor.auth.* +import io.ktor.http.* +import io.ktor.request.* +import io.ktor.response.* +import io.ktor.routing.* +import io.ktor.sessions.* + +fun Application.userRoutes( + permissionRepository: PermissionRepository, + sessionRepository: SessionRepository, + userRepository: UserRepository +) { + routing { + route("/api/users") { + post("/login") { + val request = call.receive() + val user = + userRepository.find(name = request.username, password = request.password.hash()).firstOrNull() + ?: userRepository.find(email = request.username, password = request.password.hash()) + .firstOrNull() + ?: run { + errorResponse(HttpStatusCode.Unauthorized, "Invalid credentials") + return@post + } + val session = sessionRepository.save(Session(userId = user.id)) + call.sessions.set(session) + call.respond(session.asResponse()) + } + + post("/register") { + val request = call.receive() + if (request.username.isNullOrBlank()) { + errorResponse(HttpStatusCode.BadRequest, "Username must not be null or blank") + return@post + } + if (request.password.isNullOrBlank()) { + errorResponse(HttpStatusCode.BadRequest, "Password must not be null or blank") + return@post + } + call.respond( + userRepository.save( + User( + name = request.username, + password = request.password, + email = request.email + ) + ).asResponse() + ) + } + + authenticate(optional = false) { + get("/") { + val query = call.request.queryParameters.getAll("query") + if (query?.firstOrNull()?.isNotBlank() == true) { + call.respond(userRepository.findAll(nameLike = query.first()).map{ it.asResponse() }) + return@get + } + permissionRepository.findAll( + budgetIds = call.request.queryParameters.getAll("budgetId") + ).mapNotNull { + userRepository.findAll(ids = listOf(it.userId)) + .firstOrNull() + ?.asResponse() + }.run { call.respond(this) } + } + + get("/{id}") { + userRepository.findAll(ids = call.parameters.getAll("id")) + .firstOrNull() + ?.asResponse() + ?: errorResponse(HttpStatusCode.NotFound) + } + + post("/") { + val request = call.receive() + if (request.username.isNullOrBlank()) { + errorResponse(HttpStatusCode.BadRequest, "Username must not be null or blank") + return@post + } + if (request.password.isNullOrBlank()) { + errorResponse(HttpStatusCode.BadRequest, "Password must not be null or blank") + return@post + } + call.respond( + userRepository.save( + User( + name = request.username, + password = request.password, + email = request.email + ) + ).asResponse() + ) + } + + put("/{id}") { + val session = call.principal()!! + val request = call.receive() + // TODO: Add some kind of admin denotation to allow admins to edit other users + if (call.parameters["id"] != session.userId) { + errorResponse(HttpStatusCode.Forbidden) + return@put + } + call.respond( + userRepository.save( + userRepository.findAll(ids = call.parameters.getAll("id")) + .first() + .run { + copy( + name = request.username ?: name, + password = request.password?.hash() ?: password, + email = request.email ?: email + ) + } + ).asResponse() + ) + } + + delete("/{id}") { + val session = call.principal()!! + // TODO: Add some kind of admin denotation to allow admins to delete other users + if (call.parameters["id"] != session.userId) { + errorResponse(HttpStatusCode.Forbidden) + return@delete + } + userRepository.deleteById(session.userId) + call.respond(HttpStatusCode.NoContent) + } + } + } + } +} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5bfbda7..b7c219a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") implementation("io.ktor:ktor-server-core:$ktorVersion") implementation("io.ktor:ktor-server-cio:$ktorVersion") + implementation("io.ktor:ktor-server-sessions:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.springframework.boot:spring-boot-starter-data-jpa") diff --git a/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt b/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt index f6cba83..03375d0 100644 --- a/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt +++ b/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt @@ -2,14 +2,52 @@ package com.wbrawner.twigs.server import com.wbrawner.twigs.budgetRoutes import com.wbrawner.twigs.categoryRoutes +import com.wbrawner.twigs.storage.* +import com.wbrawner.twigs.twoWeeksFromNow +import com.wbrawner.twigs.userRoutes import io.ktor.application.* import io.ktor.auth.* +import io.ktor.sessions.* +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.time.Duration +import kotlin.time.ExperimentalTime fun main(args: Array): Unit = io.ktor.server.cio.EngineMain.main(args) -fun Application.module(budgetReposi) { - - install(Authentication) - budgetRoutes() - categoryRoutes() +@ExperimentalTime +fun Application.module( + budgetRepository: BudgetRepository, + categoryRepository: CategoryRepository, + permissionRepository: PermissionRepository, + sessionRepository: SessionRepository, + userRepository: UserRepository +) { + install(Sessions) { + header("Authorization") + } + install(Authentication) { + session { + validate { token -> + val session = sessionRepository.findAll(token).firstOrNull() + ?: return@validate null + return@validate if (twoWeeksFromNow.after(session.expiration)) { + session + } else { + null + } + } + } + } + budgetRoutes(budgetRepository, permissionRepository) + categoryRoutes(categoryRepository, permissionRepository) + userRoutes(permissionRepository, sessionRepository, userRepository) + launch { + while (currentCoroutineContext().isActive) { + delay(Duration.hours(24)) + sessionRepository.deleteExpired() + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/wbrawner/twigs/server/session/SessionCleanupTask.kt b/app/src/main/kotlin/com/wbrawner/twigs/server/session/SessionCleanupTask.kt deleted file mode 100644 index 9e83bfb..0000000 --- a/app/src/main/kotlin/com/wbrawner/twigs/server/session/SessionCleanupTask.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.wbrawner.twigs.server.session - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.scheduling.annotation.Scheduled -import org.springframework.stereotype.Component -import java.util.* - -@Component -open class SessionCleanupTask @Autowired constructor(private val sessionRepository: UserSessionRepository) { - @Scheduled(cron = "0 0 * * * *") - open fun cleanup() { - sessionRepository.deleteAllByExpirationBefore(Date()) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/wbrawner/twigs/server/user/UserController.kt b/app/src/main/kotlin/com/wbrawner/twigs/server/user/UserController.kt deleted file mode 100644 index 9a153c5..0000000 --- a/app/src/main/kotlin/com/wbrawner/twigs/server/user/UserController.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.wbrawner.twigs.server.user - -import com.wbrawner.twigs.ErrorResponse -import com.wbrawner.twigs.server.budget.BudgetRepository -import com.wbrawner.twigs.server.currentUser -import com.wbrawner.twigs.server.permission.UserPermission -import com.wbrawner.twigs.server.permission.UserPermissionRepository -import com.wbrawner.twigs.server.permission.UserPermissionResponse -import com.wbrawner.twigs.server.session.Session -import com.wbrawner.twigs.server.session.SessionResponse -import com.wbrawner.twigs.server.session.UserSessionRepository -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.authentication.dao.DaoAuthenticationProvider -import org.springframework.security.core.Authentication -import org.springframework.security.core.AuthenticationException -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.web.bind.annotation.* -import javax.transaction.Transactional - -@RestController -@RequestMapping("/users") -@Transactional -open class UserController @Autowired constructor( - private val budgetRepository: BudgetRepository, - private val userRepository: UserRepository, - private val userSessionRepository: UserSessionRepository, - private val passwordEncoder: PasswordEncoder, - private val userPermissionsRepository: UserPermissionRepository, - private val authenticationProvider: DaoAuthenticationProvider -) { - @GetMapping(path = [""], produces = [MediaType.APPLICATION_JSON_VALUE]) - open fun getUsers(budgetId: String): ResponseEntity> { - val budget = budgetRepository.findById(budgetId).orElse(null) - ?: return ResponseEntity.notFound().build() - val userPermissions = userPermissionsRepository.findAllByBudget(budget, null) - val userInBudget = userPermissions.stream() - .anyMatch { userPermission: UserPermission -> userPermission.user!!.id == currentUser!!.id } - return if (!userInBudget) { - ResponseEntity.notFound().build() - } else ResponseEntity.ok( - userPermissions.map { userPermission: UserPermission -> UserPermissionResponse(userPermission) } - ) - } - - @PostMapping(path = ["/login"], produces = [MediaType.APPLICATION_JSON_VALUE]) - open fun login(@RequestBody request: LoginRequest): ResponseEntity { - val authReq = UsernamePasswordAuthenticationToken(request.username, request.password) - val auth: Authentication - auth = try { - authenticationProvider.authenticate(authReq) - } catch (e: AuthenticationException) { - return ResponseEntity.notFound().build() - } - SecurityContextHolder.getContext().authentication = auth - val session = userSessionRepository.save(Session(currentUser!!.id)) - return ResponseEntity.ok(SessionResponse(session)) - } - - @GetMapping(path = ["/me"], produces = [MediaType.APPLICATION_JSON_VALUE]) - open fun getProfile(): ResponseEntity { - val user = currentUser - ?: return ResponseEntity.status(401).build() - return ResponseEntity.ok(UserResponse(user)) - } - - @GetMapping(path = ["/search"], produces = [MediaType.APPLICATION_JSON_VALUE]) - open fun searchUsers(query: String?): ResponseEntity> { - return ResponseEntity.ok( - userRepository.findByNameContains(query) - .map { user: User -> UserResponse(user) } - ) - } - - @GetMapping(path = ["/{id}"]) - open fun getUser(@PathVariable id: String): ResponseEntity { - val user = userRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build() - return ResponseEntity.ok(UserResponse(user)) - } - - @PostMapping( - path = [""], - consumes = [MediaType.APPLICATION_JSON_VALUE], - produces = [MediaType.APPLICATION_JSON_VALUE] - ) - fun newUser(@RequestBody request: NewUserRequest): ResponseEntity { - if (userRepository.findByName(request.username).isPresent) return ResponseEntity.badRequest() - .body(ErrorResponse("Username taken")) - if (userRepository.findByEmail(request.email).isPresent) return ResponseEntity.badRequest() - .body(ErrorResponse("Email taken")) - return if (request.password.isBlank()) ResponseEntity.badRequest() - .body(ErrorResponse("Invalid password")) else ResponseEntity.ok( - UserResponse( - userRepository.save( - User( - name = request.username, - passphrase = passwordEncoder.encode(request.password), - email = request.email - ) - ) - ) - ) - } - - @PutMapping( - path = ["/{id}"], - consumes = [MediaType.APPLICATION_JSON_VALUE], - produces = [MediaType.APPLICATION_JSON_VALUE] - ) - fun updateUser(@PathVariable id: String, @RequestBody request: UpdateUserRequest): ResponseEntity { - if (currentUser?.id != id) return ResponseEntity.status(403).build() - var user = userRepository.findById(currentUser!!.id).orElse(null) - ?: return ResponseEntity.notFound().build() - if (request.username != null) { - if (userRepository.findByName(request.username).isPresent) return ResponseEntity.badRequest() - .body(ErrorResponse("Username taken")) - user = user.copy(name = request.username) - } - if (request.email != null) { - if (userRepository.findByEmail(request.email).isPresent) return ResponseEntity.badRequest() - .body(ErrorResponse("Email taken")) - user = user.copy(email = request.email) - } - if (request.password != null) { - if (request.password.isBlank()) return ResponseEntity.badRequest().body(ErrorResponse("Invalid password")) - user = user.copy(passphrase = passwordEncoder.encode(request.password)) - } - return ResponseEntity.ok(UserResponse(userRepository.save(user))) - } - - @DeleteMapping(path = ["/{id}"], produces = [MediaType.TEXT_PLAIN_VALUE]) - fun deleteUser(@PathVariable id: String): ResponseEntity { - if (currentUser?.id != id) return ResponseEntity.status(403).build() - userRepository.deleteById(id) - return ResponseEntity.ok().build() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/wbrawner/twigs/server/user/UserRepository.kt b/app/src/main/kotlin/com/wbrawner/twigs/server/user/UserRepository.kt deleted file mode 100644 index b93ec14..0000000 --- a/app/src/main/kotlin/com/wbrawner/twigs/server/user/UserRepository.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.wbrawner.twigs.server.user - -import org.springframework.data.repository.PagingAndSortingRepository -import java.util.* - -interface UserRepository : PagingAndSortingRepository { - fun findByName(name: String?): Optional - fun findByNameContains(name: String?): List - fun findByEmail(email: String?): Optional -} \ No newline at end of file diff --git a/app/src/main/resources/application.conf b/app/src/main/resources/application.conf index 908b401..870e65c 100644 --- a/app/src/main/resources/application.conf +++ b/app/src/main/resources/application.conf @@ -1,8 +1,19 @@ ktor { deployment { port = 8080 + port = ${?TWIGS_PORT} } application { modules = [ com.wbrawner.twigs.server.ApplicationKt.module ] } + database { + host = localhost + host = ${?TWIGS_DB_HOST} + port = 5432 + port = ${?TWIGS_DB_PORT} + user = twigs + user = ${?TWIGS_DB_USER} + password = twigs + password = ${?TWIGS_DB_PASS} + } } diff --git a/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt b/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt index a796f34..7f72d8a 100644 --- a/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt +++ b/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt @@ -41,3 +41,6 @@ fun randomString(length: Int = 32): String { } return id.toString() } + +// TODO: Use bcrypt to hash strings +fun String.hash(): String = this diff --git a/storage/build.gradle.kts b/storage/build.gradle.kts index 6625996..215a0fb 100644 --- a/storage/build.gradle.kts +++ b/storage/build.gradle.kts @@ -3,9 +3,12 @@ plugins { `java-library` } +val ktorVersion: String by rootProject.extra + dependencies { implementation(kotlin("stdlib")) implementation(project(":core")) + api("io.ktor:ktor-auth:$ktorVersion") api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") implementation("org.postgresql:postgresql:42.2.23") testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0") diff --git a/api/src/main/kotlin/com/wbrawner/twigs/Session.kt b/storage/src/main/kotlin/com/wbrawner/twigs/storage/Session.kt similarity index 66% rename from api/src/main/kotlin/com/wbrawner/twigs/Session.kt rename to storage/src/main/kotlin/com/wbrawner/twigs/storage/Session.kt index d91a063..1dbbf06 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/Session.kt +++ b/storage/src/main/kotlin/com/wbrawner/twigs/storage/Session.kt @@ -1,5 +1,7 @@ -package com.wbrawner.twigs +package com.wbrawner.twigs.storage +import com.wbrawner.twigs.randomString +import com.wbrawner.twigs.twoWeeksFromNow import io.ktor.auth.* import java.util.* diff --git a/storage/src/main/kotlin/com/wbrawner/twigs/storage/SessionRepository.kt b/storage/src/main/kotlin/com/wbrawner/twigs/storage/SessionRepository.kt new file mode 100644 index 0000000..c7656e2 --- /dev/null +++ b/storage/src/main/kotlin/com/wbrawner/twigs/storage/SessionRepository.kt @@ -0,0 +1,9 @@ +package com.wbrawner.twigs.storage + +interface SessionRepository : Repository { + fun findAll( + token: String + ): List + + fun deleteExpired() +} \ No newline at end of file diff --git a/storage/src/main/kotlin/com/wbrawner/twigs/storage/UserRepository.kt b/storage/src/main/kotlin/com/wbrawner/twigs/storage/UserRepository.kt new file mode 100644 index 0000000..7063a49 --- /dev/null +++ b/storage/src/main/kotlin/com/wbrawner/twigs/storage/UserRepository.kt @@ -0,0 +1,19 @@ +package com.wbrawner.twigs.storage + +import com.wbrawner.twigs.model.User + +interface UserRepository : Repository { + fun findAll( + ids: List? = null, + ): List + + fun find( + name: String? = null, + email: String? = null, + password: String? = null + ): List + + fun findAll(nameLike: String): List + + fun deleteById(id: String): Boolean +} \ No newline at end of file