270 lines
11 KiB
Kotlin
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"),
|
|
)
|