mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-03 03:12:50 +02:00
userinfo: fix logout button, add sign out confirm page (#3058)
* userinfo: fix logout button, add sign out confirm page * fix test
This commit is contained in:
parent
9300208e87
commit
38c7089642
18 changed files with 251 additions and 162 deletions
|
@ -8,7 +8,7 @@ linters-settings:
|
|||
lines: 100
|
||||
statements: 50
|
||||
gci:
|
||||
local-prefixes: github.com/pomerium/pomerium
|
||||
local-prefixes: github.com/pomerium
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
|
@ -28,7 +28,7 @@ linters-settings:
|
|||
gocyclo:
|
||||
min-complexity: 15
|
||||
goimports:
|
||||
local-prefixes: github.com/pomerium/pomerium
|
||||
local-prefixes: github.com/pomerium
|
||||
govet:
|
||||
check-shadowing: false
|
||||
lll:
|
||||
|
|
|
@ -12,11 +12,11 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pomerium/csrf"
|
||||
"github.com/rs/cors"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/pomerium/csrf"
|
||||
"github.com/pomerium/pomerium/authenticate/handlers"
|
||||
"github.com/pomerium/pomerium/authenticate/handlers/webauthn"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
|
@ -55,8 +55,7 @@ func (a *Authenticate) Mount(r *mux.Router) {
|
|||
csrf.Path("/"),
|
||||
csrf.UnsafePaths(
|
||||
[]string{
|
||||
"/oauth2/callback", // rfc6749#section-10.12 accepts GET
|
||||
"/.pomerium/sign_out", // https://openid.net/specs/openid-connect-frontchannel-1_0.html
|
||||
"/oauth2/callback", // rfc6749#section-10.12 accepts GET
|
||||
}),
|
||||
csrf.FormValueName("state"), // rfc6749#section-10.12
|
||||
csrf.CookieName(csrfKey),
|
||||
|
@ -96,14 +95,10 @@ func (a *Authenticate) mountDashboard(r *mux.Router) {
|
|||
sr.Use(a.VerifySession)
|
||||
sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo))
|
||||
sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn))
|
||||
sr.Path("/sign_out").Handler(a.requireValidSignature(a.SignOut))
|
||||
sr.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut))
|
||||
sr.Path("/webauthn").Handler(webauthn.New(a.getWebauthnState))
|
||||
sr.Path("/device-enrolled").Handler(httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
authenticateURL, err := a.options.Load().GetAuthenticateURL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handlers.DeviceEnrolled(authenticateURL, a.state.Load().sharedKey).ServeHTTP(w, r)
|
||||
handlers.DeviceEnrolled().ServeHTTP(w, r)
|
||||
return nil
|
||||
}))
|
||||
|
||||
|
@ -276,6 +271,25 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) error {
|
|||
// SignOut signs the user out and attempts to revoke the user's identity session
|
||||
// Handles both GET and POST.
|
||||
func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error {
|
||||
// check for an HMAC'd URL. If none is found, show a confirmation page.
|
||||
err := middleware.ValidateRequestURL(a.getExternalRequest(r), a.state.Load().sharedKey)
|
||||
if err != nil {
|
||||
authenticateURL, err := a.options.Load().GetAuthenticateURL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handlers.SignOutConfirm(handlers.SignOutConfirmData{
|
||||
URL: urlutil.SignOutURL(r, authenticateURL, a.state.Load().sharedKey),
|
||||
}).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
// otherwise actually do the sign out
|
||||
return a.signOutRedirect(w, r)
|
||||
}
|
||||
|
||||
func (a *Authenticate) signOutRedirect(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx, span := trace.StartSpan(r.Context(), "authenticate.SignOut")
|
||||
defer span.End()
|
||||
|
||||
|
@ -553,7 +567,6 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
|||
DirectoryUser: pbDirectoryUser,
|
||||
IsImpersonated: isImpersonated,
|
||||
Session: pbSession,
|
||||
SignOutURL: urlutil.SignOutURL(r, authenticateURL, state.sharedKey),
|
||||
User: pbUser,
|
||||
WebAuthnURL: urlutil.WebAuthnURL(r, authenticateURL, state.sharedKey, r.URL.Query()),
|
||||
}).ServeHTTP(w, r)
|
||||
|
|
|
@ -2,18 +2,14 @@ package handlers
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
"github.com/pomerium/pomerium/ui"
|
||||
)
|
||||
|
||||
// DeviceEnrolled displays an HTML page informing the user that they've successfully enrolled a device.
|
||||
func DeviceEnrolled(authenticateURL *url.URL, sharedKey []byte) http.Handler {
|
||||
func DeviceEnrolled() http.Handler {
|
||||
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return ui.ServePage(w, r, "DeviceEnrolled", map[string]interface{}{
|
||||
"signOutUrl": urlutil.SignOutURL(r, authenticateURL, sharedKey),
|
||||
})
|
||||
return ui.ServePage(w, r, "DeviceEnrolled", map[string]interface{}{})
|
||||
})
|
||||
}
|
||||
|
|
27
authenticate/handlers/signout.go
Normal file
27
authenticate/handlers/signout.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/ui"
|
||||
)
|
||||
|
||||
// SignOutConfirmData is the data for the SignOutConfirm page.
|
||||
type SignOutConfirmData struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
// ToJSON converts the data into a JSON map.
|
||||
func (data SignOutConfirmData) ToJSON() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"url": data.URL,
|
||||
}
|
||||
}
|
||||
|
||||
// SignOutConfirm returns a handler that renders the sign out confirm page.
|
||||
func SignOutConfirm(data SignOutConfirmData) http.Handler {
|
||||
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return ui.ServePage(w, r, "SignOutConfirm", data.ToJSON())
|
||||
})
|
||||
}
|
|
@ -20,7 +20,6 @@ type UserInfoData struct {
|
|||
DirectoryUser *directory.User
|
||||
IsImpersonated bool
|
||||
Session *session.Session
|
||||
SignOutURL string
|
||||
User *user.User
|
||||
WebAuthnURL string
|
||||
}
|
||||
|
@ -43,7 +42,6 @@ func (data UserInfoData) ToJSON() map[string]interface{} {
|
|||
if bs, err := protojson.Marshal(data.Session); err == nil {
|
||||
m["session"] = json.RawMessage(bs)
|
||||
}
|
||||
m["signOutUrl"] = data.SignOutURL
|
||||
if bs, err := protojson.Marshal(data.User); err == nil {
|
||||
m["user"] = json.RawMessage(bs)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"net/url"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pomerium/webauthn"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
@ -30,6 +29,7 @@ import (
|
|||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||
"github.com/pomerium/pomerium/pkg/webauthnutil"
|
||||
"github.com/pomerium/pomerium/ui"
|
||||
"github.com/pomerium/webauthn"
|
||||
)
|
||||
|
||||
const maxAuthenticateResponses = 5
|
||||
|
@ -373,7 +373,6 @@ func (h *Handler) handleView(w http.ResponseWriter, r *http.Request, state *Stat
|
|||
"creationOptions": creationOptions,
|
||||
"requestOptions": requestOptions,
|
||||
"selfUrl": r.URL.String(),
|
||||
"signOutUrl": urlutil.SignOutURL(r, state.AuthenticateURL, state.SharedKey),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -227,12 +227,84 @@ func TestAuthenticate_SignOut(t *testing.T) {
|
|||
wantCode int
|
||||
wantBody string
|
||||
}{
|
||||
{"good post", http.MethodPost, nil, "https://corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""},
|
||||
{"signout redirect url", http.MethodPost, nil, "", "https://signout-redirect-url.example.com", "sig", "ts", identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""},
|
||||
{"failed revoke", http.MethodPost, nil, "https://corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""},
|
||||
{"load session error", http.MethodPost, errors.New("error"), "https://corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""},
|
||||
{"bad redirect uri", http.MethodPost, nil, "corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{LogOutError: oidc.ErrSignoutNotImplemented}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""},
|
||||
{"no redirect uri", http.MethodPost, nil, "", "", "sig", "ts", identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusOK, "{\"Status\":200,\"Error\":\"OK: user logged out\"}\n"},
|
||||
{
|
||||
"good post",
|
||||
http.MethodPost,
|
||||
nil,
|
||||
"https://corp.pomerium.io/",
|
||||
"",
|
||||
"sig",
|
||||
"ts",
|
||||
identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))},
|
||||
&mstore.Store{Encrypted: true, Session: &sessions.State{}},
|
||||
http.StatusFound,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"signout redirect url",
|
||||
http.MethodPost,
|
||||
nil,
|
||||
"",
|
||||
"https://signout-redirect-url.example.com",
|
||||
"sig",
|
||||
"ts",
|
||||
identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))},
|
||||
&mstore.Store{Encrypted: true, Session: &sessions.State{}},
|
||||
http.StatusFound,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"failed revoke",
|
||||
http.MethodPost,
|
||||
nil,
|
||||
"https://corp.pomerium.io/",
|
||||
"",
|
||||
"sig",
|
||||
"ts",
|
||||
identity.MockProvider{RevokeError: errors.New("OH NO")},
|
||||
&mstore.Store{Encrypted: true, Session: &sessions.State{}},
|
||||
http.StatusFound,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"load session error",
|
||||
http.MethodPost,
|
||||
errors.New("error"),
|
||||
"https://corp.pomerium.io/",
|
||||
"",
|
||||
"sig",
|
||||
"ts",
|
||||
identity.MockProvider{RevokeError: errors.New("OH NO")},
|
||||
&mstore.Store{Encrypted: true, Session: &sessions.State{}},
|
||||
http.StatusFound,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"bad redirect uri",
|
||||
http.MethodPost,
|
||||
nil,
|
||||
"corp.pomerium.io/",
|
||||
"",
|
||||
"sig",
|
||||
"ts",
|
||||
identity.MockProvider{LogOutError: oidc.ErrSignoutNotImplemented},
|
||||
&mstore.Store{Encrypted: true, Session: &sessions.State{}},
|
||||
http.StatusFound,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"no redirect uri",
|
||||
http.MethodPost,
|
||||
nil,
|
||||
"",
|
||||
"",
|
||||
"sig",
|
||||
"ts",
|
||||
identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))},
|
||||
&mstore.Store{Encrypted: true, Session: &sessions.State{}},
|
||||
http.StatusOK,
|
||||
"{\"Status\":200,\"Error\":\"OK: user logged out\"}\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
@ -295,7 +367,7 @@ func TestAuthenticate_SignOut(t *testing.T) {
|
|||
r.Header.Set("Accept", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
httputil.HandlerFunc(a.SignOut).ServeHTTP(w, r)
|
||||
httputil.HandlerFunc(a.signOutRedirect).ServeHTTP(w, r)
|
||||
if status := w.Code; status != tt.wantCode {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode)
|
||||
}
|
||||
|
@ -727,89 +799,6 @@ func (m mockDirectoryServiceClient) RefreshUser(ctx context.Context, in *directo
|
|||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
func TestAuthenticate_SignOut_CSRF(t *testing.T) {
|
||||
now := time.Now()
|
||||
signer, err := jws.NewHS256Signer(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pbNow, _ := ptypes.TimestampProto(now)
|
||||
a := &Authenticate{
|
||||
options: config.NewAtomicOptions(),
|
||||
state: newAtomicAuthenticateState(&authenticateState{
|
||||
// sessionStore: tt.sessionStore,
|
||||
cookieSecret: cryptutil.NewKey(),
|
||||
encryptedEncoder: signer,
|
||||
sharedEncoder: signer,
|
||||
dataBrokerClient: mockDataBrokerServiceClient{
|
||||
get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
|
||||
data, err := ptypes.MarshalAny(&session.Session{
|
||||
Id: "SESSION_ID",
|
||||
UserId: "USER_ID",
|
||||
IdToken: &session.IDToken{IssuedAt: pbNow},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &databroker.GetResponse{
|
||||
Record: &databroker.Record{
|
||||
Version: 1,
|
||||
Type: data.GetTypeUrl(),
|
||||
Id: "SESSION_ID",
|
||||
Data: data,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
directoryClient: new(mockDirectoryServiceClient),
|
||||
}),
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
setCSRFCookie bool
|
||||
method string
|
||||
wantStatus int
|
||||
wantBody string
|
||||
}{
|
||||
{"GET without CSRF should fail", false, "GET", 400, "{\"Status\":400,\"Error\":\"Bad Request: CSRF token invalid\"}\n"},
|
||||
{"POST without CSRF should fail", false, "POST", 400, "{\"Status\":400,\"Error\":\"Bad Request: CSRF token invalid\"}\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := a.Handler()
|
||||
|
||||
// Obtain a CSRF cookie via a GET request.
|
||||
orr, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
s.ServeHTTP(rr, orr)
|
||||
|
||||
r, err := http.NewRequest(tt.method, "/.pomerium/sign_out", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.setCSRFCookie {
|
||||
r.Header.Set("Cookie", rr.Header().Get("Set-Cookie"))
|
||||
}
|
||||
r.Header.Set("Accept", "application/json")
|
||||
r.Header.Set("Referer", "/")
|
||||
rr = httptest.NewRecorder()
|
||||
s.ServeHTTP(rr, r)
|
||||
|
||||
if rr.Code != tt.wantStatus {
|
||||
t.Errorf("status: got %v want %v", rr.Code, tt.wantStatus)
|
||||
}
|
||||
body := rr.Body.String()
|
||||
if diff := cmp.Diff(body, tt.wantBody); diff != "" {
|
||||
t.Errorf("handler returned wrong body Body: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseURL(rawurl string) *url.URL {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"sync/atomic"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/pomerium/webauthn"
|
||||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/encoding"
|
||||
|
@ -24,6 +23,7 @@ import (
|
|||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||
"github.com/pomerium/pomerium/pkg/webauthnutil"
|
||||
"github.com/pomerium/webauthn"
|
||||
)
|
||||
|
||||
var outboundGRPCConnection = new(grpc.CachedOutboundGRPClientConn)
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pomerium/csrf"
|
||||
|
||||
"github.com/pomerium/csrf"
|
||||
"github.com/pomerium/pomerium/ui"
|
||||
)
|
||||
|
||||
|
|
|
@ -3,13 +3,13 @@ package webauthnutil
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pomerium/webauthn"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/pomerium/pomerium/pkg/encoding/base58"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/device"
|
||||
"github.com/pomerium/webauthn"
|
||||
)
|
||||
|
||||
// CredentialStorage stores credentials in the databroker.
|
||||
|
|
|
@ -4,13 +4,13 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/pomerium/webauthn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||
"github.com/pomerium/webauthn"
|
||||
)
|
||||
|
||||
type mockDataBrokerServiceClient struct {
|
||||
|
|
|
@ -3,7 +3,6 @@ package webauthnutil
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pomerium/webauthn/cose"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
@ -11,6 +10,7 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/device"
|
||||
"github.com/pomerium/webauthn/cose"
|
||||
)
|
||||
|
||||
// DefaultDeviceType is the default device type when none is specified.
|
||||
|
|
|
@ -3,12 +3,12 @@ package webauthnutil
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pomerium/webauthn"
|
||||
"github.com/pomerium/webauthn/cose"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/pomerium/pomerium/pkg/grpc/device"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||
"github.com/pomerium/webauthn"
|
||||
"github.com/pomerium/webauthn/cose"
|
||||
)
|
||||
|
||||
func TestGenerateCreationOptions(t *testing.T) {
|
||||
|
|
|
@ -2,9 +2,9 @@ package webauthnutil
|
|||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pomerium/webauthn"
|
||||
|
||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||
"github.com/pomerium/webauthn"
|
||||
)
|
||||
|
||||
var pomeriumUserNamespace = uuid.MustParse("2929d3f7-f0b0-478f-9dd5-970d51eb3859")
|
||||
|
|
|
@ -2,16 +2,17 @@ import DeviceEnrolledPage from "./components/DeviceEnrolledPage";
|
|||
import ErrorPage from "./components/ErrorPage";
|
||||
import Footer from "./components/Footer";
|
||||
import Header from "./components/Header";
|
||||
import SignOutConfirmPage from "./components/SignOutConfirmPage";
|
||||
import { ToolbarOffset } from "./components/ToolbarOffset";
|
||||
import UserInfoPage from "./components/UserInfoPage";
|
||||
import WebAuthnRegistrationPage from "./components/WebAuthnRegistrationPage";
|
||||
import { SubpageContextProvider } from "./context/Subpage";
|
||||
import { createTheme } from "./theme";
|
||||
import { PageData } from "./types";
|
||||
import Box from "@mui/material/Box";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import React, { FC } from "react";
|
||||
import {ToolbarOffset} from "./components/ToolbarOffset";
|
||||
import Box from "@mui/material/Box";
|
||||
import {SubpageContextProvider} from "./context/Subpage";
|
||||
|
||||
const theme = createTheme();
|
||||
|
||||
|
@ -25,6 +26,9 @@ const App: FC = () => {
|
|||
case "Error":
|
||||
body = <ErrorPage data={data} />;
|
||||
break;
|
||||
case "SignOutConfirm":
|
||||
body = <SignOutConfirmPage data={data} />;
|
||||
break;
|
||||
case "UserInfo":
|
||||
body = <UserInfoPage data={data} />;
|
||||
break;
|
||||
|
@ -36,15 +40,16 @@ const App: FC = () => {
|
|||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<SubpageContextProvider>
|
||||
<Header
|
||||
includeSidebar={data?.page === "UserInfo"}
|
||||
data={data}
|
||||
/>
|
||||
<Header includeSidebar={data?.page === "UserInfo"} data={data} />
|
||||
<ToolbarOffset />
|
||||
<Box
|
||||
sx={{overflow: 'hidden', height: 'calc(100vh - 120px)'}}
|
||||
>
|
||||
<Box sx={{overflow: 'auto', height: '100%', paddingTop: theme.spacing(5)}}>
|
||||
<Box sx={{ overflow: "hidden", height: "calc(100vh - 120px)" }}>
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "auto",
|
||||
height: "100%",
|
||||
paddingTop: theme.spacing(5),
|
||||
}}
|
||||
>
|
||||
{body}
|
||||
<ToolbarOffset />
|
||||
</Box>
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
import { PageData } from "../types";
|
||||
import { Avatar } from "./Avatar";
|
||||
import Logo from "./Logo";
|
||||
import { ToolbarOffset } from "./ToolbarOffset";
|
||||
import UserSidebarContent from "./UserSidebarContent";
|
||||
import {
|
||||
Drawer,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import React, {FC, useState} from "react";
|
||||
import {useTheme} from "@mui/material/styles";
|
||||
import {Drawer, IconButton, Menu, MenuItem, useMediaQuery} from "@mui/material";
|
||||
import {ToolbarOffset} from "./ToolbarOffset";
|
||||
import UserSidebarContent from "./UserSidebarContent";
|
||||
import {ChevronLeft, ChevronRight, Menu as MenuIcon} from "react-feather";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import styled from "@mui/material/styles/styled";
|
||||
import {Avatar} from "./Avatar";
|
||||
import {PageData} from "../types";
|
||||
import {get} from 'lodash';
|
||||
import { get } from "lodash";
|
||||
import React, { FC, useState } from "react";
|
||||
import { ChevronLeft, ChevronRight, Menu as MenuIcon } from "react-feather";
|
||||
|
||||
const DrawerHeader = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
const DrawerHeader = styled("div")(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: theme.spacing(0, 1),
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: "flex-end",
|
||||
}));
|
||||
|
||||
type HeaderProps = {
|
||||
|
@ -26,33 +32,34 @@ type HeaderProps = {
|
|||
};
|
||||
const Header: FC<HeaderProps> = ({ includeSidebar, data }) => {
|
||||
const theme = useTheme();
|
||||
const mdUp = useMediaQuery(() => theme.breakpoints.up('md'), {
|
||||
const mdUp = useMediaQuery(() => theme.breakpoints.up("md"), {
|
||||
defaultMatches: true,
|
||||
noSsr: false
|
||||
noSsr: false,
|
||||
});
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const handleMenuOpen = e => {
|
||||
const handleMenuOpen = (e) => {
|
||||
setAnchorEl(e.currentTarget);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const userName = get(data, 'user.name') || get(data, 'user.claims.given_name');
|
||||
const userName =
|
||||
get(data, "user.name") || get(data, "user.claims.given_name");
|
||||
|
||||
const handleDrawerOpen = () => {
|
||||
setDrawerOpen(true);
|
||||
};
|
||||
|
||||
const handleDrawerClose = ():void => {
|
||||
const handleDrawerClose = (): void => {
|
||||
setDrawerOpen(false);
|
||||
};
|
||||
|
||||
const handleLogout = (evt: React.MouseEvent):void => {
|
||||
const handleLogout = (evt: React.MouseEvent): void => {
|
||||
evt.preventDefault();
|
||||
location.href = "/.pomerium/sign_out";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
|
@ -67,7 +74,7 @@ const Header: FC<HeaderProps> = ({ includeSidebar, data }) => {
|
|||
aria-label="open drawer"
|
||||
onClick={handleDrawerOpen}
|
||||
edge="start"
|
||||
sx={{ mr: 2, ...(drawerOpen && { display: 'none' }) }}
|
||||
sx={{ mr: 2, ...(drawerOpen && { display: "none" }) }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
@ -75,11 +82,11 @@ const Header: FC<HeaderProps> = ({ includeSidebar, data }) => {
|
|||
sx={{
|
||||
width: 256,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': {
|
||||
"& .MuiDrawer-paper": {
|
||||
width: 256,
|
||||
boxSizing: 'border-box',
|
||||
backgroundColor: 'neutral.900',
|
||||
height: '100vh',
|
||||
boxSizing: "border-box",
|
||||
backgroundColor: "neutral.900",
|
||||
height: "100vh",
|
||||
},
|
||||
}}
|
||||
variant="persistent"
|
||||
|
@ -88,10 +95,14 @@ const Header: FC<HeaderProps> = ({ includeSidebar, data }) => {
|
|||
>
|
||||
<DrawerHeader>
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
{theme.direction === 'ltr' ? <ChevronLeft /> : <ChevronRight />}
|
||||
{theme.direction === "ltr" ? (
|
||||
<ChevronLeft />
|
||||
) : (
|
||||
<ChevronRight />
|
||||
)}
|
||||
</IconButton>
|
||||
</DrawerHeader>
|
||||
<UserSidebarContent close={handleDrawerClose}/>
|
||||
<UserSidebarContent close={handleDrawerClose} />
|
||||
<ToolbarOffset />
|
||||
</Drawer>
|
||||
</>
|
||||
|
@ -104,13 +115,16 @@ const Header: FC<HeaderProps> = ({ includeSidebar, data }) => {
|
|||
{userName && (
|
||||
<>
|
||||
<IconButton color="inherit" onClick={handleMenuOpen}>
|
||||
<Avatar name={userName} url={get(data, 'user.claims.picture', null)} />
|
||||
<Avatar
|
||||
name={userName}
|
||||
url={get(data, "user.claims.picture", null)}
|
||||
/>
|
||||
</IconButton>
|
||||
<Menu
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center'
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
keepMounted
|
||||
open={!!anchorEl}
|
||||
|
|
43
ui/src/components/SignOutConfirmPage.tsx
Normal file
43
ui/src/components/SignOutConfirmPage.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Button from "@mui/material/Button";
|
||||
import Container from "@mui/material/Container";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import styled from "@mui/material/styles/styled";
|
||||
import React, { FC } from "react";
|
||||
import { SignOutConfirmPageData } from "src/types";
|
||||
|
||||
type SignOutConfirmPageProps = {
|
||||
data: SignOutConfirmPageData;
|
||||
};
|
||||
const SignOutConfirmPage: FC<SignOutConfirmPageProps> = ({ data }) => {
|
||||
function handleClickCancel(evt: React.MouseEvent) {
|
||||
evt.preventDefault();
|
||||
history.back();
|
||||
}
|
||||
|
||||
function handleClickLogout(evt: React.MouseEvent) {
|
||||
evt.preventDefault();
|
||||
location.href = data.url;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Dialog open={true} disableEscapeKeyDown={true}>
|
||||
<DialogTitle>Logout?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Are you sure you want to logout?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClickCancel}>Cancel</Button>
|
||||
<Button onClick={handleClickLogout}>Logout</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default SignOutConfirmPage;
|
|
@ -81,7 +81,6 @@ export type WebAuthnRequestOptions = {
|
|||
|
||||
type BasePageData = {
|
||||
csrfToken?: string;
|
||||
signOutUrl?: string;
|
||||
};
|
||||
|
||||
export type ErrorPageData = BasePageData & {
|
||||
|
@ -100,6 +99,11 @@ export type DeviceEnrolledPageData = BasePageData & {
|
|||
page: "DeviceEnrolled";
|
||||
};
|
||||
|
||||
export type SignOutConfirmPageData = BasePageData & {
|
||||
page: "SignOutConfirm";
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type UserInfoPageData = BasePageData & {
|
||||
page: "UserInfo";
|
||||
|
||||
|
@ -123,5 +127,6 @@ export type WebAuthnRegistrationPageData = BasePageData & {
|
|||
export type PageData =
|
||||
| ErrorPageData
|
||||
| DeviceEnrolledPageData
|
||||
| SignOutConfirmPageData
|
||||
| UserInfoPageData
|
||||
| WebAuthnRegistrationPageData;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue