mirror of
https://github.com/idanoo/autobrr
synced 2025-07-22 16:29:12 +00:00
feat(oidc): show profile pic if present (#2006)
* feat(oidc): fetch profile picture * small imprvements * Add link to provider * fix(rightnav): add cursor-pointer on hover * adjust picture border and layout in RightNav and Account components * cleanup * oidc claims struct * check if profile_picture exists * simplify profile picture error handling * adhere to autobrr log style * fix: remove unused imports --------- Co-authored-by: ze0s <43699394+zze0s@users.noreply.github.com>
This commit is contained in:
parent
a8c4114d6d
commit
1c23b5df57
8 changed files with 147 additions and 48 deletions
|
@ -39,6 +39,17 @@ type OIDCHandler struct {
|
||||||
cookieStore *sessions.CookieStore
|
cookieStore *sessions.CookieStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OIDCClaims represents the claims returned from the OIDC provider
|
||||||
|
type OIDCClaims struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Username string `json:"preferred_username"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
GivenName string `json:"given_name"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Picture string `json:"picture"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewOIDCHandler(cfg *domain.Config, log zerolog.Logger) (*OIDCHandler, error) {
|
func NewOIDCHandler(cfg *domain.Config, log zerolog.Logger) (*OIDCHandler, error) {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Bool("oidc_enabled", cfg.OIDCEnabled).
|
Bool("oidc_enabled", cfg.OIDCEnabled).
|
||||||
|
@ -180,25 +191,25 @@ func (h *OIDCHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, authURL, http.StatusFound)
|
http.Redirect(w, r, authURL, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *OIDCHandler) HandleCallback(w http.ResponseWriter, r *http.Request) (string, error) {
|
func (h *OIDCHandler) HandleCallback(w http.ResponseWriter, r *http.Request) (*OIDCClaims, error) {
|
||||||
h.log.Debug().Msg("handling OIDC callback")
|
h.log.Debug().Msg("handling OIDC callback")
|
||||||
|
|
||||||
// get state from session
|
// get state from session
|
||||||
session, err := h.cookieStore.Get(r, "oidc_state")
|
session, err := h.cookieStore.Get(r, "oidc_state")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.log.Error().Err(err).Msg("state session not found")
|
h.log.Error().Err(err).Msg("state session not found")
|
||||||
return "", errors.New("state session not found")
|
return nil, errors.New("state session not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedState, ok := session.Values["state"].(string)
|
expectedState, ok := session.Values["state"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
h.log.Error().Msg("state not found in session")
|
h.log.Error().Msg("state not found in session")
|
||||||
return "", errors.New("state not found in session")
|
return nil, errors.New("state not found in session")
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.URL.Query().Get("state") != expectedState {
|
if r.URL.Query().Get("state") != expectedState {
|
||||||
h.log.Error().Str("expected", expectedState).Str("got", r.URL.Query().Get("state")).Msg("state did not match")
|
h.log.Error().Str("expected", expectedState).Str("got", r.URL.Query().Get("state")).Msg("state did not match")
|
||||||
return "", errors.New("state did not match")
|
return nil, errors.New("state did not match")
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear the state session after use
|
// clear the state session after use
|
||||||
|
@ -210,60 +221,52 @@ func (h *OIDCHandler) HandleCallback(w http.ResponseWriter, r *http.Request) (st
|
||||||
code := r.URL.Query().Get("code")
|
code := r.URL.Query().Get("code")
|
||||||
if code == "" {
|
if code == "" {
|
||||||
h.log.Error().Msg("authorization code is missing from callback request")
|
h.log.Error().Msg("authorization code is missing from callback request")
|
||||||
return "", errors.New("authorization code is missing from callback request")
|
return nil, errors.New("authorization code is missing from callback request")
|
||||||
}
|
}
|
||||||
|
|
||||||
oauth2Token, err := h.oauthConfig.Exchange(r.Context(), code)
|
oauth2Token, err := h.oauthConfig.Exchange(r.Context(), code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.log.Error().Err(err).Msg("failed to exchange token")
|
h.log.Error().Err(err).Msg("failed to exchange token")
|
||||||
return "", errors.Wrap(err, "failed to exchange token")
|
return nil, errors.Wrap(err, "failed to exchange token")
|
||||||
}
|
}
|
||||||
|
|
||||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
h.log.Error().Msg("no id_token found in oauth2 token")
|
h.log.Error().Msg("no id_token found in oauth2 token")
|
||||||
return "", errors.New("no id_token found in oauth2 token")
|
return nil, errors.New("no id_token found in oauth2 token")
|
||||||
}
|
}
|
||||||
|
|
||||||
idToken, err := h.verifier.Verify(r.Context(), rawIDToken)
|
idToken, err := h.verifier.Verify(r.Context(), rawIDToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.log.Error().Err(err).Msg("failed to verify ID Token")
|
h.log.Error().Err(err).Msg("failed to verify ID Token")
|
||||||
return "", errors.Wrap(err, "failed to verify ID Token")
|
return nil, errors.Wrap(err, "failed to verify ID Token")
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims struct {
|
var claims OIDCClaims
|
||||||
Email string `json:"email"`
|
|
||||||
Username string `json:"preferred_username"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
GivenName string `json:"given_name"`
|
|
||||||
Nickname string `json:"nickname"`
|
|
||||||
Sub string `json:"sub"`
|
|
||||||
}
|
|
||||||
if err := idToken.Claims(&claims); err != nil {
|
if err := idToken.Claims(&claims); err != nil {
|
||||||
h.log.Error().Err(err).Msg("failed to parse claims")
|
h.log.Error().Err(err).Msg("failed to parse claims")
|
||||||
return "", errors.Wrap(err, "failed to parse claims")
|
return nil, errors.Wrap(err, "failed to parse claims")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try different claims in order of preference for username
|
// Try different claims in order of preference for username
|
||||||
// This is solely used for frontend display
|
// This is solely used for frontend display
|
||||||
username := claims.Username
|
if claims.Username == "" {
|
||||||
if username == "" {
|
|
||||||
if claims.Nickname != "" {
|
if claims.Nickname != "" {
|
||||||
username = claims.Nickname
|
claims.Username = claims.Nickname
|
||||||
} else if claims.Name != "" {
|
} else if claims.Name != "" {
|
||||||
username = claims.Name
|
claims.Username = claims.Name
|
||||||
} else if claims.Email != "" {
|
} else if claims.Email != "" {
|
||||||
username = claims.Email
|
claims.Username = claims.Email
|
||||||
} else if claims.Sub != "" {
|
} else if claims.Sub != "" {
|
||||||
username = claims.Sub
|
claims.Username = claims.Sub
|
||||||
} else {
|
} else {
|
||||||
username = "oidc_user"
|
claims.Username = "oidc_user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h.log.Debug().Str("username", username).Str("email", claims.Email).Str("nickname", claims.Nickname).Str("name", claims.Name).Str("sub", claims.Sub).Msg("successfully processed OIDC claims")
|
h.log.Debug().Str("username", claims.Username).Str("email", claims.Email).Str("nickname", claims.Nickname).Str("name", claims.Name).Str("sub", claims.Sub).Str("picture", claims.Picture).Msg("successfully processed OIDC claims")
|
||||||
|
|
||||||
return username, nil
|
return &claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateRandomState() string {
|
func generateRandomState() string {
|
||||||
|
@ -288,6 +291,7 @@ type GetConfigResponse struct {
|
||||||
AuthorizationURL string `json:"authorizationUrl"`
|
AuthorizationURL string `json:"authorizationUrl"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
DisableBuiltInLogin bool `json:"disableBuiltInLogin"`
|
DisableBuiltInLogin bool `json:"disableBuiltInLogin"`
|
||||||
|
IssuerURL string `json:"issuerUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *OIDCHandler) GetConfigResponse() GetConfigResponse {
|
func (h *OIDCHandler) GetConfigResponse() GetConfigResponse {
|
||||||
|
@ -295,19 +299,21 @@ func (h *OIDCHandler) GetConfigResponse() GetConfigResponse {
|
||||||
return GetConfigResponse{
|
return GetConfigResponse{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
DisableBuiltInLogin: false,
|
DisableBuiltInLogin: false,
|
||||||
|
IssuerURL: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state := generateRandomState()
|
state := generateRandomState()
|
||||||
authURL := h.oauthConfig.AuthCodeURL(state)
|
authURL := h.oauthConfig.AuthCodeURL(state)
|
||||||
|
|
||||||
h.log.Debug().Bool("enabled", h.config.Enabled).Str("authorization_url", authURL).Str("state", state).Bool("disable_built_in_login", h.config.DisableBuiltInLogin).Msg("returning OIDC config response")
|
h.log.Debug().Bool("enabled", h.config.Enabled).Str("authorization_url", authURL).Str("state", state).Bool("disable_built_in_login", h.config.DisableBuiltInLogin).Str("issuer_url", h.config.Issuer).Msg("returning OIDC config response")
|
||||||
|
|
||||||
return GetConfigResponse{
|
return GetConfigResponse{
|
||||||
Enabled: h.config.Enabled,
|
Enabled: h.config.Enabled,
|
||||||
AuthorizationURL: authURL,
|
AuthorizationURL: authURL,
|
||||||
State: state,
|
State: state,
|
||||||
DisableBuiltInLogin: h.config.DisableBuiltInLogin,
|
DisableBuiltInLogin: h.config.DisableBuiltInLogin,
|
||||||
|
IssuerURL: h.config.Issuer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -217,6 +217,10 @@ func (h authHandler) validate(w http.ResponseWriter, r *http.Request) {
|
||||||
"auth_method": session.Values["auth_method"],
|
"auth_method": session.Values["auth_method"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if profilePicture, ok := session.Values["profile_picture"].(string); ok && profilePicture != "" {
|
||||||
|
response["profile_picture"] = profilePicture
|
||||||
|
}
|
||||||
|
|
||||||
h.encoder.StatusResponse(w, http.StatusOK, response)
|
h.encoder.StatusResponse(w, http.StatusOK, response)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -279,7 +283,7 @@ func (h authHandler) handleOIDCCallback(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username, err := h.oidcHandler.HandleCallback(w, r)
|
claims, err := h.oidcHandler.HandleCallback(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.encoder.StatusError(w, http.StatusUnauthorized, errors.Wrap(err, "OIDC authentication failed"))
|
h.encoder.StatusError(w, http.StatusUnauthorized, errors.Wrap(err, "OIDC authentication failed"))
|
||||||
return
|
return
|
||||||
|
@ -288,7 +292,7 @@ func (h authHandler) handleOIDCCallback(w http.ResponseWriter, r *http.Request)
|
||||||
// Create new session
|
// Create new session
|
||||||
session, err := h.cookieStore.Get(r, "user_session")
|
session, err := h.cookieStore.Get(r, "user_session")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.log.Error().Err(err).Msgf("Auth: Failed to create cookies with attempt username: [%s] ip: %s", username, r.RemoteAddr)
|
h.log.Error().Err(err).Msgf("Auth: Failed to create cookies with attempt username: [%s] ip: %s", claims.Username, r.RemoteAddr)
|
||||||
h.encoder.StatusError(w, http.StatusInternalServerError, errors.New("could not create cookies"))
|
h.encoder.StatusError(w, http.StatusInternalServerError, errors.New("could not create cookies"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -296,8 +300,12 @@ func (h authHandler) handleOIDCCallback(w http.ResponseWriter, r *http.Request)
|
||||||
// Set user as authenticated
|
// Set user as authenticated
|
||||||
session.Values["authenticated"] = true
|
session.Values["authenticated"] = true
|
||||||
session.Values["created"] = time.Now().Unix()
|
session.Values["created"] = time.Now().Unix()
|
||||||
session.Values["username"] = username
|
session.Values["username"] = claims.Username
|
||||||
session.Values["auth_method"] = "oidc"
|
session.Values["auth_method"] = "oidc"
|
||||||
|
if claims.Picture != "" {
|
||||||
|
session.Values["profile_picture"] = claims.Picture
|
||||||
|
h.log.Debug().Str("profile_picture", claims.Picture).Msg("storing profile picture URL in session")
|
||||||
|
}
|
||||||
|
|
||||||
// Set cookie options
|
// Set cookie options
|
||||||
session.Options.HttpOnly = true
|
session.Options.HttpOnly = true
|
||||||
|
|
|
@ -13,6 +13,7 @@ type Primitive = string | number | boolean | symbol | undefined;
|
||||||
type ValidateResponse = {
|
type ValidateResponse = {
|
||||||
username?: AuthInfo['username'];
|
username?: AuthInfo['username'];
|
||||||
auth_method?: AuthInfo['authMethod'];
|
auth_method?: AuthInfo['authMethod'];
|
||||||
|
profile_picture?: AuthInfo['profilePicture'];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HttpConfig {
|
interface HttpConfig {
|
||||||
|
@ -262,10 +263,10 @@ export const APIClient = {
|
||||||
{ body: req }),
|
{ body: req }),
|
||||||
getOIDCConfig: async () => {
|
getOIDCConfig: async () => {
|
||||||
try {
|
try {
|
||||||
return await appClient.Get<{ enabled: boolean; authorizationUrl: string; state: string; disableBuiltInLogin: boolean }>("api/auth/oidc/config");
|
return await appClient.Get<{ enabled: boolean; authorizationUrl: string; state: string; disableBuiltInLogin: boolean; issuerUrl: string }>("api/auth/oidc/config");
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error && error.message?.includes('404')) {
|
if (error instanceof Error && error.message?.includes('404')) {
|
||||||
return { enabled: false, authorizationUrl: '', state: '', disableBuiltInLogin: false };
|
return { enabled: false, authorizationUrl: '', state: '', disableBuiltInLogin: false, issuerUrl: '' };
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { AuthContext, SettingsContext } from "@utils/Context";
|
||||||
|
|
||||||
export const RightNav = (props: RightNavProps) => {
|
export const RightNav = (props: RightNavProps) => {
|
||||||
const [settings, setSettings] = SettingsContext.use();
|
const [settings, setSettings] = SettingsContext.use();
|
||||||
|
|
||||||
const auth = AuthContext.get();
|
const auth = AuthContext.get();
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
|
@ -35,7 +34,7 @@ export const RightNav = (props: RightNavProps) => {
|
||||||
<div className="mt-1 items-center">
|
<div className="mt-1 items-center">
|
||||||
<button
|
<button
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
className="p-1 rounded-full focus:outline-hidden focus:none transition duration-100 ease-out transform hover:bg-gray-200 dark:hover:bg-gray-800 hover:scale-100"
|
className="p-1 rounded-full hover:cursor-pointer focus:outline-hidden focus:none transition duration-100 ease-out transform hover:bg-gray-200 dark:hover:bg-gray-800 hover:scale-100"
|
||||||
title={settings.darkTheme ? "Switch to light mode (currently dark mode)" : "Switch to dark mode (currently light mode)"}
|
title={settings.darkTheme ? "Switch to light mode (currently dark mode)" : "Switch to dark mode (currently light mode)"}
|
||||||
>
|
>
|
||||||
{settings.darkTheme ? (
|
{settings.darkTheme ? (
|
||||||
|
@ -56,25 +55,34 @@ export const RightNav = (props: RightNavProps) => {
|
||||||
"transition duration-200"
|
"transition duration-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="hidden text-sm font-medium sm:block">
|
<span className="hidden hover:cursor-pointer text-sm font-medium sm:flex items-center">
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
Open user menu for{" "}
|
Open user menu for{" "}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">{auth.username}</span>
|
||||||
{auth.username}
|
{auth.authMethod === 'oidc' ? (
|
||||||
{auth.authMethod === 'oidc' ? (
|
auth.profilePicture ? (
|
||||||
|
<div className="relative flex-shrink-0 ml-2 w-6 h-6 overflow-hidden rounded-full ring-1 ring-white dark:ring-gray-700">
|
||||||
|
<img
|
||||||
|
src={auth.profilePicture}
|
||||||
|
alt={`${auth.username}'s profile`}
|
||||||
|
className="object-cover w-full h-full transition-opacity duration-200"
|
||||||
|
onError={() => auth.profilePicture = undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faOpenid}
|
icon={faOpenid}
|
||||||
className="inline ml-1 h-4 w-4 text-gray-500 dark:text-gray-500"
|
className="inline ml-1 h-4 w-4 text-gray-500 dark:text-gray-500"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
) : (
|
)
|
||||||
<UserIcon
|
) : (
|
||||||
className="inline ml-1 h-5 w-5"
|
<UserIcon
|
||||||
aria-hidden="true"
|
className="inline ml-1 h-5 w-5"
|
||||||
/>
|
aria-hidden="true"
|
||||||
)}
|
/>
|
||||||
</span>
|
)}
|
||||||
</span>
|
</span>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<Transition
|
<Transition
|
||||||
|
|
|
@ -304,10 +304,19 @@ export const AuthRoute = createRoute({
|
||||||
if (!AuthContext.get().isLoggedIn) {
|
if (!AuthContext.get().isLoggedIn) {
|
||||||
try {
|
try {
|
||||||
const response = await APIClient.auth.validate();
|
const response = await APIClient.auth.validate();
|
||||||
|
// Also get OIDC config if needed
|
||||||
|
let issuerUrl;
|
||||||
|
if (response.auth_method === 'oidc') {
|
||||||
|
const oidcConfig = await APIClient.auth.getOIDCConfig();
|
||||||
|
issuerUrl = oidcConfig.issuerUrl;
|
||||||
|
}
|
||||||
|
|
||||||
AuthContext.set({
|
AuthContext.set({
|
||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
username: response.username || 'unknown',
|
username: response.username || 'unknown',
|
||||||
authMethod: response.auth_method
|
authMethod: response.auth_method,
|
||||||
|
profilePicture: response.profile_picture,
|
||||||
|
issuerUrl: issuerUrl
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.debug("Authentication validation failed:", error);
|
console.debug("Authentication validation failed:", error);
|
||||||
|
|
|
@ -30,6 +30,7 @@ type LoginFormFields = {
|
||||||
type ValidateResponse = {
|
type ValidateResponse = {
|
||||||
username?: AuthInfo['username'];
|
username?: AuthInfo['username'];
|
||||||
auth_method?: AuthInfo['authMethod'];
|
auth_method?: AuthInfo['authMethod'];
|
||||||
|
profile_picture?: AuthInfo['profilePicture'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
|
@ -85,7 +86,8 @@ export const Login = () => {
|
||||||
setAuth({
|
setAuth({
|
||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
username: response.username || 'unknown',
|
username: response.username || 'unknown',
|
||||||
authMethod: response.auth_method || (oidcConfig?.enabled ? 'oidc' : 'password')
|
authMethod: response.auth_method || (oidcConfig?.enabled ? 'oidc' : 'password'),
|
||||||
|
profilePicture: response.profile_picture,
|
||||||
});
|
});
|
||||||
router.invalidate();
|
router.invalidate();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
|
|
@ -160,6 +160,21 @@ function Credentials() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function OIDCAccount() {
|
function OIDCAccount() {
|
||||||
|
const auth = AuthContext.get();
|
||||||
|
|
||||||
|
// Helper function to format the issuer URL for display
|
||||||
|
const getFormattedIssuerName = () => {
|
||||||
|
if (!auth.issuerUrl) return "your identity provider";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(auth.issuerUrl);
|
||||||
|
// Return domain name without 'www.'
|
||||||
|
return url.hostname.replace(/^www\./i, '');
|
||||||
|
} catch {
|
||||||
|
return "your identity provider";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
titleElement={
|
titleElement={
|
||||||
|
@ -172,6 +187,52 @@ function OIDCAccount() {
|
||||||
description="Your account credentials are managed by your OpenID Connect provider. To change your username, please visit your provider's settings page and log in again."
|
description="Your account credentials are managed by your OpenID Connect provider. To change your username, please visit your provider's settings page and log in again."
|
||||||
noLeftPadding
|
noLeftPadding
|
||||||
>
|
>
|
||||||
|
<div className="px-4 py-5 sm:p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700 transition duration-150">
|
||||||
|
<div className="flex flex-col sm:flex-row items-center">
|
||||||
|
<div className="flex-shrink-0 relative">
|
||||||
|
{auth.profilePicture ? (
|
||||||
|
<img
|
||||||
|
src={auth.profilePicture}
|
||||||
|
alt={`${auth.username}'s profile picture`}
|
||||||
|
className="h-16 w-16 sm:h-20 sm:w-20 rounded-full object-cover border-1 border-gray-200 dark:border-gray-700 transition duration-200"
|
||||||
|
onError={() => auth.profilePicture = undefined}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-16 w-16 sm:h-20 sm:w-20 rounded-full flex items-center justify-center bg-gray-100 dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-700 transition duration-200">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faOpenid}
|
||||||
|
className="h-16 w-16 text-gray-500 dark:text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 sm:mt-0 sm:ml-6 text-center sm:text-left">
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
|
||||||
|
{auth.username}
|
||||||
|
</h3>
|
||||||
|
<div className="mt-1 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<FontAwesomeIcon icon={faOpenid} className="mr-1.5 h-4 w-4 flex-shrink-0" />
|
||||||
|
<p>Authenticated via OpenID Connect</p>
|
||||||
|
</div>
|
||||||
|
{auth.issuerUrl && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<a
|
||||||
|
href={auth.issuerUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 border border-blue-200 dark:border-blue-800 transition-colors duration-150"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
|
</svg>
|
||||||
|
{getFormattedIssuerName()}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,13 +24,17 @@ export interface AuthInfo {
|
||||||
username: string;
|
username: string;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
authMethod?: 'password' | 'oidc';
|
authMethod?: 'password' | 'oidc';
|
||||||
|
profilePicture?: string;
|
||||||
|
issuerUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default values
|
// Default values
|
||||||
const AuthContextDefaults: AuthInfo = {
|
const AuthContextDefaults: AuthInfo = {
|
||||||
username: "",
|
username: "",
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
authMethod: undefined
|
authMethod: undefined,
|
||||||
|
profilePicture: undefined,
|
||||||
|
issuerUrl: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsContextDefaults: SettingsType = {
|
const SettingsContextDefaults: SettingsType = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue