login method changed to chzzk login

This commit is contained in:
dalbodeule
2025-01-08 23:13:04 +09:00
parent eccf1a29bc
commit d3ed6c2d86
10 changed files with 175 additions and 118 deletions

View File

@@ -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<String, String>()
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<UserSession>("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<OAuthAccessTokenResponse.OAuth2>()
val session = call.sessions.get<UserSession>()
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<UserSession>()
if (session == null || session.state != receivedState) {
call.respond(HttpStatusCode.BadRequest, "Invalid state parameter")
return@get
}
get("/callback") {
val currentPrincipal = call.principal<OAuthAccessTokenResponse.OAuth2>()
currentPrincipal?.let { principal ->
principal.state?.let { state ->
val userInfo: NaverAPI<NaverMeAPI> = 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<TokenResponse>()
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<String>
val discordGuildList: List<String>,
)
@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<T>(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<DiscordGuildListAPI> {
DiscordRatelimits.setRateLimit(rateLimit, remaining, resetAfter)
return response.body<List<DiscordGuildListAPI>>()
}
@Serializable
data class ChzzkMeApi(
val channelId: String,
val channelName: String,
val nickname: String,
)
@Serializable
data class ChzzkApi<T>(
val code: Int,
val message: String?,
val content: T?
)
suspend fun getChzzkUser(accessToken: String): ChzzkApi<ChzzkMeApi> {
val response = applicationHttpClient.get("https://openapi.chzzk.naver.com/open/v1/users/me") {
headers {
append(HttpHeaders.Authorization, "Bearer $accessToken")
}
}
return response.body<ChzzkApi<ChzzkMeApi>>()
}
fun generateSecureRandomState(): String {
return BigInteger(130, SecureRandom()).toString(32)
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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<GetSessionDTO>()
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

View File

@@ -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
}

View File

@@ -109,7 +109,7 @@ fun Routing.wsSongListRoutes() {
webSocket("/songlist") {
val session = call.sessions.get<UserSession>()
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