package com.andr3h3nriqu3s.applications import java.util.UUID import kotlin.io.encoding.Base64 import kotlin.random.Random import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.jdbc.core.JdbcTemplate import org.springframework.security.crypto.argon2.Argon2PasswordEncoder import org.springframework.stereotype.Service import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController enum class UserLevel(val level: Int) { NORMAL(1), ADMIN(255) } data class UserDb( var id: String, var username: String, var email: String, var password: String, var level: Int ) { fun toSafe(): User { return User(this.id, this.username, this.email, this.level) } fun checkLevel(level: Int): Boolean { return (level and this.level) == level } fun checkLevelThrow(level: Int) { if (!this.checkLevel(level)) { throw NotAuth() } } } data class UserCreateRequest(val email: String, val password: String, val username: String) data class LoginUserRequest(val email: String, val password: String) data class LoggedInUser(val token: String, val user: User) data class User(val id: String, var username: String, var email: String, var level: Int) @RestController @ControllerAdvice @RequestMapping("/api/user") @kotlin.io.encoding.ExperimentalEncodingApi class UserController( val userService: UserService, val sessionService: SessionService, val sessionServiceCreator: SessionServiceCreator, ) { @PostMapping(path = ["/login"], produces = [MediaType.APPLICATION_JSON_VALUE]) public fun login(@RequestBody user: LoginUserRequest): LoggedInUser { val userdb = userService.login(user.email, user.password) val session = sessionServiceCreator.createSession(userdb) return LoggedInUser(session.token, userdb.toSafe()) } @GetMapping(path = ["/list"], produces = [MediaType.APPLICATION_JSON_VALUE]) public fun list(@RequestHeader("token") token: String): List { sessionService.verifyTokenThrow(token).checkLevelThrow(UserLevel.ADMIN.ordinal) return userService.findUsers().map { elm -> elm.toSafe() } } @PostMapping(path = ["/register"], produces = [MediaType.APPLICATION_JSON_VALUE]) public fun register(@RequestBody user: UserCreateRequest): LoggedInUser { var new_user = userService.createUser(user) val session = sessionServiceCreator.createSession(new_user) return LoggedInUser(session.token, new_user.toSafe()) } } @Service class UserService(val db: JdbcTemplate) { fun login(email: String, passwd: String): UserDb { var user = this.findUserByEmail(email) if (user == null) { throw UserNotFound() } var arg2SpringSecurity = Argon2PasswordEncoder(16, 32, 1, 60000, 10) // TODO make this safe if (!arg2SpringSecurity.matches(passwd, user.password)) { throw UserNotFound() } return user } fun findUserByEmail(email: String): UserDb? { var users = db .query("select * from users where email=?", arrayOf(email)) { response, _ -> UserDb( response.getString("id"), response.getString("username"), response.getString("email"), response.getString("passwd"), response.getInt("level") ) } .toList() if (users.size == 0) { return null } return users[0] } fun createUser(user: UserCreateRequest): UserDb { var user_check = findUserByEmail(user.email) if (user_check != null) { throw Exception("User already exists") } val id = UUID.randomUUID().toString() var arg2SpringSecurity = Argon2PasswordEncoder(16, 32, 1, 60000, 10) val hashPassword = arg2SpringSecurity.encode(user.password) var new_user = UserDb(id, user.username, user.email, hashPassword, UserLevel.NORMAL.ordinal) db.update( "insert into users (id, username, email, passwd, level) values (?, ?, ?, ?, ?)", new_user.id, user.username, user.email, new_user.password, new_user.level, ) return new_user } fun findUsers(): List = db.query("select * from users") { response, _ -> UserDb( response.getString("id"), response.getString("username"), response.getString("email"), response.getString("passwd"), response.getInt("level"), ) } } data class Session(val token: String, val user_id: String) @Service class SessionService(val db: JdbcTemplate) { fun verifyToken(token: String): UserDb? { var users = db .query( "select * from tokens as t inner join users as u on id = user_id where token = ?", arrayOf(token) ) { response, _ -> UserDb( response.getString("id"), response.getString("username"), response.getString("email"), response.getString("passwd"), response.getInt("level"), ) } .toList() if (users.size == 0) { return null } return users[0] } fun verifyTokenThrow(token: String): UserDb { val new_user = this.verifyToken(token) if (new_user == null) { throw NoToken() } return new_user } } @Service @kotlin.io.encoding.ExperimentalEncodingApi class SessionServiceCreator(val db: JdbcTemplate) { fun createSession(user: UserDb): Session { val session = Session(this.generateToken(), user.id) db.update( "insert into tokens (token, user_id) values (?,?);", session.token, session.user_id ) return session } fun generateToken(): String { var b = ByteArray(60) Random.nextBytes(b) return Base64.encode(b) } }