tag(功能):完善登录功能
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
package top.OrangeRoll
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.sql.Connection
|
||||
import java.time.Instant
|
||||
import java.util.Base64
|
||||
import java.util.UUID
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
|
||||
data class User(
|
||||
val id: UUID,
|
||||
val username: String,
|
||||
val passwordHash: String,
|
||||
val displayName: String,
|
||||
val isAdmin: Boolean,
|
||||
)
|
||||
|
||||
data class RoomRecord(
|
||||
val id: UUID,
|
||||
val title: String,
|
||||
val status: String,
|
||||
val createdBy: UUID,
|
||||
)
|
||||
|
||||
data class RoomMember(val roomId: UUID, val userId: UUID, val role: RoomRole)
|
||||
|
||||
enum class InvitationType { user, admin }
|
||||
|
||||
object UserRepository {
|
||||
fun findByUsername(username: String): User? = Database.useConnection { connection ->
|
||||
connection.prepareStatement(
|
||||
"SELECT id, username, password_hash, display_name, is_admin FROM users WHERE username = ?",
|
||||
).use { statement ->
|
||||
statement.setString(1, username)
|
||||
statement.executeQuery().use { result ->
|
||||
if (result.next()) result.toUser() else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun findById(id: UUID): User? = Database.useConnection { connection ->
|
||||
connection.prepareStatement(
|
||||
"SELECT id, username, password_hash, display_name, is_admin FROM users WHERE id = ?",
|
||||
).use { statement ->
|
||||
statement.setObject(1, id)
|
||||
statement.executeQuery().use { result ->
|
||||
if (result.next()) result.toUser() else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bootstrapAdmin(username: String, password: String, displayName: String) {
|
||||
if (findByUsername(username) != null) return
|
||||
Database.useConnection { connection ->
|
||||
connection.prepareStatement(
|
||||
"INSERT INTO users (id, username, password_hash, display_name, is_admin) VALUES (?, ?, ?, ?, TRUE)",
|
||||
).use { statement ->
|
||||
statement.setObject(1, UUID.randomUUID())
|
||||
statement.setString(2, username)
|
||||
statement.setString(3, BCrypt.hashpw(password, BCrypt.gensalt()))
|
||||
statement.setString(4, displayName)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun register(inviteCode: String, username: String, password: String, displayName: String): User =
|
||||
Database.useConnection { connection ->
|
||||
connection.autoCommit = false
|
||||
try {
|
||||
val invitation = connection.prepareStatement(
|
||||
"""
|
||||
SELECT id, invitation_type, expires_at, used_at
|
||||
FROM invitations
|
||||
WHERE code_hash = ?
|
||||
FOR UPDATE
|
||||
""".trimIndent(),
|
||||
).use { statement ->
|
||||
statement.setString(1, hashInviteCode(inviteCode))
|
||||
statement.executeQuery().use { result ->
|
||||
if (!result.next()) throw ApiException("邀请码无效", 400)
|
||||
InviteRow(
|
||||
id = result.getObject("id", UUID::class.java),
|
||||
type = InvitationType.valueOf(result.getString("invitation_type")),
|
||||
expiresAt = result.getTimestamp("expires_at").toInstant(),
|
||||
usedAt = result.getTimestamp("used_at")?.toInstant(),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (invitation.usedAt != null || invitation.expiresAt.isBefore(Instant.now())) {
|
||||
throw ApiException("邀请码已使用或已过期", 400)
|
||||
}
|
||||
|
||||
val user = User(
|
||||
id = UUID.randomUUID(),
|
||||
username = username,
|
||||
passwordHash = BCrypt.hashpw(password, BCrypt.gensalt()),
|
||||
displayName = displayName,
|
||||
isAdmin = invitation.type == InvitationType.admin,
|
||||
)
|
||||
connection.prepareStatement(
|
||||
"INSERT INTO users (id, username, password_hash, display_name, is_admin) VALUES (?, ?, ?, ?, ?)",
|
||||
).use { statement ->
|
||||
statement.setObject(1, user.id)
|
||||
statement.setString(2, user.username)
|
||||
statement.setString(3, user.passwordHash)
|
||||
statement.setString(4, user.displayName)
|
||||
statement.setBoolean(5, user.isAdmin)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
connection.prepareStatement(
|
||||
"UPDATE invitations SET used_by = ?, used_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
).use { statement ->
|
||||
statement.setObject(1, user.id)
|
||||
statement.setObject(2, invitation.id)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
connection.commit()
|
||||
user
|
||||
} catch (error: Throwable) {
|
||||
connection.rollback()
|
||||
throw error
|
||||
} finally {
|
||||
connection.autoCommit = true
|
||||
}
|
||||
}
|
||||
|
||||
private data class InviteRow(
|
||||
val id: UUID,
|
||||
val type: InvitationType,
|
||||
val expiresAt: Instant,
|
||||
val usedAt: Instant?,
|
||||
)
|
||||
}
|
||||
|
||||
object InvitationRepository {
|
||||
fun create(createdBy: UUID, type: InvitationType, expiresAt: Instant): String {
|
||||
val code = Base64.getUrlEncoder().withoutPadding()
|
||||
.encodeToString(SecureRandom().generateSeed(24))
|
||||
Database.useConnection { connection ->
|
||||
connection.prepareStatement(
|
||||
"""
|
||||
INSERT INTO invitations (id, code_hash, invitation_type, created_by, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""".trimIndent(),
|
||||
).use { statement ->
|
||||
statement.setObject(1, UUID.randomUUID())
|
||||
statement.setString(2, hashInviteCode(code))
|
||||
statement.setString(3, type.name)
|
||||
statement.setObject(4, createdBy)
|
||||
statement.setTimestamp(5, java.sql.Timestamp.from(expiresAt))
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
return code
|
||||
}
|
||||
}
|
||||
|
||||
object RoomRepository {
|
||||
fun create(hostId: UUID, title: String): RoomRecord {
|
||||
val room = RoomRecord(UUID.randomUUID(), title, "open", hostId)
|
||||
Database.useConnection { connection ->
|
||||
connection.autoCommit = false
|
||||
try {
|
||||
connection.prepareStatement(
|
||||
"INSERT INTO rooms (id, slug, title, created_by) VALUES (?, ?, ?, ?)",
|
||||
).use { statement ->
|
||||
statement.setObject(1, room.id)
|
||||
statement.setString(2, room.id.toString())
|
||||
statement.setString(3, room.title)
|
||||
statement.setObject(4, hostId)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
connection.prepareStatement(
|
||||
"INSERT INTO room_members (room_id, user_id, role) VALUES (?, ?, 'host')",
|
||||
).use { statement ->
|
||||
statement.setObject(1, room.id)
|
||||
statement.setObject(2, hostId)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
connection.commit()
|
||||
} catch (error: Throwable) {
|
||||
connection.rollback()
|
||||
throw error
|
||||
} finally {
|
||||
connection.autoCommit = true
|
||||
}
|
||||
}
|
||||
return room
|
||||
}
|
||||
|
||||
fun find(roomId: UUID): RoomRecord? = Database.useConnection { connection ->
|
||||
connection.prepareStatement("SELECT id, title, status, created_by FROM rooms WHERE id = ?").use { statement ->
|
||||
statement.setObject(1, roomId)
|
||||
statement.executeQuery().use { result ->
|
||||
if (result.next()) {
|
||||
RoomRecord(
|
||||
result.getObject("id", UUID::class.java),
|
||||
result.getString("title"),
|
||||
result.getString("status"),
|
||||
result.getObject("created_by", UUID::class.java),
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun member(roomId: UUID, userId: UUID): RoomMember? = Database.useConnection { connection ->
|
||||
connection.prepareStatement(
|
||||
"SELECT room_id, user_id, role FROM room_members WHERE room_id = ? AND user_id = ?",
|
||||
).use { statement ->
|
||||
statement.setObject(1, roomId)
|
||||
statement.setObject(2, userId)
|
||||
statement.executeQuery().use { result ->
|
||||
if (result.next()) {
|
||||
RoomMember(
|
||||
result.getObject("room_id", UUID::class.java),
|
||||
result.getObject("user_id", UUID::class.java),
|
||||
RoomRole.valueOf(result.getString("role")),
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun joinAsViewer(roomId: UUID, userId: UUID): RoomMember {
|
||||
val room = find(roomId) ?: throw ApiException("房间不存在", 404)
|
||||
if (room.status != "open") throw ApiException("房间已关闭", 403)
|
||||
Database.useConnection { connection ->
|
||||
connection.prepareStatement(
|
||||
"INSERT INTO room_members (room_id, user_id, role) VALUES (?, ?, 'viewer') ON CONFLICT DO NOTHING",
|
||||
).use { statement ->
|
||||
statement.setObject(1, roomId)
|
||||
statement.setObject(2, userId)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
return member(roomId, userId) ?: error("Room membership was not created")
|
||||
}
|
||||
|
||||
fun updateRole(roomId: UUID, userId: UUID, role: RoomRole): RoomMember {
|
||||
Database.useConnection { connection ->
|
||||
connection.prepareStatement(
|
||||
"UPDATE room_members SET role = ? WHERE room_id = ? AND user_id = ?",
|
||||
).use { statement ->
|
||||
statement.setString(1, role.name)
|
||||
statement.setObject(2, roomId)
|
||||
statement.setObject(3, userId)
|
||||
if (statement.executeUpdate() == 0) throw ApiException("房间成员不存在", 404)
|
||||
}
|
||||
}
|
||||
return member(roomId, userId) ?: error("Room membership was not updated")
|
||||
}
|
||||
}
|
||||
|
||||
fun hashInviteCode(value: String): String =
|
||||
MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.toByteArray(Charsets.UTF_8))
|
||||
.joinToString("") { byte -> "%02x".format(byte) }
|
||||
|
||||
private fun java.sql.ResultSet.toUser(): User =
|
||||
User(
|
||||
id = getObject("id", UUID::class.java),
|
||||
username = getString("username"),
|
||||
passwordHash = getString("password_hash"),
|
||||
displayName = getString("display_name"),
|
||||
isAdmin = getBoolean("is_admin"),
|
||||
)
|
||||
Reference in New Issue
Block a user