Files
ScreenShare/apps/ScreenShareApi/src/main/kotlin/Repositories.kt
T

270 lines
11 KiB
Kotlin

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