mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Add OpenID support to webapp
This commit is contained in:
@ -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] =
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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,6 +216,7 @@ updateWithSub msg model =
|
|||||||
NavRequest req ->
|
NavRequest req ->
|
||||||
case req of
|
case req of
|
||||||
Internal url ->
|
Internal url ->
|
||||||
|
if String.startsWith "/app" url.path then
|
||||||
let
|
let
|
||||||
isCurrent =
|
isCurrent =
|
||||||
Page.fromUrl url
|
Page.fromUrl url
|
||||||
@ -231,6 +232,9 @@ updateWithSub msg model =
|
|||||||
, Sub.none
|
, Sub.none
|
||||||
)
|
)
|
||||||
|
|
||||||
|
else
|
||||||
|
( model, Nav.load <| Url.toString url, Sub.none )
|
||||||
|
|
||||||
External url ->
|
External url ->
|
||||||
( model
|
( model
|
||||||
, Nav.load url
|
, Nav.load url
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 )
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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" ] []
|
||||||
|
Reference in New Issue
Block a user