@@ -6,26 +6,51 @@ import com.panomc.platform.db.model.Server
66import com.panomc.platform.server.ServerEvent
77import com.panomc.platform.server.event.request.GenerateLinkCodeEventRequest
88import com.panomc.platform.server.event.response.GenerateLinkCodeEventResponse
9- import java.util.Random
9+ import com.panomc.platform.server.event.response.GenerateLinkCodeStatus
10+ import java.security.SecureRandom
1011
1112@Event
12- class GenerateLinkCodeEvent (private val databaseManager : DatabaseManager ) :
13+ class GenerateLinkCodeEvent (private val databaseManager : DatabaseManager ) :
1314 ServerEvent <GenerateLinkCodeEventRequest , GenerateLinkCodeEventResponse >() {
1415
15- override suspend fun handle (request : GenerateLinkCodeEventRequest , server : Server ): GenerateLinkCodeEventResponse ? {
16+ companion object {
17+ // Lifetime of a generated link code in milliseconds. Must stay in sync with
18+ // VerifyLinkCodeAPI's expiry check (Calendar.SECOND, 30).
19+ private const val LINK_CODE_TTL_MS = 30_000L
20+ }
21+
22+ private val secureRandom = SecureRandom ()
23+
24+ override suspend fun handle (request : GenerateLinkCodeEventRequest , server : Server ): GenerateLinkCodeEventResponse {
1625 val username = request.username
1726 val sqlClient = databaseManager.getSqlClient()
1827
19- val user = databaseManager.userDao.getByUsername(username, sqlClient) ? : return null
20- // If user not found, return null (no response). Or strictly speaking, we could return error response if protocol allows.
21- // But for now, silence is fine, or simple error string in code.
22- // Or create user? No, they should join first.
28+ val user = databaseManager.userDao.getByUsername(username, sqlClient)
29+ ? : return GenerateLinkCodeEventResponse (status = GenerateLinkCodeStatus .USER_NOT_FOUND )
30+
31+ // Mirror LoginAPI's LinkCodeRequired condition: /link only makes sense for users
32+ // that have neither password nor email set. Any other state means the user can
33+ // log in via the regular flow and shouldn't be issued a new link code.
34+ val hasPassword = databaseManager.userDao.hasPassword(user.id, sqlClient)
35+ val hasEmail = ! user.email.isNullOrEmpty()
36+ if (hasPassword || hasEmail) {
37+ return GenerateLinkCodeEventResponse (status = GenerateLinkCodeStatus .ALREADY_REGISTERED )
38+ }
39+
40+ val now = System .currentTimeMillis()
2341
24- val code = String .format(" %06d" , Random ().nextInt(999999 ))
25- val createdAt = System .currentTimeMillis()
42+ // Reuse an existing code while it is still valid so spamming /link does not
43+ // rotate the code that the user is currently trying to enter on the website.
44+ val existing = databaseManager.userDao.getLinkCode(username, sqlClient)
45+ val existingCode = existing?.first
46+ val existingCreatedAt = existing?.second ? : 0L
47+ if (! existingCode.isNullOrEmpty() && now - existingCreatedAt < LINK_CODE_TTL_MS ) {
48+ return GenerateLinkCodeEventResponse (code = existingCode)
49+ }
2650
27- databaseManager.userDao.setLinkCode(username, code, createdAt, sqlClient)
51+ val code = String .format(" %06d" , secureRandom.nextInt(1_000_000 ))
52+ databaseManager.userDao.setLinkCode(username, code, now, sqlClient)
2853
29- return GenerateLinkCodeEventResponse (code)
54+ return GenerateLinkCodeEventResponse (code = code )
3055 }
3156}
0 commit comments