Add user routes
This commit is contained in:
parent
4de09a8f1c
commit
f6e0157560
18 changed files with 309 additions and 231 deletions
|
@ -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")
|
||||
|
|
58
api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt
Normal file
58
api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt
Normal file
|
@ -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<Unit, ApplicationCall>.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<Unit, ApplicationCall>.budgetWithPermission(
|
||||
budgetRepository: BudgetRepository,
|
||||
permissionRepository: PermissionRepository,
|
||||
budgetId: String,
|
||||
permission: Permission,
|
||||
block: suspend (Budget) -> Unit
|
||||
) {
|
||||
val session = call.principal<Session>()!!
|
||||
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<Unit, ApplicationCall>.errorResponse(
|
||||
httpStatusCode: HttpStatusCode = HttpStatusCode.NotFound,
|
||||
message: String? = null
|
||||
) {
|
||||
message?.let {
|
||||
call.respond(httpStatusCode, ErrorResponse(message))
|
||||
}?: call.respond(httpStatusCode)
|
||||
}
|
|
@ -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<Unit, ApplicationCall>.budgetWithPermission(
|
||||
budgetId: String,
|
||||
permission: Permission,
|
||||
block: suspend (Budget) -> Unit
|
||||
) {
|
||||
val session = call.principal<Session>()!!
|
||||
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<Session>()!!
|
||||
val request = call.receive<BudgetRequest>()
|
||||
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<BudgetRequest>()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ data class CategoryResponse(
|
|||
category.title,
|
||||
category.description,
|
||||
category.amount,
|
||||
category.budget!!.id,
|
||||
category.budgetId!!,
|
||||
category.expense,
|
||||
category.archived
|
||||
)
|
||||
|
|
|
@ -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<Session>()!!
|
||||
val request = call.receive<CategoryRequest>()
|
||||
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<Unit, ApplicationCall>.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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,9 +22,7 @@ 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,
|
||||
|
@ -39,3 +30,7 @@ data class PasswordResetRequest(
|
|||
private val date: Calendar = GregorianCalendar(),
|
||||
private val token: String = randomString()
|
||||
)
|
||||
|
||||
fun User.asResponse(): UserResponse = UserResponse(id, name, email)
|
||||
|
||||
fun Session.asResponse(): SessionResponse = SessionResponse(token, expiration.toInstant().toString())
|
139
api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt
Normal file
139
api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt
Normal file
|
@ -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<LoginRequest>()
|
||||
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<UserRequest>()
|
||||
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<UserRequest>()
|
||||
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<Session>()!!
|
||||
val request = call.receive<UserRequest>()
|
||||
// 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<Session>()!!
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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<String>): 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<String>("Authorization")
|
||||
}
|
||||
install(Authentication) {
|
||||
session<String> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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<List<UserPermissionResponse>> {
|
||||
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<SessionResponse> {
|
||||
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<UserResponse> {
|
||||
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<List<UserResponse>> {
|
||||
return ResponseEntity.ok(
|
||||
userRepository.findByNameContains(query)
|
||||
.map { user: User -> UserResponse(user) }
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping(path = ["/{id}"])
|
||||
open fun getUser(@PathVariable id: String): ResponseEntity<UserResponse> {
|
||||
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<Any> {
|
||||
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<Any> {
|
||||
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<Void> {
|
||||
if (currentUser?.id != id) return ResponseEntity.status(403).build()
|
||||
userRepository.deleteById(id)
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package com.wbrawner.twigs.server.user
|
||||
|
||||
import org.springframework.data.repository.PagingAndSortingRepository
|
||||
import java.util.*
|
||||
|
||||
interface UserRepository : PagingAndSortingRepository<User, String> {
|
||||
fun findByName(name: String?): Optional<User>
|
||||
fun findByNameContains(name: String?): List<User>
|
||||
fun findByEmail(email: String?): Optional<User>
|
||||
}
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,3 +41,6 @@ fun randomString(length: Int = 32): String {
|
|||
}
|
||||
return id.toString()
|
||||
}
|
||||
|
||||
// TODO: Use bcrypt to hash strings
|
||||
fun String.hash(): String = this
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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.*
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.wbrawner.twigs.storage
|
||||
|
||||
interface SessionRepository : Repository<Session> {
|
||||
fun findAll(
|
||||
token: String
|
||||
): List<Session>
|
||||
|
||||
fun deleteExpired()
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.wbrawner.twigs.storage
|
||||
|
||||
import com.wbrawner.twigs.model.User
|
||||
|
||||
interface UserRepository : Repository<User> {
|
||||
fun findAll(
|
||||
ids: List<String>? = null,
|
||||
): List<User>
|
||||
|
||||
fun find(
|
||||
name: String? = null,
|
||||
email: String? = null,
|
||||
password: String? = null
|
||||
): List<User>
|
||||
|
||||
fun findAll(nameLike: String): List<User>
|
||||
|
||||
fun deleteById(id: String): Boolean
|
||||
}
|
Loading…
Reference in a new issue