Make impersonation ending work, fix frontend events
This commit is contained in:
parent
522c5785a2
commit
4b87c74f41
18 changed files with 255 additions and 106 deletions
|
|
@ -17,7 +17,7 @@ type ConnectionSSE struct {
|
|||
chanEvent chan platform.Event
|
||||
id uuid.UUID
|
||||
organizationID int32
|
||||
userID int
|
||||
userID int32
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
|
|
@ -50,6 +50,9 @@ func SetEventChannel(chan_envelopes <-chan platform.Envelope) {
|
|||
if conn.organizationID == envelope.OrganizationID {
|
||||
log.Debug().Int("type", int(envelope.Event.Type)).Int32("env-org", envelope.OrganizationID).Msg("pushed event to client")
|
||||
conn.chanEvent <- envelope.Event
|
||||
} else if conn.userID == envelope.UserID {
|
||||
log.Debug().Int("type", int(envelope.Event.Type)).Int32("env-user", envelope.UserID).Msg("pushed event to user")
|
||||
conn.chanEvent <- envelope.Event
|
||||
} else {
|
||||
log.Debug().Int("type", int(envelope.Event.Type)).Int32("env-org", envelope.OrganizationID).Int32("conn-org", conn.organizationID).Msg("skipped event, bad org")
|
||||
}
|
||||
|
|
@ -87,7 +90,7 @@ func streamEvents(w http.ResponseWriter, r *http.Request, u platform.User) {
|
|||
chanEvent: make(chan platform.Event),
|
||||
id: uid,
|
||||
organizationID: u.Organization.ID,
|
||||
userID: u.ID,
|
||||
userID: int32(u.ID),
|
||||
}
|
||||
connectionsSSE[&connection] = true
|
||||
log.Debug().Int32("org", u.Organization.ID).Int("user", u.ID).Str("id", uid.String()).Msg("connected SSE client")
|
||||
|
|
|
|||
|
|
@ -20,6 +20,28 @@ import (
|
|||
|
||||
var decoder = schema.NewDecoder()
|
||||
|
||||
type handlerFunctionDelete func(context.Context, *http.Request, platform.User) *nhttp.ErrorWithStatus
|
||||
|
||||
func authenticatedHandlerDelete(f handlerFunctionDelete) http.Handler {
|
||||
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
|
||||
ctx := r.Context()
|
||||
e := f(ctx, r, u)
|
||||
if e != nil {
|
||||
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
|
||||
body, err := json.Marshal(ErrorAPI{Message: e.Error()})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal error")
|
||||
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Error(w, string(body), e.Status)
|
||||
return
|
||||
}
|
||||
http.Error(w, "", http.StatusNoContent)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
type handlerFunctionGetImage func(context.Context, *http.Request, platform.User) (file.Collection, uuid.UUID, *nhttp.ErrorWithStatus)
|
||||
|
||||
func authenticatedHandlerGetImage(f handlerFunctionGetImage) http.Handler {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ func AddRoutes(r *mux.Router) {
|
|||
r.Handle("/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentPost)).Methods("POST")
|
||||
impersonation := resource.Impersonation(router)
|
||||
r.Handle("/impersonation", authenticatedHandlerJSONPost(impersonation.Create)).Methods("POST")
|
||||
r.Handle("/impersonation", authenticatedHandlerDelete(impersonation.Delete)).Methods("DELETE")
|
||||
lead := resource.Lead(r)
|
||||
r.Handle("/leads", authenticatedHandlerJSON(lead.List)).Methods("GET")
|
||||
r.Handle("/leads", authenticatedHandlerJSONPost(lead.Create)).Methods("POST")
|
||||
|
|
|
|||
13
auth/auth.go
13
auth/auth.go
|
|
@ -36,6 +36,9 @@ func AddUserSession(ctx context.Context, user *platform.User) {
|
|||
sessionManager.Put(ctx, "username", user.Username)
|
||||
log.Debug().Str("id", id_str).Str("username", user.Username).Msg("added user session")
|
||||
}
|
||||
func ImpersonateEnd(ctx context.Context) {
|
||||
sessionManager.Put(ctx, "impersonated_user_id", "")
|
||||
}
|
||||
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)
|
||||
|
|
@ -53,7 +56,17 @@ func ImpersonatedUser(ctx context.Context) *int32 {
|
|||
result := int32(i)
|
||||
return &result
|
||||
}
|
||||
func ImpersonatorID(ctx context.Context) *int32 {
|
||||
user_id_str := sessionManager.GetString(ctx, "user_id")
|
||||
user_id, err := strconv.Atoi(user_id_str)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("user_id", user_id_str).Msg("failed to parse user_id")
|
||||
return nil
|
||||
}
|
||||
result := int32(user_id)
|
||||
return &result
|
||||
|
||||
}
|
||||
func GetAuthenticatedUser(r *http.Request) (*platform.User, error) {
|
||||
ctx := r.Context()
|
||||
user_id_str := sessionManager.GetString(ctx, "user_id")
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ func (e Event) MarshalJSON() ([]byte, error) {
|
|||
}
|
||||
|
||||
type Envelope struct {
|
||||
OrganizationID int32
|
||||
Event Event
|
||||
OrganizationID int32
|
||||
UserID int32
|
||||
}
|
||||
|
||||
func SetEventChannel(chan_events chan<- Envelope) {
|
||||
|
|
@ -90,6 +91,7 @@ const (
|
|||
TypeRMONuisance
|
||||
TypeRMOReport
|
||||
TypeRMOWater
|
||||
TypeSession
|
||||
TypeSignal
|
||||
)
|
||||
|
||||
|
|
@ -115,6 +117,17 @@ func Updated(t ResourceType, organization_id int32, uri_id string) {
|
|||
OrganizationID: organization_id,
|
||||
})
|
||||
}
|
||||
func UpdatedUser(t ResourceType, user_id int32, uri_id string) {
|
||||
go Send(Envelope{
|
||||
Event: Event{
|
||||
Resource: resourceString(t),
|
||||
Time: time.Now(),
|
||||
Type: EventTypeUpdated,
|
||||
URI: makeURI(t, uri_id),
|
||||
},
|
||||
UserID: user_id,
|
||||
})
|
||||
}
|
||||
func Send(env Envelope) {
|
||||
chanEvents <- env
|
||||
}
|
||||
|
|
@ -127,13 +140,15 @@ func resourceString(t ResourceType) string {
|
|||
case TypeNoteImage:
|
||||
return "sync:note:image"
|
||||
case TypeReviewTask:
|
||||
return "sync:review_task"
|
||||
return "sync:review-task"
|
||||
case TypeRMONuisance:
|
||||
return "rmo:nuisance"
|
||||
case TypeRMOReport:
|
||||
return "rmo:report"
|
||||
case TypeRMOWater:
|
||||
return "rmo:water"
|
||||
case TypeSession:
|
||||
return "sync:session"
|
||||
case TypeSignal:
|
||||
return "sync:signal"
|
||||
default:
|
||||
|
|
@ -143,19 +158,21 @@ func resourceString(t ResourceType) string {
|
|||
func makeURI(t ResourceType, id string) string {
|
||||
switch t {
|
||||
case TypeFileCSV:
|
||||
return config.MakeURLNidus("/upload/%s", id)
|
||||
return config.MakeURLNidus("/api/upload/%s", id)
|
||||
case TypeNoteAudio:
|
||||
return config.MakeURLNidus("/note/%s", id)
|
||||
return config.MakeURLNidus("/api/note/%s", id)
|
||||
case TypeNoteImage:
|
||||
return config.MakeURLNidus("/note/%s", id)
|
||||
return config.MakeURLNidus("/api/note/%s", id)
|
||||
case TypeReviewTask:
|
||||
return config.MakeURLNidus("/review/%s", id)
|
||||
return config.MakeURLNidus("/api/review/%s", id)
|
||||
case TypeRMONuisance:
|
||||
return config.MakeURLReport("/report/%s", id)
|
||||
return config.MakeURLReport("/api/report/%s", id)
|
||||
case TypeRMOWater:
|
||||
return config.MakeURLReport("/report/%s", id)
|
||||
return config.MakeURLReport("/api/report/%s", id)
|
||||
case TypeSession:
|
||||
return config.MakeURLReport("/api/session")
|
||||
case TypeSignal:
|
||||
return config.MakeURLReport("/signal/%s", id)
|
||||
return config.MakeURLReport("/api/signal/%s", id)
|
||||
default:
|
||||
return config.MakeURLReport("/unknown")
|
||||
}
|
||||
|
|
|
|||
53
platform/impersonation.go
Normal file
53
platform/impersonation.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/event"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func ImpersonationCreate(ctx context.Context, user User, target int) (*models.LogImpersonation, error) {
|
||||
if !user.HasRoot() {
|
||||
return nil, fmt.Errorf("user %d is not root, and therefore can't impersonate user %d", user.ID, target)
|
||||
}
|
||||
setter := models.LogImpersonationSetter{
|
||||
BeginAt: omit.From(time.Now()),
|
||||
EndAt: omitnull.FromPtr[time.Time](nil),
|
||||
//ID: ,
|
||||
ImpersonatorID: omit.From(int32(user.ID)),
|
||||
TargetID: omit.From(int32(target)),
|
||||
}
|
||||
log, err := models.LogImpersonations.Insert(&setter).One(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("insert log: %w", err)
|
||||
}
|
||||
event.UpdatedUser(event.TypeSession, user.model.ID, "")
|
||||
event.UpdatedUser(event.TypeSession, int32(target), "")
|
||||
return log, nil
|
||||
}
|
||||
func ImpersonationEnd(ctx context.Context, user User, impersonator_id int32) error {
|
||||
l, err := models.LogImpersonations.Query(
|
||||
models.SelectWhere.LogImpersonations.EndAt.IsNull(),
|
||||
models.SelectWhere.LogImpersonations.ImpersonatorID.EQ(impersonator_id),
|
||||
).One(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("query impersonations: %w", err)
|
||||
}
|
||||
err = l.Update(ctx, db.PGInstance.BobDB, &models.LogImpersonationSetter{
|
||||
EndAt: omitnull.From(time.Now()),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update impersonation log: %w", err)
|
||||
}
|
||||
log.Info().Int32("impersonator", l.ImpersonatorID).Int32("target", l.TargetID).Msg("Stopped impersonating")
|
||||
event.UpdatedUser(event.TypeSession, user.model.ID, "")
|
||||
event.UpdatedUser(event.TypeSession, impersonator_id, "")
|
||||
return nil
|
||||
}
|
||||
56
resource/impersonation.go
Normal file
56
resource/impersonation.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/auth"
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Impersonation(r *router) *impersonationR {
|
||||
return &impersonationR{
|
||||
router: r,
|
||||
}
|
||||
}
|
||||
|
||||
type impersonationR struct {
|
||||
router *router
|
||||
}
|
||||
type impersonation struct {
|
||||
UserID omit.Val[int] `json:"id"`
|
||||
}
|
||||
|
||||
func (res *impersonationR) Create(ctx context.Context, r *http.Request, u platform.User, i impersonation) (*impersonation, *nhttp.ErrorWithStatus) {
|
||||
if i.UserID.IsUnset() {
|
||||
return nil, nhttp.NewBadRequest("you must provide an 'id'")
|
||||
}
|
||||
target_id := i.UserID.MustGet()
|
||||
l, err := platform.ImpersonationCreate(ctx, u, target_id)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("create impersonation: %w", err)
|
||||
}
|
||||
auth.ImpersonateUser(ctx, target_id)
|
||||
log.Info().Int("user.id", u.ID).Str("username", u.Username).Int("target.id", target_id).Int32("log.id", l.ID).Msg("Impersonation begins")
|
||||
return &impersonation{
|
||||
UserID: i.UserID,
|
||||
}, nil
|
||||
}
|
||||
func (res *impersonationR) Delete(ctx context.Context, r *http.Request, u platform.User) *nhttp.ErrorWithStatus {
|
||||
if auth.ImpersonatedUser == nil {
|
||||
return nhttp.NewBadRequest("not impersonating")
|
||||
}
|
||||
real_user_id := auth.ImpersonatorID(ctx)
|
||||
if real_user_id == nil {
|
||||
return nhttp.NewError("No impersonator ID")
|
||||
}
|
||||
err := platform.ImpersonationEnd(ctx, u, *real_user_id)
|
||||
if err != nil {
|
||||
return nhttp.NewError("end impersonation: %w", err)
|
||||
}
|
||||
auth.ImpersonateEnd(ctx)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
// Define types for the SSE data structure
|
||||
interface SSEMessage {
|
||||
export interface SSEMessage {
|
||||
resource: string;
|
||||
time: string;
|
||||
type: string;
|
||||
count?: number;
|
||||
[key: string]: any; // Allow additional properties
|
||||
uri: string;
|
||||
}
|
||||
|
||||
type SSEHandler = (data: SSEMessage) => void;
|
||||
|
|
@ -10,48 +11,25 @@ type SSEHandler = (data: SSEMessage) => void;
|
|||
interface SSEManagerType {
|
||||
connect: (url: string) => Promise<EventSource>;
|
||||
disconnect: () => void;
|
||||
subscribe: (eventType: string, handler: SSEHandler) => void;
|
||||
unsubscribe: (eventType: string, handler: SSEHandler) => void;
|
||||
subscribe: (handler: SSEHandler) => string;
|
||||
unsubscribe: (uuid: string) => void;
|
||||
ready: (callback: (eventSource: EventSource) => void) => void;
|
||||
}
|
||||
|
||||
/*
|
||||
declare global {
|
||||
interface Window {
|
||||
SSEManager: SSEManagerType;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
export const SSEManager: SSEManagerType = (function (): SSEManagerType {
|
||||
let eventSource: EventSource | null = null;
|
||||
let subscribers: Map<string, SSEHandler[]> = new Map();
|
||||
let subscribers: Map<string, SSEHandler> = new Map();
|
||||
let isConnected: boolean = false;
|
||||
let connectionPromise: Promise<EventSource> | null = null;
|
||||
|
||||
function subscribe(eventType: string, handler: SSEHandler): void {
|
||||
if (!subscribers.has(eventType)) {
|
||||
subscribers.set(eventType, []);
|
||||
}
|
||||
subscribers.get(eventType)!.push(handler);
|
||||
|
||||
// If already connected, attach the listener immediately
|
||||
if (isConnected && eventSource) {
|
||||
eventSource.addEventListener(eventType, handler as EventListener);
|
||||
}
|
||||
}
|
||||
|
||||
function unsubscribe(eventType: string, handler: SSEHandler): void {
|
||||
if (subscribers.has(eventType)) {
|
||||
const handlers = subscribers.get(eventType)!;
|
||||
const index = handlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
if (eventSource) {
|
||||
eventSource.removeEventListener(eventType, handler as EventListener);
|
||||
}
|
||||
}
|
||||
|
||||
function connect(url: string): Promise<EventSource> {
|
||||
if (connectionPromise) {
|
||||
return connectionPromise;
|
||||
|
|
@ -63,19 +41,9 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType {
|
|||
eventSource.onopen = function (): void {
|
||||
isConnected = true;
|
||||
|
||||
// Attach all pre-registered handlers
|
||||
subscribers.forEach((handlers: SSEHandler[], eventType: string) => {
|
||||
handlers.forEach((handler: SSEHandler) => {
|
||||
eventSource!.addEventListener(
|
||||
"message",
|
||||
(message: MessageEvent) => {
|
||||
const data: SSEMessage = JSON.parse(message.data);
|
||||
if (eventType === "*" || eventType === data.type) {
|
||||
handler(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
eventSource!.addEventListener("message", (message: MessageEvent) => {
|
||||
const data: SSEMessage = JSON.parse(message.data);
|
||||
handleMessage(data);
|
||||
});
|
||||
|
||||
console.log("SSE connected");
|
||||
|
|
@ -115,6 +83,15 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType {
|
|||
}
|
||||
}
|
||||
|
||||
function handleMessage(msg: SSEMessage) {
|
||||
if (msg.type == "heartbeat") {
|
||||
return;
|
||||
}
|
||||
subscribers.forEach((handler: SSEHandler, _: string) => {
|
||||
handler(msg);
|
||||
});
|
||||
}
|
||||
|
||||
function ready(callback: (eventSource: EventSource) => void): void {
|
||||
if (connectionPromise) {
|
||||
connectionPromise.then(callback);
|
||||
|
|
@ -129,6 +106,18 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType {
|
|||
}
|
||||
}
|
||||
|
||||
function subscribe(handler: SSEHandler): string {
|
||||
const uuid = crypto.randomUUID();
|
||||
subscribers.set(uuid.toString(), handler);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
function unsubscribe(uuid: string): void {
|
||||
if (subscribers.has(uuid)) {
|
||||
subscribers.delete(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connect,
|
||||
disconnect,
|
||||
|
|
@ -137,11 +126,3 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType {
|
|||
ready,
|
||||
};
|
||||
})();
|
||||
|
||||
function updateNotificationBadge(data: SSEMessage): void {
|
||||
const badge = document.querySelector<HTMLElement>(".notification-badge");
|
||||
if (badge) {
|
||||
badge.textContent = String(data.count || 0);
|
||||
badge.style.display = (data.count || 0) > 0 ? "block" : "none";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,8 +208,9 @@
|
|||
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||
import { Tooltip, Popover } from "bootstrap";
|
||||
import NavigationLink from "@/components/common/NavigationLink.vue";
|
||||
import { SSEManager } from "@/SSEManager";
|
||||
import { SSEManager, type SSEMessage } from "@/SSEManager";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
import type { Session } from "@/types";
|
||||
|
||||
// Reactive state
|
||||
const isCollapsed = ref(false);
|
||||
|
|
@ -279,6 +280,14 @@ const setTooltipsForSidebar = () => {
|
|||
|
||||
// Lifecycle hooks
|
||||
onMounted(async () => {
|
||||
const sub = SSEManager.subscribe((msg: SSEMessage) => {
|
||||
if (msg.resource != "sync:session") {
|
||||
return;
|
||||
}
|
||||
session.fetchSession().then((s: Session) => {
|
||||
isImpersonating.value = !!s.impersonating;
|
||||
});
|
||||
});
|
||||
restoreLocalStorage();
|
||||
|
||||
await nextTick();
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
<i class="bi bi-people"></i> Impersonate User
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<template v-if="isImpersonating && impersonatedUser">
|
||||
<template v-if="session.impersonating">
|
||||
<h1>You're impersonating</h1>
|
||||
<p>{{ impersonatedUser.username }}</p>
|
||||
<p>{{ session.impersonating }}</p>
|
||||
<button class="btn btn-primary" @click="doImpersonationEnd">
|
||||
End Impersonation
|
||||
</button>
|
||||
|
|
@ -96,14 +96,13 @@ const doImpersonationStart = async () => {
|
|||
const new_session = await session.fetchSession();
|
||||
console.log("session is now", new_session);
|
||||
};
|
||||
/*
|
||||
onMounted(() => {
|
||||
session.get().then((session: Session) => {
|
||||
if (session.impersonating) {
|
||||
isImpersonating.value = true;
|
||||
console.log("is impersonating, but who?");
|
||||
user.byURI(session.impersonating).then((user: User | null) => {
|
||||
impersonatedUser.value = user;
|
||||
console.log("is impersonating", user);
|
||||
});
|
||||
} else {
|
||||
isImpersonating.value = false;
|
||||
|
|
@ -111,4 +110,5 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
});
|
||||
*/
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createApp } from "vue";
|
|||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { SSEManager } from "./SSEManager";
|
||||
import { SSEManager, type SSEMessage } from "./SSEManager";
|
||||
//import { SetupSidebar } from "./sidebar";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
|
|
@ -22,9 +22,9 @@ window.SSEManager = SSEManager;
|
|||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
SSEManager.connect("/api/events");
|
||||
SSEManager.subscribe("*", (e) => {
|
||||
if (e.type != "heartbeat") {
|
||||
console.log("SSE", e);
|
||||
SSEManager.subscribe((msg: SSEMessage) => {
|
||||
if (msg.type != "heartbeat") {
|
||||
console.log("SSE", msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import { Communication } from "../types";
|
||||
import { SSEManager } from "../SSEManager";
|
||||
import { SSEManager, SSEMessage } from "../SSEManager";
|
||||
import { useSessionStore } from "./session";
|
||||
|
||||
export const useCommunicationStore = defineStore("communication", () => {
|
||||
|
|
@ -11,8 +11,8 @@ export const useCommunicationStore = defineStore("communication", () => {
|
|||
const error = ref(null);
|
||||
|
||||
// Subscription
|
||||
SSEManager.subscribe("*", (e) => {
|
||||
if (e.resource.startsWith("rmo")) {
|
||||
SSEManager.subscribe((msg: SSEMessage) => {
|
||||
if (msg.resource.startsWith("rmo:")) {
|
||||
fetchAll();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { SSEManager } from "@/SSEManager";
|
||||
import { SSEManager, SSEMessage } from "@/SSEManager";
|
||||
import { ReviewTask } from "@/types";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
|
||||
|
|
@ -11,8 +11,8 @@ export const useReviewTaskStore = defineStore("review-task", () => {
|
|||
const error = ref<string | null>(null);
|
||||
|
||||
// Subscription
|
||||
SSEManager.subscribe("*", (e) => {
|
||||
if (e.resource.startsWith("review-task")) {
|
||||
SSEManager.subscribe((msg: SSEMessage) => {
|
||||
if (msg.resource.startsWith("sync:review-task")) {
|
||||
fetchAll();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { SSEManager } from "@/SSEManager";
|
||||
import { SSEManager, type SSEMessage } from "@/SSEManager";
|
||||
import {
|
||||
Organization,
|
||||
Session,
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
|
||||
export const useSessionStore = defineStore("session", () => {
|
||||
// State
|
||||
const impersonating = ref<string | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
const loading = ref(false);
|
||||
const current = ref<Session | null>(null);
|
||||
|
|
@ -21,8 +22,8 @@ export const useSessionStore = defineStore("session", () => {
|
|||
const urls = ref<URLs | null>(null);
|
||||
|
||||
// Subscription
|
||||
SSEManager.subscribe("*", (e) => {
|
||||
if (e.type !== "heartbeat") {
|
||||
SSEManager.subscribe((msg: SSEMessage) => {
|
||||
if (msg.type !== "sync:session") {
|
||||
fetchSession();
|
||||
}
|
||||
});
|
||||
|
|
@ -37,6 +38,7 @@ export const useSessionStore = defineStore("session", () => {
|
|||
if (!response.ok) throw new Error("Failed to fetch user");
|
||||
|
||||
const data: Session = await response.json();
|
||||
impersonating.value = data.impersonating || null;
|
||||
notification_counts.value = data.notification_counts;
|
||||
organization.value = data.organization;
|
||||
self.value = data.self;
|
||||
|
|
@ -65,15 +67,15 @@ export const useSessionStore = defineStore("session", () => {
|
|||
return ongoingFetch.value;
|
||||
}
|
||||
|
||||
ongoingFetch.value = fetchSession().finally(() => {
|
||||
ongoingFetch.value = null;
|
||||
});
|
||||
return ongoingFetch.value;
|
||||
const s = await fetchSession();
|
||||
current.value = s;
|
||||
ongoingFetch.value = null;
|
||||
return s;
|
||||
}
|
||||
return {
|
||||
// State
|
||||
current,
|
||||
error,
|
||||
impersonating,
|
||||
loading,
|
||||
notification_counts,
|
||||
organization,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import { Signal } from "../types";
|
||||
import { SSEManager } from "../SSEManager";
|
||||
import { SSEManager, type SSEMessage } from "../SSEManager";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
|
||||
export const useSignalStore = defineStore("signal", () => {
|
||||
|
|
@ -11,8 +11,8 @@ export const useSignalStore = defineStore("signal", () => {
|
|||
const error = ref(null);
|
||||
|
||||
// Subscription
|
||||
SSEManager.subscribe("*", (e) => {
|
||||
if (e.resource.startsWith("signal")) {
|
||||
SSEManager.subscribe((msg: SSEMessage) => {
|
||||
if (msg.resource.startsWith("sync:signal")) {
|
||||
fetchAll();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { Upload } from "@/types";
|
||||
import { SSEManager } from "@/SSEManager";
|
||||
import { SSEManager, type SSEMessage } from "@/SSEManager";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
|
||||
export const useUploadStore = defineStore("upload", () => {
|
||||
|
|
@ -12,8 +12,8 @@ export const useUploadStore = defineStore("upload", () => {
|
|||
const error = ref(null);
|
||||
|
||||
// Subscription
|
||||
SSEManager.subscribe("*", (e) => {
|
||||
if (e.resource.startsWith("upload")) {
|
||||
SSEManager.subscribe((msg: SSEMessage) => {
|
||||
if (msg.resource.startsWith("sync:upload")) {
|
||||
fetchAll();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { Session, User } from "@/types";
|
||||
import { SSEManager } from "@/SSEManager";
|
||||
import { SSEManager, type SSEMessage } from "@/SSEManager";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
|
||||
export const useUserStore = defineStore("users", () => {
|
||||
|
|
@ -13,8 +13,8 @@ export const useUserStore = defineStore("users", () => {
|
|||
const ongoingFetch = ref<Promise<User[]> | null>(null);
|
||||
|
||||
// Subscription
|
||||
SSEManager.subscribe("*", (e) => {
|
||||
if (e.resource.startsWith("users")) {
|
||||
SSEManager.subscribe((msg: SSEMessage) => {
|
||||
if (msg.resource.startsWith("sync:user")) {
|
||||
fetchAll();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -59,14 +59,15 @@
|
|||
import { computed, onMounted, ref } from "vue";
|
||||
import maplibregl from "maplibre-gl";
|
||||
|
||||
import { useCommunicationStore } from "@/store/communication";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
import CommunicationColumnAction from "@/components/CommunicationColumnAction.vue";
|
||||
import CommunicationColumnDetail from "@/components/CommunicationColumnDetail.vue";
|
||||
import CommunicationColumnList from "@/components/CommunicationColumnList.vue";
|
||||
import ImageViewerModal from "@/components/ImageViewerModal.vue";
|
||||
import ThreeColumn from "@/components/layout/ThreeColumn.vue";
|
||||
import ToastNotification from "@/components/ToastNotification.vue";
|
||||
import { SSEManager } from "@/SSEManager";
|
||||
import { useCommunicationStore } from "@/store/communication";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
import { Bounds, Communication, Marker } from "@/types";
|
||||
|
||||
const communication = useCommunicationStore();
|
||||
|
|
@ -307,15 +308,6 @@ function updateMap() {
|
|||
onMounted(async () => {
|
||||
await loadFromAPI();
|
||||
|
||||
// Subscribe to SSE events
|
||||
if (window.SSEManager) {
|
||||
window.SSEManager.subscribe("*", (e) => {
|
||||
if (e.resource.startsWith("rmo:")) {
|
||||
fetchCommunications();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup map layer after next tick to ensure map is mounted
|
||||
/*
|
||||
if (mapRef.value) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue