From 76c395d61372ce6875a16e87fbee98082e0a3311 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 2 Apr 2026 17:39:16 +0000 Subject: [PATCH] Add display in sidebar for impersonation --- api/routes.go | 2 + api/signup.go | 2 +- auth/auth.go | 35 ++++-- resource/session.go | 14 ++- resource/user.go | 6 +- scss/sidebar.scss | 140 --------------------- ts/App.vue | 5 +- ts/components/UserSelector.vue | 32 ++++- ts/components/layout/Sidebar.vue | 152 ++++++++++++++++++++++- ts/components/sudo/UserImpersonation.vue | 104 +++++----------- ts/types.ts | 1 + 11 files changed, 259 insertions(+), 234 deletions(-) diff --git a/api/routes.go b/api/routes.go index 8213b220..a946d0e2 100644 --- a/api/routes.go +++ b/api/routes.go @@ -27,6 +27,8 @@ func AddRoutes(r *mux.Router) { r.Handle("/image/{uuid}", auth.NewEnsureAuth(apiImagePost)).Methods("POST") r.Handle("/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentGet)).Methods("GET") r.Handle("/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentPost)).Methods("POST") + impersonation := resource.Impersonation(router) + r.Handle("/impersonation", authenticatedHandlerJSONPost(impersonation.Create)).Methods("POST") lead := resource.Lead(r) r.Handle("/leads", authenticatedHandlerJSON(lead.List)).Methods("GET") r.Handle("/leads", authenticatedHandlerJSONPost(lead.Create)).Methods("POST") diff --git a/api/signup.go b/api/signup.go index e8e7bad6..2208dea4 100644 --- a/api/signup.go +++ b/api/signup.go @@ -31,7 +31,7 @@ func postSignup(ctx context.Context, r *http.Request, signup reqSignup) (string, return "", nhttp.NewError("Failed to signup user", err) } - auth.AddUserSession(r, user) + auth.AddUserSession(ctx, user) return "/", nil } diff --git a/auth/auth.go b/auth/auth.go index 9d0a37c3..fa34d815 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -30,16 +30,37 @@ type EnsureAuth struct { handler AuthenticatedHandler } -func AddUserSession(r *http.Request, user *platform.User) { - id := strconv.Itoa(int(user.ID)) - sessionManager.Put(r.Context(), "user_id", id) - sessionManager.Put(r.Context(), "username", user.Username) - log.Debug().Str("id", id).Str("username", user.Username).Msg("added user session") +func AddUserSession(ctx context.Context, user *platform.User) { + id_str := strconv.Itoa(int(user.ID)) + sessionManager.Put(ctx, "user_id", id_str) + sessionManager.Put(ctx, "username", user.Username) + log.Debug().Str("id", id_str).Str("username", user.Username).Msg("added user session") +} +func ImpersonateUser(ctx context.Context, target_user_id int) { + target_user_id_str := strconv.Itoa(int(target_user_id)) + sessionManager.Put(ctx, "impersonated_user_id", target_user_id_str) +} +func ImpersonatedUser(ctx context.Context) *int32 { + i_str := sessionManager.GetString(ctx, "impersonated_user_id") + if i_str == "" { + return nil + } + i, err := strconv.Atoi(i_str) + if err != nil { + log.Error().Err(err).Str("impersonated_user_id", i_str).Msg("failed to parse impersonated_user_id") + return nil + } + result := int32(i) + return &result } func GetAuthenticatedUser(r *http.Request) (*platform.User, error) { ctx := r.Context() user_id_str := sessionManager.GetString(ctx, "user_id") + impersonated_user_id_str := sessionManager.GetString(ctx, "impersonated_user_id") + if impersonated_user_id_str != "" { + user_id_str = impersonated_user_id_str + } if user_id_str != "" { user_id, err := strconv.Atoi(user_id_str) if err != nil { @@ -59,7 +80,7 @@ func GetAuthenticatedUser(r *http.Request) (*platform.User, error) { if err != nil { return nil, err } - AddUserSession(r, user) + AddUserSession(ctx, user) return user, nil } @@ -108,7 +129,7 @@ func SigninUser(r *http.Request, username string, password string) (*platform.Us if user == nil { return nil, errors.New("No matching user") } - AddUserSession(r, user) + AddUserSession(r.Context(), user) return user, nil } diff --git a/resource/session.go b/resource/session.go index 1fb21297..ee978a92 100644 --- a/resource/session.go +++ b/resource/session.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "github.com/Gleipnir-Technology/nidus-sync/auth" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/html" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" @@ -47,6 +48,7 @@ type sessionURL struct { type sessionURLAPI struct { Avatar string `json:"avatar"` Communication string `json:"communication"` + Impersonation string `json:"impersonation"` PublicreportMessage string `json:"publicreport_message"` ReviewTask string `json:"review_task"` Signal string `json:"signal"` @@ -65,8 +67,17 @@ func (res *sessionR) Get(ctx context.Context, r *http.Request, user platform.Use if err != nil { return nil, nhttp.NewError("create user: %w", err) } + var impersonating *string + impersonating_id := auth.ImpersonatedUser(ctx) + if impersonating_id != nil { + i, err := res.router.IDToURI("user.ByIDGet", int(*impersonating_id)) + if err != nil { + return nil, nhttp.NewError("create impersonating uri: %w", err) + } + impersonating = &i + } return &session{ - Impersonating: nil, + Impersonating: impersonating, NotificationCounts: sessionNotificationCounts{ Communications: counts.Communications, Home: counts.Home, @@ -81,6 +92,7 @@ func (res *sessionR) Get(ctx context.Context, r *http.Request, user platform.Use API: sessionURLAPI{ Avatar: config.MakeURLNidus("/api/avatar"), Communication: urls.API.Communication, + Impersonation: config.MakeURLNidus("/api/impersonation"), PublicreportMessage: urls.API.Publicreport.Message, ReviewTask: config.MakeURLNidus("/api/review-task"), Signal: config.MakeURLNidus("/api/signal"), diff --git a/resource/user.go b/resource/user.go index 714e23f3..3e64a9d7 100644 --- a/resource/user.go +++ b/resource/user.go @@ -14,7 +14,7 @@ import ( "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/gorilla/mux" - "github.com/rs/zerolog/log" + //"github.com/rs/zerolog/log" ) type user struct { @@ -159,9 +159,9 @@ func (res *userR) List(ctx context.Context, r *http.Request, u platform.User, qu return nil, nhttp.NewError("list users: %w", err) } results := make([]*user, len(users)) - log.Debug().Int("len", len(users)).Msg("building response") + //log.Debug().Int("len", len(users)).Msg("building response") for i, v := range users { - log.Debug().Int("i", i).Msg("making results") + //log.Debug().Int("i", i).Msg("making results") resp, err := res.response(v) if err != nil { return nil, nhttp.NewError("create response: %w", err) diff --git a/scss/sidebar.scss b/scss/sidebar.scss index 24c9fc48..e69de29b 100644 --- a/scss/sidebar.scss +++ b/scss/sidebar.scss @@ -1,140 +0,0 @@ -.logo-container { - display: flex; - justify-content: center; - align-items: center; - transition: all 0.3s ease; -} - -.logo { - max-width: 100%; - height: auto; - transition: all 0.3s ease; -} - -#sidebar { - background-color: $off-white; - min-height: 100vh; - transition: all 0.3s; - width: 250px; - position: fixed; - z-index: 1000; - padding: 20px; -} - -#sidebar.collapsed { - width: 70px; - padding: 20px 10px; -} -/* Logo style when sidebar is collapsed */ -#sidebar.collapsed .logo-container { - width: 100%; -} - -#sidebar.collapsed .logo-img { - max-width: 40px; /* smaller size for collapsed state */ -} -#content { - transition: all 0.3s; - margin-left: 250px; - padding: 10px; - width: calc(100% - 250px); -} - -#content.expanded { - margin-left: 70px; - width: calc(100% - 70px); -} - -.sidebar-header { - padding-bottom: 20px; - border-bottom: 1px solid $off-black; - margin-bottom: 20px; - overflow: hidden; - white-space: nowrap; - display: flex; - justify-content: center; /* Center for the logo */ -} - -.sidebar-menu { - list-style: none; - padding: 0; -} - -.sidebar-menu li { - padding: 10px 0; -} - -.sidebar-menu li a { - text-decoration: none; - color: $off-black; - display: flex; - align-items: center; - overflow: hidden; - white-space: nowrap; -} - -.sidebar-menu li a:hover { - color: $primary; -} - -.sidebar-menu .menu-icon { - font-size: 1.2rem; - min-width: 30px; - display: flex; - justify-content: center; -} -.sidebar-menu .menu-icon svg { - width: 1.5em; - height: 1.5em; -} -.sidebar-menu .menu-text { - transition: opacity 0.3s; -} - -#sidebar.collapsed .menu-text { - opacity: 0; - visibility: hidden; - width: 0; -} - -#sidebar.collapsed .sidebar-header h4 { - opacity: 0; - visibility: hidden; -} - -#sidebar.collapsed .sidebar-menu .menu-icon { - min-width: 100%; - font-size: 1.5rem; -} - -#sidebarToggle { - position: absolute; - left: calc(250px - 15px); - top: 50%; - transform: translateY(-50%); - z-index: 1050; - width: 30px; - height: 30px; - border-radius: 50%; - border: 1px solid #dee2e6; - display: flex; - align-items: center; - transition: left 0.3s; - padding: 0; -} -#sidebarToggle i { - transition: transform 0.3s; -} - -#sidebar.collapsed > #sidebarToggle { - left: calc(70px - 15px); -} - -#sidebar > #sidebarToggle i { - position: relative; - left: 5px; -} - -#sidebar.collapsed > #sidebarToggle i { - transform: rotate(180deg); -} diff --git a/ts/App.vue b/ts/App.vue index cdfb8186..fa877d66 100644 --- a/ts/App.vue +++ b/ts/App.vue @@ -12,6 +12,7 @@ diff --git a/ts/components/UserSelector.vue b/ts/components/UserSelector.vue index 07aa826f..adc8330b 100644 --- a/ts/components/UserSelector.vue +++ b/ts/components/UserSelector.vue @@ -49,24 +49,25 @@ diff --git a/ts/types.ts b/ts/types.ts index a6ef61bb..17bc4406 100644 --- a/ts/types.ts +++ b/ts/types.ts @@ -229,6 +229,7 @@ export interface URLs { interface URLsAPI { avatar: string; communication: string; + impersonation: string; publicreport_message: string; review_task: string; signal: string;