Add display in sidebar for impersonation
This commit is contained in:
parent
51811132a4
commit
76c395d613
11 changed files with 259 additions and 234 deletions
|
|
@ -27,6 +27,8 @@ func AddRoutes(r *mux.Router) {
|
||||||
r.Handle("/image/{uuid}", auth.NewEnsureAuth(apiImagePost)).Methods("POST")
|
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(apiImageContentGet)).Methods("GET")
|
||||||
r.Handle("/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentPost)).Methods("POST")
|
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)
|
lead := resource.Lead(r)
|
||||||
r.Handle("/leads", authenticatedHandlerJSON(lead.List)).Methods("GET")
|
r.Handle("/leads", authenticatedHandlerJSON(lead.List)).Methods("GET")
|
||||||
r.Handle("/leads", authenticatedHandlerJSONPost(lead.Create)).Methods("POST")
|
r.Handle("/leads", authenticatedHandlerJSONPost(lead.Create)).Methods("POST")
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ func postSignup(ctx context.Context, r *http.Request, signup reqSignup) (string,
|
||||||
return "", nhttp.NewError("Failed to signup user", err)
|
return "", nhttp.NewError("Failed to signup user", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.AddUserSession(r, user)
|
auth.AddUserSession(ctx, user)
|
||||||
|
|
||||||
return "/", nil
|
return "/", nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
auth/auth.go
35
auth/auth.go
|
|
@ -30,16 +30,37 @@ type EnsureAuth struct {
|
||||||
handler AuthenticatedHandler
|
handler AuthenticatedHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddUserSession(r *http.Request, user *platform.User) {
|
func AddUserSession(ctx context.Context, user *platform.User) {
|
||||||
id := strconv.Itoa(int(user.ID))
|
id_str := strconv.Itoa(int(user.ID))
|
||||||
sessionManager.Put(r.Context(), "user_id", id)
|
sessionManager.Put(ctx, "user_id", id_str)
|
||||||
sessionManager.Put(r.Context(), "username", user.Username)
|
sessionManager.Put(ctx, "username", user.Username)
|
||||||
log.Debug().Str("id", id).Str("username", user.Username).Msg("added user session")
|
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) {
|
func GetAuthenticatedUser(r *http.Request) (*platform.User, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
user_id_str := sessionManager.GetString(ctx, "user_id")
|
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 != "" {
|
if user_id_str != "" {
|
||||||
user_id, err := strconv.Atoi(user_id_str)
|
user_id, err := strconv.Atoi(user_id_str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -59,7 +80,7 @@ func GetAuthenticatedUser(r *http.Request) (*platform.User, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
AddUserSession(r, user)
|
AddUserSession(ctx, user)
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +129,7 @@ func SigninUser(r *http.Request, username string, password string) (*platform.Us
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, errors.New("No matching user")
|
return nil, errors.New("No matching user")
|
||||||
}
|
}
|
||||||
AddUserSession(r, user)
|
AddUserSession(r.Context(), user)
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/auth"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/html"
|
"github.com/Gleipnir-Technology/nidus-sync/html"
|
||||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||||
|
|
@ -47,6 +48,7 @@ type sessionURL struct {
|
||||||
type sessionURLAPI struct {
|
type sessionURLAPI struct {
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Communication string `json:"communication"`
|
Communication string `json:"communication"`
|
||||||
|
Impersonation string `json:"impersonation"`
|
||||||
PublicreportMessage string `json:"publicreport_message"`
|
PublicreportMessage string `json:"publicreport_message"`
|
||||||
ReviewTask string `json:"review_task"`
|
ReviewTask string `json:"review_task"`
|
||||||
Signal string `json:"signal"`
|
Signal string `json:"signal"`
|
||||||
|
|
@ -65,8 +67,17 @@ func (res *sessionR) Get(ctx context.Context, r *http.Request, user platform.Use
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nhttp.NewError("create user: %w", err)
|
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{
|
return &session{
|
||||||
Impersonating: nil,
|
Impersonating: impersonating,
|
||||||
NotificationCounts: sessionNotificationCounts{
|
NotificationCounts: sessionNotificationCounts{
|
||||||
Communications: counts.Communications,
|
Communications: counts.Communications,
|
||||||
Home: counts.Home,
|
Home: counts.Home,
|
||||||
|
|
@ -81,6 +92,7 @@ func (res *sessionR) Get(ctx context.Context, r *http.Request, user platform.Use
|
||||||
API: sessionURLAPI{
|
API: sessionURLAPI{
|
||||||
Avatar: config.MakeURLNidus("/api/avatar"),
|
Avatar: config.MakeURLNidus("/api/avatar"),
|
||||||
Communication: urls.API.Communication,
|
Communication: urls.API.Communication,
|
||||||
|
Impersonation: config.MakeURLNidus("/api/impersonation"),
|
||||||
PublicreportMessage: urls.API.Publicreport.Message,
|
PublicreportMessage: urls.API.Publicreport.Message,
|
||||||
ReviewTask: config.MakeURLNidus("/api/review-task"),
|
ReviewTask: config.MakeURLNidus("/api/review-task"),
|
||||||
Signal: config.MakeURLNidus("/api/signal"),
|
Signal: config.MakeURLNidus("/api/signal"),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/aarondl/opt/omitnull"
|
"github.com/aarondl/opt/omitnull"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/rs/zerolog/log"
|
//"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type user struct {
|
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)
|
return nil, nhttp.NewError("list users: %w", err)
|
||||||
}
|
}
|
||||||
results := make([]*user, len(users))
|
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 {
|
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)
|
resp, err := res.response(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nhttp.NewError("create response: %w", err)
|
return nil, nhttp.NewError("create response: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
import { useSessionStore } from "@/store/session";
|
import { useSessionStore } from "@/store/session";
|
||||||
|
import { Session } from "@/types";
|
||||||
|
|
||||||
import Sidebar from "./components/layout/Sidebar.vue";
|
import Sidebar from "./components/layout/Sidebar.vue";
|
||||||
import MainContent from "./components/layout/MainContent.vue";
|
import MainContent from "./components/layout/MainContent.vue";
|
||||||
|
|
@ -19,7 +20,9 @@ import NavigationLink from "@/components/common/NavigationLink.vue";
|
||||||
|
|
||||||
const session = useSessionStore();
|
const session = useSessionStore();
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
session.fetchSession();
|
session.get().then((session: Session) => {
|
||||||
|
console.log("session loaded", session);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,24 +49,25 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed, onMounted, watch } from "vue";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
import { useUserStore } from "@/store/user";
|
import { useUserStore } from "@/store/user";
|
||||||
import type { User } from "@/types";
|
import type { User } from "@/types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
modelValue?: User | null;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
minChars?: number;
|
minChars?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: null,
|
||||||
placeholder: "Search users...",
|
placeholder: "Search users...",
|
||||||
minChars: 3,
|
minChars: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
select: [user: User];
|
"update:modelValue": [user: User | null];
|
||||||
input: [query: string];
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const usersStore = useUserStore();
|
const usersStore = useUserStore();
|
||||||
|
|
@ -78,8 +79,25 @@ onMounted(async () => {
|
||||||
if (!usersStore.all) {
|
if (!usersStore.all) {
|
||||||
await usersStore.fetchAll();
|
await usersStore.fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize search query with selected user's name if provided
|
||||||
|
if (props.modelValue) {
|
||||||
|
searchQuery.value = props.modelValue.display_name;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watch for external changes to modelValue
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
searchQuery.value = newValue.display_name;
|
||||||
|
} else {
|
||||||
|
searchQuery.value = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const filteredUsers = computed(() => {
|
const filteredUsers = computed(() => {
|
||||||
if (searchQuery.value.length < props.minChars || !usersStore.all) {
|
if (searchQuery.value.length < props.minChars || !usersStore.all) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -98,7 +116,11 @@ const filteredUsers = computed(() => {
|
||||||
|
|
||||||
function onInput() {
|
function onInput() {
|
||||||
showDropdown.value = searchQuery.value.length >= props.minChars;
|
showDropdown.value = searchQuery.value.length >= props.minChars;
|
||||||
emit("input", searchQuery.value);
|
|
||||||
|
// Clear selection if user is typing
|
||||||
|
if (props.modelValue && searchQuery.value !== props.modelValue.display_name) {
|
||||||
|
emit("update:modelValue", null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFocus() {
|
function onFocus() {
|
||||||
|
|
@ -117,7 +139,7 @@ function onBlur() {
|
||||||
function selectUser(user: User) {
|
function selectUser(user: User) {
|
||||||
searchQuery.value = user.display_name;
|
searchQuery.value = user.display_name;
|
||||||
showDropdown.value = false;
|
showDropdown.value = false;
|
||||||
emit("select", user);
|
emit("update:modelValue", user);
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightMatch(text: string): string {
|
function highlightMatch(text: string): string {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,152 @@
|
||||||
|
<style scoped lang="scss">
|
||||||
|
#content {
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-left: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
width: calc(100% - 250px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#content.expanded {
|
||||||
|
margin-left: 70px;
|
||||||
|
width: calc(100% - 70px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 */
|
||||||
|
}
|
||||||
|
#sidebar.impersonating {
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<template>
|
<template>
|
||||||
<div id="sidebar" :class="{ collapsed: isCollapsed }">
|
<div
|
||||||
|
id="sidebar"
|
||||||
|
:class="{ collapsed: isCollapsed, impersonating: isImpersonating }"
|
||||||
|
>
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<img class="logo" src="/static/img/nidus-logo-256-transparent.png" />
|
<img class="logo" src="/static/img/nidus-logo-256-transparent.png" />
|
||||||
|
|
@ -66,6 +213,7 @@ import { useSessionStore } from "@/store/session";
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const isCollapsed = ref(false);
|
const isCollapsed = ref(false);
|
||||||
|
const isImpersonating = ref(false);
|
||||||
|
|
||||||
const session = useSessionStore();
|
const session = useSessionStore();
|
||||||
|
|
||||||
|
|
@ -135,6 +283,8 @@ onMounted(async () => {
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
|
const s = await session.get();
|
||||||
|
isImpersonating.value = !!s.impersonating;
|
||||||
initializeBootstrap();
|
initializeBootstrap();
|
||||||
setTooltipsForSidebar();
|
setTooltipsForSidebar();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,9 @@
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="userSearch" class="form-label">Search Users</label>
|
<label for="userSearch" class="form-label">Search Users</label>
|
||||||
<UserSelector
|
<UserSelector
|
||||||
v-model="selectedUserId"
|
v-model="selectedUser"
|
||||||
label="Choose a user"
|
label="Choose a user"
|
||||||
placeholder="Select a user..."
|
placeholder="Select a user..."
|
||||||
@change="onUserChange"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|
@ -29,76 +28,9 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
<div class="table-responsive">
|
<button class="btn btn-danger" @click="doImpersonation" type="submit">
|
||||||
<table class="table table-hover">
|
Impersonate
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>User ID</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>1001</td>
|
|
||||||
<td>John Doe</td>
|
|
||||||
<td>john.doe@example.com</td>
|
|
||||||
<td><span class="badge bg-primary">Admin</span></td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-primary">
|
|
||||||
Impersonate <i class="bi bi-box-arrow-in-right"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>1002</td>
|
|
||||||
<td>Jane Smith</td>
|
|
||||||
<td>jane.smith@example.com</td>
|
|
||||||
<td><span class="badge bg-info">Support</span></td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-primary">
|
|
||||||
Impersonate <i class="bi bi-box-arrow-in-right"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>1003</td>
|
|
||||||
<td>Robert Johnson</td>
|
|
||||||
<td>robert@example.com</td>
|
|
||||||
<td><span class="badge bg-success">Premium User</span></td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-primary">
|
|
||||||
Impersonate <i class="bi bi-box-arrow-in-right"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>1004</td>
|
|
||||||
<td>Maria Garcia</td>
|
|
||||||
<td>maria@example.com</td>
|
|
||||||
<td><span class="badge bg-secondary">Standard User</span></td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-primary">
|
|
||||||
Impersonate <i class="bi bi-box-arrow-in-right"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="alert alert-warning mt-3 mb-0"
|
|
||||||
id="impersonationStatus"
|
|
||||||
style="display: none"
|
|
||||||
>
|
|
||||||
<i class="bi bi-exclamation-triangle"></i> You are currently
|
|
||||||
impersonating <strong>John Doe</strong>
|
|
||||||
<button class="btn btn-sm btn-warning float-end">
|
|
||||||
Exit Impersonation <i class="bi bi-box-arrow-left"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -113,9 +45,31 @@ import type { User } from "@/types";
|
||||||
|
|
||||||
const session = useSessionStore();
|
const session = useSessionStore();
|
||||||
|
|
||||||
const selectedUserId = ref<number | null>(null);
|
const selectedUser = ref<User | null>(null);
|
||||||
|
|
||||||
const onUserChange = (user: User | null) => {
|
const doImpersonation = async () => {
|
||||||
console.log("Selected user:", user);
|
if (!selectedUser.value) {
|
||||||
|
console.log("Can't impersonate, null user");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("doing impersonation of user", selectedUser.value);
|
||||||
|
const body = {
|
||||||
|
id: selectedUser.value.id,
|
||||||
|
};
|
||||||
|
const url = session.urls!.api.impersonation;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Upload failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
console.log("impersonation", result);
|
||||||
|
const new_session = await session.fetchSession();
|
||||||
|
console.log("session is now", new_session);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,7 @@ export interface URLs {
|
||||||
interface URLsAPI {
|
interface URLsAPI {
|
||||||
avatar: string;
|
avatar: string;
|
||||||
communication: string;
|
communication: string;
|
||||||
|
impersonation: string;
|
||||||
publicreport_message: string;
|
publicreport_message: string;
|
||||||
review_task: string;
|
review_task: string;
|
||||||
signal: string;
|
signal: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue