com.lightningkite.lightningserver.auth.SmsAuthEndpoints.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of server-core Show documentation
Show all versions of server-core Show documentation
A set of tools to fill in/replace what Ktor is lacking in.
The newest version!
package com.lightningkite.lightningserver.auth
import com.lightningkite.lightningserver.HtmlDefaults
import com.lightningkite.lightningserver.cache.Cache
import com.lightningkite.lightningserver.core.ContentType
import com.lightningkite.lightningserver.core.ServerPathGroup
import com.lightningkite.lightningserver.exceptions.BadRequestException
import com.lightningkite.lightningserver.http.*
import com.lightningkite.lightningserver.settings.generalSettings
import com.lightningkite.lightningserver.sms.SMSClient
import com.lightningkite.lightningserver.typed.ApiExample
import com.lightningkite.lightningserver.typed.typed
import java.net.URLDecoder
import java.time.Duration
/**
* Authentication endpoints for logging in with SMS PINs.
*/
open class SmsAuthEndpoints(
val base: BaseAuthEndpoints,
val phoneAccess: UserPhoneAccess,
private val cache: () -> Cache,
private val sms: () -> SMSClient,
val pinAvailableCharacters: List = ('0'..'9').toList(),
val pinLength: Int = 6,
val pinExpiration: Duration = Duration.ofMinutes(15),
val pinMaxAttempts: Int = 5,
private val template: suspend (code: String) -> String = { code -> "Your ${generalSettings().projectName} code is ${code}. Don't share this with anyone." }
) : ServerPathGroup(base.path) {
val pin = PinHandler(
cache,
"sms",
availableCharacters = pinAvailableCharacters,
length = pinLength,
expiration = pinExpiration,
maxAttempts = pinMaxAttempts,
)
val loginSms = path("login-sms").post.typed(
summary = "SMS Login Code",
description = "Sends a text to the given phone with a PIN that can be used to log in.",
errorCases = listOf(),
examples = listOf(
ApiExample(
input = "801-369-3729",
output = Unit
),
ApiExample(
input = "+18013693729",
output = Unit,
notes = "The phone number format doesn't matter - all non-numeric characters are stripped."
),
),
successCode = HttpStatus.NoContent,
implementation = { user: Unit, phoneUnsafe: String ->
val phone = phoneUnsafe.filter { it.isDigit() }
if(phone.isEmpty()) throw BadRequestException("Blank phone number given.")
if(phone.isEmpty()) throw BadRequestException("Invalid phone number.")
val pin = pin.establish(phone)
sms().send(
to = phone,
message = template(pin)
)
Unit
}
)
val loginSmsPin = path("login-sms-pin").post.typed(
summary = "SMS PIN Login",
description = "Logs in to the given account with a PIN that was provided in a text sent earlier. Note that the PIN expires in ${pinExpiration.toMinutes()} minutes, and you are only permitted ${pinMaxAttempts} attempts.",
errorCases = listOf(),
examples = listOf(ApiExample(PhonePinLogin("801-369-3729", pin.generate()), "jwt.jwt.jwt")),
successCode = HttpStatus.OK,
implementation = { anon: Unit, input: PhonePinLogin ->
val phone = input.phone.filter { it.isDigit() }
pin.assert(phone, input.pin)
base.token(phoneAccess.byPhone(input.phone))
}
)
val loginSmsHtml = path("login-sms/").get.handler {
HttpResponse(
body = HttpContent.Text(
string = HtmlDefaults.basePage(
"""
""".trimIndent()
),
type = ContentType.Text.Html
)
)
}
val loginSmsHtmlPost = path("login-sms/form-post/").post.handler {
val phone = it.body!!.text().split('&')
.associate { it.substringBefore('=') to URLDecoder.decode(it.substringAfter('='), Charsets.UTF_8) }
.get("phone")!!.filter { it.isDigit() }
val basis = try {
loginSms.implementation(Unit, phone)
} catch (e: Exception) {
e.printStackTrace()
throw e
}
HttpResponse(
body = HttpContent.Text(
string = HtmlDefaults.basePage(
"""
Success! A text has been sent with a code to log in.
""".trimIndent()
),
type = ContentType.Text.Html
)
)
}
val loginSmsPinHtmlPost = path("login-sms/form-post-code/").post.handler {
val basis = try {
val content = it.body!!.text().split('&')
.associate { it.substringBefore('=') to URLDecoder.decode(it.substringAfter('='), Charsets.UTF_8) }
val pin = content.get("pin")!!
val phone = content.get("phone")!!.filter { it.isDigit() }
loginSmsPin.implementation(Unit, PhonePinLogin(phone, pin))
} catch (e: Exception) {
e.printStackTrace()
throw e
}
base.redirectToLanding(basis)
}
}