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"), )