diff --git a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/ChzzkHandler.kt b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/ChzzkHandler.kt index 69de373..150d454 100644 --- a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/ChzzkHandler.kt +++ b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/ChzzkHandler.kt @@ -39,7 +39,7 @@ object ChzzkHandler { fun enable() { botUid = chzzk.loggedUser.userId UserService.getAllUsers().map { - if(it.token != null && !it.isDisabled) + if(!it.isDisabled) chzzk.getChannel(it.token)?.let { token -> addUser(token, it) } } diff --git a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt index 8b1bd5b..e4300b7 100644 --- a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt +++ b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt @@ -42,7 +42,7 @@ class MessageHandler( if(it.type == SongType.STREAM_OFF) { val user = UserService.getUser(channel.channelId) if(! user?.let { usr -> SongListService.getSong(usr) }.isNullOrEmpty()) { - SongListService.deleteUser(user!!) + SongListService.deleteUser(user) } } } @@ -161,7 +161,7 @@ class MessageHandler( CoroutineScope(Dispatchers.Default).launch { dispatcher.post( TimerEvent( - user.token!!, + user.token, TimerType.UPTIME, getUptime(handler.streamStartTime!!) ) @@ -171,7 +171,7 @@ class MessageHandler( "삭제" -> { logger.debug("${user.token} / 삭제") CoroutineScope(Dispatchers.Default).launch { - dispatcher.post(TimerEvent(user.token!!, TimerType.REMOVE, "")) + dispatcher.post(TimerEvent(user.token, TimerType.REMOVE, "")) } } "설정" -> { @@ -195,7 +195,7 @@ class MessageHandler( val timestamp = currentTime.plus(time.toLong(), ChronoUnit.MINUTES) CoroutineScope(Dispatchers.Default).launch { - dispatcher.post(TimerEvent(user.token!!, TimerType.TIMER, timestamp.toString())) + dispatcher.post(TimerEvent(user.token, TimerType.TIMER, timestamp.toString())) } } catch (e: NumberFormatException) { listener.sendChat("!타이머/숫자 형식으로 적어주세요! 단위: 분") @@ -267,7 +267,7 @@ class MessageHandler( CoroutineScope(Dispatchers.Default).launch { dispatcher.post( SongEvent( - user.token!!, + user.token, SongType.ADD, msg.userId, null, diff --git a/common/src/main/kotlin/space/mori/chzzk_bot/common/models/User.kt b/common/src/main/kotlin/space/mori/chzzk_bot/common/models/User.kt index 6becf58..ef6320c 100644 --- a/common/src/main/kotlin/space/mori/chzzk_bot/common/models/User.kt +++ b/common/src/main/kotlin/space/mori/chzzk_bot/common/models/User.kt @@ -8,9 +8,8 @@ import org.jetbrains.exposed.dao.id.IntIdTable object Users: IntIdTable("users") { val username = varchar("username", 255) - val token = varchar("token", 64).nullable() + val token = varchar("token", 64) val discord = long("discord").nullable() - val naverId = varchar("naver_id", 128) val liveAlertGuild = long("live_alert_guild").nullable() val liveAlertChannel = long("live_alert_channel").nullable() val liveAlertMessage = text("live_alert_message").nullable() @@ -24,7 +23,6 @@ class User(id: EntityID) : IntEntity(id) { var username by Users.username var token by Users.token var discord by Users.discord - var naverId by Users.naverId var liveAlertGuild by Users.liveAlertGuild var liveAlertChannel by Users.liveAlertChannel var liveAlertMessage by Users.liveAlertMessage diff --git a/common/src/main/kotlin/space/mori/chzzk_bot/common/services/UserService.kt b/common/src/main/kotlin/space/mori/chzzk_bot/common/services/UserService.kt index 64a2258..f3e3ddb 100644 --- a/common/src/main/kotlin/space/mori/chzzk_bot/common/services/UserService.kt +++ b/common/src/main/kotlin/space/mori/chzzk_bot/common/services/UserService.kt @@ -6,11 +6,11 @@ import space.mori.chzzk_bot.common.models.User import space.mori.chzzk_bot.common.models.Users object UserService { - fun saveUser(username: String, naverId: String): User { + fun saveUser(username: String, token: String): User { return transaction { User.new { this.username = username - this.naverId = naverId + this.token = token } } } @@ -64,14 +64,6 @@ object UserService { } } - fun getUserWithNaverId(naverId: String): User? { - return transaction { - val user = User.find{ Users.naverId eq naverId }.firstOrNull() - user?.load(User::subordinates, User::managers) - user - } - } - fun getAllUsers(): List { return transaction { User.all().toList() diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/Main.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/Main.kt index 56e2ad4..c6093dd 100644 --- a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/Main.kt +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/Main.kt @@ -26,6 +26,8 @@ import space.mori.chzzk_bot.common.services.UserService import space.mori.chzzk_bot.webserver.routes.* import space.mori.chzzk_bot.webserver.utils.DiscordRatelimits import wsSongListRoutes +import java.math.BigInteger +import java.security.SecureRandom import java.time.Duration import kotlin.time.toKotlinDuration @@ -33,14 +35,12 @@ val dotenv = dotenv { ignoreIfMissing = true } -const val naverMeAPIURL = "https://openapi.naver.com/v1/nid/me" - val redirects = mutableMapOf() val server = embeddedServer(Netty, port = 8080, ) { install(WebSockets) { pingPeriod = Duration.ofSeconds(15).toKotlinDuration() - timeout = Duration.ofSeconds(15).toKotlinDuration() + timeout = Duration.ofSeconds(100).toKotlinDuration() maxFrameSize = Long.MAX_VALUE masking = false contentConverter = KotlinxWebsocketSerializationConverter(Json) @@ -56,26 +56,6 @@ val server = embeddedServer(Netty, port = 8080, ) { cookie("user_session", storage = MariadbSessionStorage()) {} } install(Authentication) { - oauth("auth-oauth-naver") { - urlProvider = { "${dotenv["HOST"]}/auth/callback" } - providerLookup = { OAuthServerSettings.OAuth2ServerSettings( - name = "naver", - authorizeUrl = "https://nid.naver.com/oauth2.0/authorize", - accessTokenUrl = "https://nid.naver.com/oauth2.0/token", - requestMethod = HttpMethod.Post, - clientId = dotenv["NAVER_CLIENT_ID"], - clientSecret = dotenv["NAVER_CLIENT_SECRET"], - defaultScopes = listOf(""), - extraAuthParameters = listOf(), - onStateCreated = { call, state -> - //saves new state with redirect url value - call.request.queryParameters["redirectUrl"]?.let { - redirects[state] = it - } - } - )} - client = applicationHttpClient - } oauth("auth-oauth-discord") { urlProvider = { "${dotenv["HOST"]}/auth/callback/discord" } providerLookup = { OAuthServerSettings.OAuth2ServerSettings( @@ -112,7 +92,7 @@ val server = embeddedServer(Netty, port = 8080, ) { try { val principal = call.principal() val session = call.sessions.get() - val user = session?.id?.let { UserService.getUserWithNaverId(it) } + val user = session?.id?.let { UserService.getUser(it) } if(principal != null && session != null && user != null) { try { @@ -150,31 +130,80 @@ val server = embeddedServer(Netty, port = 8080, ) { } // naver login - authenticate("auth-oauth-naver") { - get("/login") { + get("/login") { + val state = generateSecureRandomState() + // 세션에 상태 값 저장 + call.sessions.set(UserSession( + state, + "", + listOf(), + )) + + // OAuth 제공자의 인증 URL 구성 + val authUrl = URLBuilder("https://chzzk.naver.com/account-interlock").apply { + parameters.append("clientId", dotenv["NAVER_CLIENT_ID"]) // 비표준 파라미터 이름 + parameters.append("redirectUri", "${dotenv["HOST"]}/auth/callback") + parameters.append("state", state) + // 추가적인 파라미터가 필요하면 여기에 추가 + }.build().toString() + + // 사용자에게 인증 페이지로 리다이렉트 + call.respondRedirect(authUrl) + } + get("/callback") { + val receivedState = call.parameters["state"] + val code = call.parameters["code"] + + // 세션에서 상태 값 가져오기 + val session = call.sessions.get() + if (session == null || session.state != receivedState) { + call.respond(HttpStatusCode.BadRequest, "Invalid state parameter") + return@get } - get("/callback") { - val currentPrincipal = call.principal() - currentPrincipal?.let { principal -> - principal.state?.let { state -> - val userInfo: NaverAPI = applicationHttpClient.get(naverMeAPIURL) { - headers { - append(HttpHeaders.Authorization, "Bearer ${principal.accessToken}") - } - }.body() - call.sessions.set(userInfo.response?.let { profile -> - UserSession(state, profile.id, listOf()) - }) + if (code == null) { + call.respond(HttpStatusCode.BadRequest, "Missing code parameter") + return@get + } + try { + // Access Token 요청 + val tokenRequest = TokenRequest( + grantType = "authorization_code", + state = session.state, + code = code, + clientId = dotenv["NAVER_CLIENT_ID"], + clientSecret = dotenv["NAVER_CLIENT_SECRET"] + ) - redirects[state]?.let { redirect -> - call.respondRedirect(redirect) - return@get - } - } + val response = applicationHttpClient.post("https://chzzk.naver.com/auth/v1/token") { + contentType(ContentType.Application.Json) + setBody(tokenRequest) } - call.respondRedirect(getFrontendURL("")) + + val tokenResponse = response.body() + + if(tokenResponse.content == null) { + call.respond(HttpStatusCode.InternalServerError, "Failed to obtain access token") + return@get + } + + // Access Token 사용: 예를 들어, 사용자 정보 요청 + val userInfo = getChzzkUser(tokenResponse.content.accessToken) + + if(userInfo.content != null) { + call.sessions.set( + UserSession( + session.state, + userInfo.content.channelId, + listOf() + ) + ) + call.respondRedirect(getFrontendURL("")) + } + } catch (e: Exception) { + e.printStackTrace() + call.respond(HttpStatusCode.InternalServerError, "Failed to obtain access token") } } @@ -231,16 +260,33 @@ fun getFrontendURL(path: String) data class UserSession( val state: String, val id: String, - val discordGuildList: List + val discordGuildList: List, + ) @Serializable -data class NaverMeAPI( - val id: String +data class TokenRequest( + val grantType: String, + val state: String, + val code: String, + val clientId: String, + val clientSecret: String ) @Serializable -data class NaverAPI(val resultcode: String, val message: String, val response: T?) +data class TokenResponse( + val code: Int, + val message: String?, + val content: TokenResponseBody? +) + +@Serializable +data class TokenResponseBody( + val accessToken: String, + val tokenType: String, + val expiresIn: Int, + val refreshToken: String? = null +) @Serializable data class DiscordMeAPI( @@ -352,4 +398,32 @@ suspend fun getUserGuilds(accessToken: String): List { DiscordRatelimits.setRateLimit(rateLimit, remaining, resetAfter) return response.body>() +} + +@Serializable +data class ChzzkMeApi( + val channelId: String, + val channelName: String, + val nickname: String, +) + +@Serializable +data class ChzzkApi( + val code: Int, + val message: String?, + val content: T? +) + +suspend fun getChzzkUser(accessToken: String): ChzzkApi { + val response = applicationHttpClient.get("https://openapi.chzzk.naver.com/open/v1/users/me") { + headers { + append(HttpHeaders.Authorization, "Bearer $accessToken") + } + } + + return response.body>() +} + +fun generateSecureRandomState(): String { + return BigInteger(130, SecureRandom()).toString(32) } \ No newline at end of file diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiCommandRoutes.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiCommandRoutes.kt index b7a1456..bacb6ff 100644 --- a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiCommandRoutes.kt +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiCommandRoutes.kt @@ -1,7 +1,6 @@ package space.mori.chzzk_bot.webserver.routes import io.ktor.http.* -import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -58,7 +57,7 @@ fun Routing.apiCommandRoutes() { val managers = transaction { user.managers.toList() } - if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) { + if(!managers.any { it.token == session?.id } && user.token != session?.id) { call.respond(HttpStatusCode.BadRequest, "User does not exist") return@put } @@ -70,7 +69,7 @@ fun Routing.apiCommandRoutes() { ) CoroutineScope(Dispatchers.Default).launch { for(i: Int in 0..3) { - dispatcher.post(CommandReloadEvent(user.token ?: "")) + dispatcher.post(CommandReloadEvent(user.token)) } } call.respond(HttpStatusCode.OK) @@ -93,7 +92,7 @@ fun Routing.apiCommandRoutes() { val managers = transaction { user.managers.toList() } - if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) { + if(!managers.any { it.token == session?.id } && user.token != session?.id) { call.respond(HttpStatusCode.BadRequest, "User does not exist") return@post } @@ -107,7 +106,7 @@ fun Routing.apiCommandRoutes() { ) CoroutineScope(Dispatchers.Default).launch { for(i: Int in 0..3) { - dispatcher.post(CommandReloadEvent(user.token ?: "")) + dispatcher.post(CommandReloadEvent(user.token)) } } call.respond(HttpStatusCode.OK) @@ -133,7 +132,7 @@ fun Routing.apiCommandRoutes() { val managers = transaction { user.managers.toList() } - if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) { + if(!managers.any { it.token == session?.id } && user.token != session?.id) { call.respond(HttpStatusCode.BadRequest, "User does not exist") return@delete } @@ -142,7 +141,7 @@ fun Routing.apiCommandRoutes() { CommandService.removeCommand(user, commandRequest.label) CoroutineScope(Dispatchers.Default).launch { for(i: Int in 0..3) { - dispatcher.post(CommandReloadEvent(user.token ?: "")) + dispatcher.post(CommandReloadEvent(user.token)) } } call.respond(HttpStatusCode.OK) diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiDiscordRoutes.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiDiscordRoutes.kt index f23a025..b31ebac 100644 --- a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiDiscordRoutes.kt +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiDiscordRoutes.kt @@ -1,7 +1,6 @@ package space.mori.chzzk_bot.webserver.routes import io.ktor.http.* -import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -34,7 +33,7 @@ fun Route.apiDiscordRoutes() { val managers = transaction { user.managers.toList() } - if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) { + if(!managers.any { it.token == session?.id } && user.token != session?.id) { call.respond(HttpStatusCode.BadRequest, "User does not exist") return@get } @@ -67,7 +66,7 @@ fun Route.apiDiscordRoutes() { val managers = transaction { user.managers.toList() } - if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) { + if(!managers.any { it.token == session?.id } && user.token != session?.id) { call.respond(HttpStatusCode.BadRequest, "User does not exist") return@post } @@ -86,7 +85,7 @@ fun Route.apiDiscordRoutes() { call.respond(HttpStatusCode.BadRequest, "Session is required") return@get } - val user = UserService.getUserWithNaverId(session.id) + val user = UserService.getUser(session.id) if(user == null) { call.respond(HttpStatusCode.BadRequest, "User does not exist") return@get diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiRoutes.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiRoutes.kt index 1b56761..388a8ff 100644 --- a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiRoutes.kt +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiRoutes.kt @@ -96,20 +96,20 @@ fun Routing.apiRoutes() { call.respondText("No session found", status = HttpStatusCode.Unauthorized) return@get } - var user = UserService.getUserWithNaverId(session.id) + var user = UserService.getUser(session.id) if(user == null) { user = UserService.saveUser("임시닉네임", session.id) } val songConfig = SongConfigService.getConfig(user) - val status = user.token?.let { it1 -> getStreamInfo(it1) } + val status = getStreamInfo(user.token) val returnUsers = mutableListOf() if (user.username == "임시닉네임") { - status?.content?.channel?.let { it1 -> UserService.updateUser(user, it1.channelId, it1.channelName) } + status.content?.channel?.let { it1 -> UserService.updateUser(user, it1.channelId, it1.channelName) } } - if(status?.content == null) { - call.respondText(user.naverId, status = HttpStatusCode.NotFound) + if(status.content == null) { + call.respondText(user.token, status = HttpStatusCode.NotFound) return@get } @@ -128,8 +128,8 @@ fun Routing.apiRoutes() { user.subordinates.toList() } returnUsers.addAll(subordinates.map { - val subStatus = it.token?.let { token -> ChzzkUserCache.getCachedUser(token) } - return@map if (it.token == null || subStatus?.content == null) { + val subStatus = it.token.let { token -> ChzzkUserCache.getCachedUser(token) } + return@map if (subStatus?.content == null) { null } else { GetSessionDTO( @@ -156,7 +156,7 @@ fun Routing.apiRoutes() { val body: RegisterChzzkUserDTO = call.receive() - val user = UserService.getUserWithNaverId(session.id) + val user = UserService.getUser(session.id) if(user == null) { call.respondText("No session found", status = HttpStatusCode.Unauthorized) return@post @@ -197,7 +197,7 @@ fun Routing.apiRoutes() { return@get } - val user = UserService.getUserWithNaverId(session.id) + val user = UserService.getUser(session.id) if(user == null) { call.respondText("No session found", status = HttpStatusCode.Unauthorized) return@get @@ -216,7 +216,7 @@ fun Routing.apiRoutes() { val body: GetSettingDTO = call.receive() - val user = UserService.getUserWithNaverId(session.id) + val user = UserService.getUser(session.id) if(user == null) { call.respondText("No session found", status = HttpStatusCode.Unauthorized) return@post diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiTimerRoutes.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiTimerRoutes.kt index 6264545..354b3c5 100644 --- a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiTimerRoutes.kt +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiTimerRoutes.kt @@ -1,7 +1,6 @@ package space.mori.chzzk_bot.webserver.routes import io.ktor.http.* -import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -31,7 +30,7 @@ fun Routing.apiTimerRoutes() { val managers = transaction { user.managers.toList() } - if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) { + if(!managers.any { it.token == session?.id } && user.token != session?.id) { call.respond(HttpStatusCode.BadRequest, "User does not exist") return@get } @@ -58,7 +57,7 @@ fun Routing.apiTimerRoutes() { val managers = transaction { user.managers.toList() } - if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) { + if(!managers.any { it.token == session?.id } && user.token != session?.id) { call.respond(HttpStatusCode.BadRequest, "User does not exist") return@put } diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSSongListRoutes.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSSongListRoutes.kt index 1b56374..01b145b 100644 --- a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSSongListRoutes.kt +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSSongListRoutes.kt @@ -109,7 +109,7 @@ fun Routing.wsSongListRoutes() { webSocket("/songlist") { val session = call.sessions.get() - val user = session?.id?.let { UserService.getUserWithNaverId(it) } + val user = session?.id?.let { UserService.getUser(it) } if (user == null) { close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid SID")) return@webSocket @@ -117,7 +117,7 @@ fun Routing.wsSongListRoutes() { val uid = user.token - addSession(uid!!, this) + addSession(uid, this) if (status[uid] == SongType.STREAM_OFF) { CoroutineScope(Dispatchers.Default).launch { @@ -161,18 +161,16 @@ fun Routing.wsSongListRoutes() { CoroutineScope(Dispatchers.Default).launch { val user = UserService.getUser(it.uid) if (user != null) { - user.token?.let { token -> - sendWithRetry( - token, SongResponse( - it.type.value, - it.uid, - it.reqUid, - it.current?.toSerializable(), - it.next?.toSerializable(), - it.delUrl - ) + sendWithRetry( + user.token, SongResponse( + it.type.value, + it.uid, + it.reqUid, + it.current?.toSerializable(), + it.next?.toSerializable(), + it.delUrl ) - } + ) } } } @@ -182,17 +180,15 @@ fun Routing.wsSongListRoutes() { CoroutineScope(Dispatchers.Default).launch { val user = UserService.getUser(it.uid) if (user != null) { - user.token?.let { token -> - sendWithRetry( - token, SongResponse( - it.type.value, - it.uid, - null, - null, - null, - ) + sendWithRetry( + user.token, SongResponse( + it.type.value, + it.uid, + null, + null, + null, ) - } + ) } } } @@ -219,7 +215,7 @@ suspend fun handleSongRequest( CoroutineScope(Dispatchers.Default).launch { SongListService.saveSong( user, - user.token!!, + user.token, url, youtubeVideo.name, youtubeVideo.author, @@ -228,7 +224,7 @@ suspend fun handleSongRequest( ) dispatcher.post( SongEvent( - user.token!!, + user.token, SongType.ADD, user.token, CurrentSong.getSong(user), @@ -251,7 +247,7 @@ suspend fun handleSongRequest( } dispatcher.post( SongEvent( - user.token!!, + user.token, SongType.REMOVE, null, null, @@ -281,7 +277,7 @@ suspend fun handleSongRequest( } dispatcher.post( SongEvent( - user.token!!, + user.token, SongType.NEXT, song?.uid, youtubeVideo