Add OpenID support to webapp

This commit is contained in:
eikek
2021-09-05 23:43:07 +02:00
parent f8362329a9
commit 984dda9da0
13 changed files with 138 additions and 32 deletions

View File

@ -7,9 +7,8 @@
package docspell.restserver.webapp package docspell.restserver.webapp
import docspell.backend.signup.{Config => SignupConfig} import docspell.backend.signup.{Config => SignupConfig}
import docspell.common.LenientUri import docspell.common.{Ident, LenientUri}
import docspell.restserver.{BuildInfo, Config} import docspell.restserver.{BuildInfo, Config}
import io.circe._ import io.circe._
import io.circe.generic.semiauto._ import io.circe.generic.semiauto._
import yamusca.implicits._ import yamusca.implicits._
@ -25,7 +24,8 @@ case class Flags(
maxPageSize: Int, maxPageSize: Int,
maxNoteLength: Int, maxNoteLength: Int,
showClassificationSettings: Boolean, showClassificationSettings: Boolean,
uiVersion: Int uiVersion: Int,
openIdAuth: List[Flags.OpenIdAuth]
) )
object Flags { object Flags {
@ -40,9 +40,20 @@ object Flags {
cfg.maxItemPageSize, cfg.maxItemPageSize,
cfg.maxNoteLength, cfg.maxNoteLength,
cfg.showClassificationSettings, cfg.showClassificationSettings,
uiVersion uiVersion,
cfg.openid.filter(_.enabled).map(c => OpenIdAuth(c.provider.providerId, c.display))
) )
final case class OpenIdAuth(provider: Ident, name: String)
object OpenIdAuth {
implicit val jsonDecoder: Decoder[OpenIdAuth] =
deriveDecoder[OpenIdAuth]
implicit val jsonEncoder: Encoder[OpenIdAuth] =
deriveEncoder[OpenIdAuth]
}
private def getBaseUrl(cfg: Config): String = private def getBaseUrl(cfg: Config): String =
if (cfg.baseUrl.isLocal) cfg.baseUrl.rootPathToEmpty.path.asString if (cfg.baseUrl.isLocal) cfg.baseUrl.rootPathToEmpty.path.asString
else cfg.baseUrl.rootPathToEmpty.asString else cfg.baseUrl.rootPathToEmpty.asString
@ -50,6 +61,10 @@ object Flags {
implicit val jsonEncoder: Encoder[Flags] = implicit val jsonEncoder: Encoder[Flags] =
deriveEncoder[Flags] deriveEncoder[Flags]
implicit def yamuscaIdentConverter: ValueConverter[Ident] =
ValueConverter.of(id => Value.fromString(id.id))
implicit def yamuscaOpenIdAuthConverter: ValueConverter[OpenIdAuth] =
ValueConverter.deriveConverter[OpenIdAuth]
implicit def yamuscaSignupModeConverter: ValueConverter[SignupConfig.Mode] = implicit def yamuscaSignupModeConverter: ValueConverter[SignupConfig.Mode] =
ValueConverter.of(m => Value.fromString(m.name)) ValueConverter.of(m => Value.fromString(m.name))
implicit def yamuscaUriConverter: ValueConverter[LenientUri] = implicit def yamuscaUriConverter: ValueConverter[LenientUri] =

View File

@ -87,6 +87,7 @@ module Api exposing
, mergeItems , mergeItems
, moveAttachmentBefore , moveAttachmentBefore
, newInvite , newInvite
, openIdAuthLink
, postCustomField , postCustomField
, postEquipment , postEquipment
, postNewUser , postNewUser
@ -935,6 +936,11 @@ newInvite flags req receive =
--- Login --- Login
openIdAuthLink : Flags -> String -> String
openIdAuthLink flags provider =
flags.config.baseUrl ++ "/api/v1/open/auth/openid/" ++ provider
login : Flags -> UserPass -> (Result Http.Error AuthResult -> msg) -> Cmd msg login : Flags -> UserPass -> (Result Http.Error AuthResult -> msg) -> Cmd msg
login flags up receive = login flags up receive =
Http.post Http.post

View File

@ -82,6 +82,12 @@ init key url flags_ settings =
( csm, csc ) = ( csm, csc ) =
Page.CollectiveSettings.Data.init flags Page.CollectiveSettings.Data.init flags
( loginm, loginc ) =
Page.Login.Data.init flags
(Page.loginPageReferrer page
|> Tuple.second
)
homeViewMode = homeViewMode =
if settings.searchMenuVisible then if settings.searchMenuVisible then
Page.Home.Data.SearchView Page.Home.Data.SearchView
@ -94,7 +100,7 @@ init key url flags_ settings =
, page = page , page = page
, version = Api.Model.VersionInfo.empty , version = Api.Model.VersionInfo.empty
, homeModel = Page.Home.Data.init flags homeViewMode , homeModel = Page.Home.Data.init flags homeViewMode
, loginModel = Page.Login.Data.emptyModel , loginModel = loginm
, manageDataModel = mdm , manageDataModel = mdm
, collSettingsModel = csm , collSettingsModel = csm
, userSettingsModel = um , userSettingsModel = um
@ -116,6 +122,7 @@ init key url flags_ settings =
[ Cmd.map UserSettingsMsg uc [ Cmd.map UserSettingsMsg uc
, Cmd.map ManageDataMsg mdc , Cmd.map ManageDataMsg mdc
, Cmd.map CollSettingsMsg csc , Cmd.map CollSettingsMsg csc
, Cmd.map LoginMsg loginc
] ]
) )

View File

@ -158,7 +158,7 @@ updateWithSub msg model =
LogoutResp _ -> LogoutResp _ ->
( { model | loginModel = Page.Login.Data.emptyModel } ( { model | loginModel = Page.Login.Data.emptyModel }
, Page.goto (LoginPage Nothing) , Page.goto (LoginPage ( Nothing, False ))
, Sub.none , Sub.none
) )
@ -216,20 +216,24 @@ updateWithSub msg model =
NavRequest req -> NavRequest req ->
case req of case req of
Internal url -> Internal url ->
let if String.startsWith "/app" url.path then
isCurrent = let
Page.fromUrl url isCurrent =
|> Maybe.map (\p -> p == model.page) Page.fromUrl url
|> Maybe.withDefault True |> Maybe.map (\p -> p == model.page)
in |> Maybe.withDefault True
( model in
, if isCurrent then ( model
Cmd.none , if isCurrent then
Cmd.none
else else
Nav.pushUrl model.key (Url.toString url) Nav.pushUrl model.key (Url.toString url)
, Sub.none , Sub.none
) )
else
( model, Nav.load <| Url.toString url, Sub.none )
External url -> External url ->
( model ( model

View File

@ -17,6 +17,12 @@ module Data.Flags exposing
import Api.Model.AuthResult exposing (AuthResult) import Api.Model.AuthResult exposing (AuthResult)
type alias OpenIdAuth =
{ provider : String
, name : String
}
type alias Config = type alias Config =
{ appName : String { appName : String
, baseUrl : String , baseUrl : String
@ -27,6 +33,7 @@ type alias Config =
, maxPageSize : Int , maxPageSize : Int
, maxNoteLength : Int , maxNoteLength : Int
, showClassificationSettings : Bool , showClassificationSettings : Bool
, openIdAuth : List OpenIdAuth
} }

View File

@ -26,6 +26,7 @@ gb err =
, invalidInput = "Invalid input when processing the request." , invalidInput = "Invalid input when processing the request."
, notFound = "The requested resource doesn't exist." , notFound = "The requested resource doesn't exist."
, invalidBody = \str -> "There was an error decoding the response: " ++ str , invalidBody = \str -> "There was an error decoding the response: " ++ str
, accessDenied = "Access denied"
} }
in in
errorToString texts err errorToString texts err
@ -44,6 +45,7 @@ de err =
, invalidInput = "Die Daten im Request waren ungültig." , invalidInput = "Die Daten im Request waren ungültig."
, notFound = "Die angegebene Ressource wurde nicht gefunden." , notFound = "Die angegebene Ressource wurde nicht gefunden."
, invalidBody = \str -> "Es gab einen Fehler beim Dekodieren der Antwort: " ++ str , invalidBody = \str -> "Es gab einen Fehler beim Dekodieren der Antwort: " ++ str
, accessDenied = "Zugriff verweigert"
} }
in in
errorToString texts err errorToString texts err
@ -61,6 +63,7 @@ type alias Texts =
, invalidInput : String , invalidInput : String
, notFound : String , notFound : String
, invalidBody : String -> String , invalidBody : String -> String
, accessDenied : String
} }
@ -90,6 +93,9 @@ errorToString texts error =
if sc == 404 then if sc == 404 then
texts.notFound texts.notFound
else if sc == 403 then
texts.accessDenied
else if sc >= 400 && sc < 500 then else if sc >= 400 && sc < 500 then
texts.invalidInput texts.invalidInput

View File

@ -29,6 +29,7 @@ type alias Texts =
, noAccount : String , noAccount : String
, signupLink : String , signupLink : String
, otpCode : String , otpCode : String
, or : String
} }
@ -47,6 +48,7 @@ gb =
, noAccount = "No account?" , noAccount = "No account?"
, signupLink = "Sign up!" , signupLink = "Sign up!"
, otpCode = "Authentication code" , otpCode = "Authentication code"
, or = "Or"
} }
@ -65,4 +67,5 @@ de =
, noAccount = "Kein Konto?" , noAccount = "Kein Konto?"
, signupLink = "Hier registrieren!" , signupLink = "Hier registrieren!"
, otpCode = "Authentifizierungscode" , otpCode = "Authentifizierungscode"
, or = "Oder"
} }

View File

@ -33,7 +33,7 @@ import Util.Maybe
type Page type Page
= HomePage = HomePage
| LoginPage (Maybe Page) | LoginPage ( Maybe Page, Bool )
| ManageDataPage | ManageDataPage
| CollectiveSettingPage | CollectiveSettingPage
| UserSettingPage | UserSettingPage
@ -99,10 +99,10 @@ loginPage : Page -> Page
loginPage p = loginPage p =
case p of case p of
LoginPage _ -> LoginPage _ ->
LoginPage Nothing LoginPage ( Nothing, False )
_ -> _ ->
LoginPage (Just p) LoginPage ( Just p, False )
pageName : Page -> String pageName : Page -> String
@ -144,14 +144,14 @@ pageName page =
"Item" "Item"
loginPageReferrer : Page -> Maybe Page loginPageReferrer : Page -> ( Maybe Page, Bool )
loginPageReferrer page = loginPageReferrer page =
case page of case page of
LoginPage r -> LoginPage ( r, flag ) ->
r ( r, flag )
_ -> _ ->
Nothing ( Nothing, False )
uploadId : Page -> Maybe String uploadId : Page -> Maybe String
@ -170,7 +170,7 @@ pageToString page =
HomePage -> HomePage ->
"/app/home" "/app/home"
LoginPage referer -> LoginPage ( referer, _ ) ->
case referer of case referer of
Just (LoginPage _) -> Just (LoginPage _) ->
"/app/login" "/app/login"
@ -253,7 +253,7 @@ parser =
, s pathPrefix </> s "home" , s pathPrefix </> s "home"
] ]
) )
, Parser.map LoginPage (s pathPrefix </> s "login" <?> pageQuery) , Parser.map LoginPage (s pathPrefix </> s "login" <?> loginPageParser)
, Parser.map ManageDataPage (s pathPrefix </> s "managedata") , Parser.map ManageDataPage (s pathPrefix </> s "managedata")
, Parser.map CollectiveSettingPage (s pathPrefix </> s "csettings") , Parser.map CollectiveSettingPage (s pathPrefix </> s "csettings")
, Parser.map UserSettingPage (s pathPrefix </> s "usettings") , Parser.map UserSettingPage (s pathPrefix </> s "usettings")
@ -280,6 +280,16 @@ fromString str =
fromUrl url fromUrl url
loginPageOAuthQuery : Query.Parser Bool
loginPageOAuthQuery =
Query.map Util.Maybe.nonEmpty (Query.string "openid")
loginPageParser : Query.Parser ( Maybe Page, Bool )
loginPageParser =
Query.map2 Tuple.pair pageQuery loginPageOAuthQuery
pageQuery : Query.Parser (Maybe Page) pageQuery : Query.Parser (Maybe Page)
pageQuery = pageQuery =
let let

View File

@ -11,9 +11,12 @@ module Page.Login.Data exposing
, Model , Model
, Msg(..) , Msg(..)
, emptyModel , emptyModel
, init
) )
import Api
import Api.Model.AuthResult exposing (AuthResult) import Api.Model.AuthResult exposing (AuthResult)
import Data.Flags exposing (Flags)
import Http import Http
import Page exposing (Page(..)) import Page exposing (Page(..))
@ -51,6 +54,19 @@ emptyModel =
} }
init : Flags -> Bool -> ( Model, Cmd Msg )
init flags oauth =
let
cmd =
if oauth then
Api.loginSession flags AuthResp
else
Cmd.none
in
( emptyModel, cmd )
type Msg type Msg
= SetUsername String = SetUsername String
| SetPassword String | SetPassword String

View File

@ -15,8 +15,8 @@ import Page.Login.Data exposing (..)
import Ports import Ports
update : Maybe Page -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe AuthResult ) update : ( Maybe Page, Bool ) -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe AuthResult )
update referrer flags msg model = update ( referrer, oauth ) flags msg model =
case msg of case msg of
SetUsername str -> SetUsername str ->
( { model | username = str }, Cmd.none, Nothing ) ( { model | username = str }, Cmd.none, Nothing )

View File

@ -7,8 +7,10 @@
module Page.Login.View2 exposing (viewContent, viewSidebar) module Page.Login.View2 exposing (viewContent, viewSidebar)
import Api
import Api.Model.AuthResult exposing (AuthResult) import Api.Model.AuthResult exposing (AuthResult)
import Api.Model.VersionInfo exposing (VersionInfo) import Api.Model.VersionInfo exposing (VersionInfo)
import Comp.Basic as B
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
@ -53,6 +55,7 @@ viewContent texts flags versionInfo _ model =
StepLogin -> StepLogin ->
loginForm texts flags model loginForm texts flags model
, openIdLinks texts flags
] ]
, a , a
[ class "inline-flex items-center mt-4 text-xs opacity-50 hover:opacity-90" [ class "inline-flex items-center mt-4 text-xs opacity-50 hover:opacity-90"
@ -72,6 +75,35 @@ viewContent texts flags versionInfo _ model =
] ]
openIdLinks : Texts -> Flags -> Html Msg
openIdLinks texts flags =
let
renderLink prov =
a
[ href (Api.openIdAuthLink flags prov.provider)
, class S.link
]
[ i [ class "fab fa-openid mr-1" ] []
, text prov.name
]
in
case flags.config.openIdAuth of
[] ->
span [ class "hidden" ] []
provs ->
div [ class "mt-3" ]
[ B.horizontalDivider
{ label = texts.or
, topCss = "w-2/3 mb-4 hidden md:inline-flex w-full"
, labelCss = "px-4 bg-gray-200 bg-opacity-50"
, lineColor = "bg-gray-300 dark:bg-bluegray-600"
}
, div [ class "flex flex-row space-x-4 items-center justify-center" ]
(List.map renderLink provs)
]
otpForm : Texts -> Flags -> Model -> AuthResult -> Html Msg otpForm : Texts -> Flags -> Model -> AuthResult -> Html Msg
otpForm texts flags model acc = otpForm texts flags model acc =
Html.form Html.form

View File

@ -97,7 +97,7 @@ update flags msg model =
cmd = cmd =
if r.success then if r.success then
Page.goto (LoginPage Nothing) Page.goto (LoginPage ( Nothing, False ))
else else
Cmd.none Cmd.none

View File

@ -232,7 +232,7 @@ viewContent texts flags _ model =
[ text texts.alreadySignedUp [ text texts.alreadySignedUp
] ]
, a , a
[ Page.href (LoginPage Nothing) [ Page.href (LoginPage ( Nothing, False ))
, class ("ml-2" ++ S.link) , class ("ml-2" ++ S.link)
] ]
[ i [ class "fa fa-user-plus mr-1" ] [] [ i [ class "fa fa-user-plus mr-1" ] []