feat: auth and redirect landing create user
This commit is contained in:
parent
f742acd6f9
commit
ef6646e004
|
@ -623,3 +623,7 @@ video {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(37 99 235 / var(--tw-text-opacity));
|
color: rgb(37 99 235 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package example
|
||||||
|
|
||||||
import AuthService._
|
import AuthService._
|
||||||
import example.pocketbase.Api
|
import example.pocketbase.Api
|
||||||
|
import upickle.default._
|
||||||
|
import example.pocketbase.Models._
|
||||||
|
|
||||||
case class AuthService()(implicit cc: castor.Context, log: cask.Logger)
|
case class AuthService()(implicit cc: castor.Context, log: cask.Logger)
|
||||||
extends cask.Routes {
|
extends cask.Routes {
|
||||||
|
@ -23,14 +25,18 @@ case class AuthService()(implicit cc: castor.Context, log: cask.Logger)
|
||||||
@cask.get("/login")
|
@cask.get("/login")
|
||||||
def getLoginPage() = {
|
def getLoginPage() = {
|
||||||
// render auth page with the available oauth providers
|
// render auth page with the available oauth providers
|
||||||
val authOptions = pocketbaseApi.listAuthMethods().authProviders
|
val authOptions = pocketbaseApi.listAuthMethods()
|
||||||
|
|
||||||
val options = s"got following auth opitons: $authOptions"
|
val options = s"got following auth opitons: $authOptions"
|
||||||
// save states and verifiers into cookie
|
// save states and verifiers into cookie
|
||||||
|
|
||||||
val githubOption = authOptions.find(_.name == "github")
|
val oauthVerificationCookie = authOptions.toOauthCookieInfo()
|
||||||
|
val yoyo = write(oauthVerificationCookie)
|
||||||
|
|
||||||
val githubRedirect = githubOption.map(_.authUrl).getOrElse("") ++ getRedirectUrl("github")
|
val githubOption = authOptions.authProviders.find(_.name == "github")
|
||||||
|
|
||||||
|
val githubRedirect =
|
||||||
|
githubOption.map(_.authUrl).getOrElse("") ++ getRedirectUrl("github")
|
||||||
|
|
||||||
val html = s"""
|
val html = s"""
|
||||||
<h1>good enough, right</h1>
|
<h1>good enough, right</h1>
|
||||||
|
@ -41,12 +47,59 @@ case class AuthService()(implicit cc: castor.Context, log: cask.Logger)
|
||||||
cask.Response(
|
cask.Response(
|
||||||
html,
|
html,
|
||||||
headers = Seq("Content-Type" -> "text/html;charset=UTF-8"),
|
headers = Seq("Content-Type" -> "text/html;charset=UTF-8"),
|
||||||
|
cookies = Seq(cask.Cookie(name = oauthVerifiersCookieName, value = yoyo))
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @oauthVerifiers
|
||||||
|
* \- required cookie from start of oauth initialization cask will fail
|
||||||
|
* request if this is not present
|
||||||
|
*/
|
||||||
@cask.get(s"${baseRedirectUrl}/:provider")
|
@cask.get(s"${baseRedirectUrl}/:provider")
|
||||||
def receiveOauthRedirect(provider: String, state: String, code: String) = {
|
def receiveOauthRedirect(
|
||||||
println(s"received redirect for $provider with state: $state and code: $code")
|
provider: String,
|
||||||
|
state: String,
|
||||||
|
code: String,
|
||||||
|
request: cask.Request
|
||||||
|
) = {
|
||||||
|
println(
|
||||||
|
s"received redirect for $provider with state: $state and code: $code"
|
||||||
|
)
|
||||||
|
|
||||||
|
val authVerifierOpt = request.cookies
|
||||||
|
.get(oauthVerifiersCookieName)
|
||||||
|
.map(cookie => read[OauthInfoCookie](cookie.value))
|
||||||
|
.flatMap(_.providersVerification.find(_.name == provider))
|
||||||
|
|
||||||
|
val resultOpt = for {
|
||||||
|
authVerifier <- authVerifierOpt
|
||||||
|
_ <- Some(()).filter(_ => state == authVerifier.state)
|
||||||
|
pocketbaseAuthResult <- pocketbaseApi
|
||||||
|
.authWithOauth(
|
||||||
|
provider = provider,
|
||||||
|
code = code,
|
||||||
|
verifier = authVerifier.codeVerifier,
|
||||||
|
redirectUrl = getRedirectUrl(provider)
|
||||||
|
)
|
||||||
|
.toOption
|
||||||
|
} yield pocketbaseAuthResult
|
||||||
|
|
||||||
|
val okMessageFirst = resultOpt match {
|
||||||
|
case None =>
|
||||||
|
// i guess with the SSR i'll need to return message about unsuccessful auth?
|
||||||
|
s"""
|
||||||
|
<h1>Auth unsuccessful</h1>
|
||||||
|
"""
|
||||||
|
case Some(result) =>
|
||||||
|
// this is already fully successful auth
|
||||||
|
s"""
|
||||||
|
<h1>Ok, good</h1>
|
||||||
|
<p>user should be already created, current jwt : ${result.token}</p>
|
||||||
|
<p>the account is on ${result.record.email} and ${result.record.username}</p>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
*
|
*
|
||||||
get provider from path param, get verifiers and state from cookie,
|
get provider from path param, get verifiers and state from cookie,
|
||||||
|
@ -59,7 +112,18 @@ and delete the state\verifiers cookie
|
||||||
but then what? i guess call for redirect to root page again?
|
but then what? i guess call for redirect to root page again?
|
||||||
which should trigger auth check and main page render?
|
which should trigger auth check and main page render?
|
||||||
*/
|
*/
|
||||||
s"received redirect for $provider with state: $state and code: $code"
|
import java.time.Instant
|
||||||
|
cask.Response(
|
||||||
|
okMessageFirst,
|
||||||
|
headers = Seq("Content-Type" -> "text/html;charset=UTF-8"),
|
||||||
|
cookies = Seq(
|
||||||
|
cask.Cookie(
|
||||||
|
name = oauthVerifiersCookieName,
|
||||||
|
value = "",
|
||||||
|
expires = Instant.now()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize()
|
initialize()
|
||||||
|
@ -67,6 +131,12 @@ which should trigger auth check and main page render?
|
||||||
|
|
||||||
object AuthService {
|
object AuthService {
|
||||||
val authCookieName = "auth"
|
val authCookieName = "auth"
|
||||||
|
// this is to share info between /auth page and /redirect-landing
|
||||||
|
// can be improved by generating same state and codeVerifier for all oauth links
|
||||||
|
// and can also be stored purely on the backend - a tad more secure, i guess
|
||||||
|
// but if cookie is under https, should be ok
|
||||||
|
val oauthVerifiersCookieName = "oauthVerifiers"
|
||||||
|
|
||||||
val pocketbaseApi = Api("http://127.0.0.1:8090")
|
val pocketbaseApi = Api("http://127.0.0.1:8090")
|
||||||
val selfUri = "http://127.0.0.1:8080"
|
val selfUri = "http://127.0.0.1:8080"
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ final case class Api(pocketbaseUrl: String, usersCollection: String = "users") {
|
||||||
"provider" -> provider,
|
"provider" -> provider,
|
||||||
"code" -> code,
|
"code" -> code,
|
||||||
"codeVerifier" -> verifier,
|
"codeVerifier" -> verifier,
|
||||||
"redirectUri" -> redirectUrl
|
"redirectUrl" -> redirectUrl
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,22 @@ import upickle.default._
|
||||||
|
|
||||||
object Models {
|
object Models {
|
||||||
|
|
||||||
|
final case class AvailableAuthMethods(
|
||||||
|
usernamePassword: Boolean,
|
||||||
|
emailPassword: Boolean,
|
||||||
|
authProviders: List[AuthProviderInfo]
|
||||||
|
) derives ReadWriter {
|
||||||
|
def toOauthCookieInfo() = OauthInfoCookie(
|
||||||
|
providersVerification = authProviders.map(provider =>
|
||||||
|
AuthProviderVerification(
|
||||||
|
name = provider.name,
|
||||||
|
state = provider.state,
|
||||||
|
codeVerifier = provider.codeVerifier
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
final case class AuthProviderInfo(
|
final case class AuthProviderInfo(
|
||||||
name: String,
|
name: String,
|
||||||
state: String,
|
state: String,
|
||||||
|
@ -13,12 +29,15 @@ object Models {
|
||||||
authUrl: String
|
authUrl: String
|
||||||
) derives ReadWriter
|
) derives ReadWriter
|
||||||
|
|
||||||
final case class AvailableAuthMethods(
|
final case class OauthInfoCookie(
|
||||||
usernamePassword: Boolean,
|
providersVerification: List[AuthProviderVerification]
|
||||||
emailPassword: Boolean,
|
|
||||||
authProviders: List[AuthProviderInfo]
|
|
||||||
) derives ReadWriter
|
) derives ReadWriter
|
||||||
|
|
||||||
|
final case class AuthProviderVerification(
|
||||||
|
name: String,
|
||||||
|
state: String,
|
||||||
|
codeVerifier: String,
|
||||||
|
) derives ReadWriter
|
||||||
|
|
||||||
// auth methods
|
// auth methods
|
||||||
/*
|
/*
|
||||||
|
@ -39,6 +58,11 @@ object Models {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
final case class AuthReply(
|
||||||
|
token: String,
|
||||||
|
record: BaseAccountData
|
||||||
|
) derives ReadWriter
|
||||||
|
|
||||||
final case class BaseAccountData(
|
final case class BaseAccountData(
|
||||||
id: String,
|
id: String,
|
||||||
collectionId: String,
|
collectionId: String,
|
||||||
|
@ -51,11 +75,6 @@ object Models {
|
||||||
emailVisibility: Boolean
|
emailVisibility: Boolean
|
||||||
) derives ReadWriter
|
) derives ReadWriter
|
||||||
|
|
||||||
final case class AuthReply(
|
|
||||||
token: String,
|
|
||||||
record: BaseAccountData
|
|
||||||
) derives ReadWriter
|
|
||||||
|
|
||||||
// auth reply
|
// auth reply
|
||||||
/*
|
/*
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue