diff --git a/internal/api/router.go b/internal/api/router.go index dbe877a9..575c08e1 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -7,6 +7,7 @@ import ( "github.com/demodesk/neko/internal/api/members" "github.com/demodesk/neko/internal/api/room" + "github.com/demodesk/neko/internal/api/sessions" "github.com/demodesk/neko/pkg/auth" "github.com/demodesk/neko/pkg/types" "github.com/demodesk/neko/pkg/utils" @@ -45,7 +46,9 @@ func (api *ApiManagerCtx) Route(r types.Router) { r.Post("/logout", api.Logout) r.Get("/whoami", api.Whoami) - r.Get("/sessions", api.Sessions) + + sessionsHandler := sessions.New(api.sessions) + r.Route("/sessions", sessionsHandler.Route) membersHandler := members.New(api.members) r.Route("/members", membersHandler.Route) diff --git a/internal/api/session.go b/internal/api/session.go index 8c631e62..5f153160 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -83,16 +83,3 @@ func (api *ApiManagerCtx) Whoami(w http.ResponseWriter, r *http.Request) error { State: session.State(), }) } - -func (api *ApiManagerCtx) Sessions(w http.ResponseWriter, r *http.Request) error { - sessions := []SessionDataPayload{} - for _, session := range api.sessions.List() { - sessions = append(sessions, SessionDataPayload{ - ID: session.ID(), - Profile: session.Profile(), - State: session.State(), - }) - } - - return utils.HttpSuccess(w, sessions) -} diff --git a/internal/api/sessions/controller.go b/internal/api/sessions/controller.go new file mode 100644 index 00000000..c4ce44c0 --- /dev/null +++ b/internal/api/sessions/controller.go @@ -0,0 +1,80 @@ +package sessions + +import ( + "errors" + "net/http" + + "github.com/demodesk/neko/pkg/auth" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/utils" + "github.com/go-chi/chi" +) + +type SessionDataPayload struct { + ID string `json:"id"` + Profile types.MemberProfile `json:"profile"` + State types.SessionState `json:"state"` +} + +func (h *SessionsHandler) sessionsList(w http.ResponseWriter, r *http.Request) error { + sessions := []SessionDataPayload{} + for _, session := range h.sessions.List() { + sessions = append(sessions, SessionDataPayload{ + ID: session.ID(), + Profile: session.Profile(), + State: session.State(), + }) + } + + return utils.HttpSuccess(w, sessions) +} + +func (h *SessionsHandler) sessionsRead(w http.ResponseWriter, r *http.Request) error { + sessionId := chi.URLParam(r, "sessionId") + + session, ok := h.sessions.Get(sessionId) + if !ok { + return utils.HttpNotFound("session not found") + } + + return utils.HttpSuccess(w, SessionDataPayload{ + ID: session.ID(), + Profile: session.Profile(), + State: session.State(), + }) +} + +func (h *SessionsHandler) sessionsDelete(w http.ResponseWriter, r *http.Request) error { + session, _ := auth.GetSession(r) + + sessionId := chi.URLParam(r, "sessionId") + if sessionId == session.ID() { + return utils.HttpBadRequest("cannot delete own session") + } + + err := h.sessions.Delete(sessionId) + if err != nil { + if errors.Is(err, types.ErrSessionNotFound) { + return utils.HttpBadRequest("session not found") + } else { + return utils.HttpInternalServerError().WithInternalErr(err) + } + } + + return utils.HttpSuccess(w) +} + +func (h *SessionsHandler) sessionsDisconnect(w http.ResponseWriter, r *http.Request) error { + sessionId := chi.URLParam(r, "sessionId") + + err := h.sessions.Disconnect(sessionId) + if err != nil { + if errors.Is(err, types.ErrSessionNotFound) { + return utils.HttpBadRequest("session not found") + } else { + return utils.HttpInternalServerError().WithInternalErr(err) + } + } + + return utils.HttpSuccess(w) +} diff --git a/internal/api/sessions/handler.go b/internal/api/sessions/handler.go new file mode 100644 index 00000000..5f5b7711 --- /dev/null +++ b/internal/api/sessions/handler.go @@ -0,0 +1,30 @@ +package sessions + +import ( + "github.com/demodesk/neko/pkg/auth" + "github.com/demodesk/neko/pkg/types" +) + +type SessionsHandler struct { + sessions types.SessionManager +} + +func New( + sessions types.SessionManager, +) *SessionsHandler { + // Init + + return &SessionsHandler{ + sessions: sessions, + } +} + +func (h *SessionsHandler) Route(r types.Router) { + r.Get("/", h.sessionsList) + + r.With(auth.AdminsOnly).Route("/{sessionId}", func(r types.Router) { + r.Get("/", h.sessionsRead) + r.Delete("/", h.sessionsDelete) + r.Post("/disconnect", h.sessionsDisconnect) + }) +} diff --git a/internal/session/manager.go b/internal/session/manager.go index 44f9d57b..7b278b99 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -158,6 +158,26 @@ func (manager *SessionManagerCtx) Delete(id string) error { return nil } +func (manager *SessionManagerCtx) Disconnect(id string) error { + manager.sessionsMu.Lock() + session, ok := manager.sessions[id] + if !ok { + manager.sessionsMu.Unlock() + return types.ErrSessionNotFound + } + manager.sessionsMu.Unlock() + + if session.State().IsConnected { + session.DestroyWebSocketPeer("session disconnected") + } + + if session.State().IsWatching { + session.GetWebRTCPeer().Destroy() + } + + return nil +} + func (manager *SessionManagerCtx) Get(id string) (types.Session, bool) { manager.sessionsMu.Lock() defer manager.sessionsMu.Unlock() diff --git a/openapi.yaml b/openapi.yaml index 2fa9e787..b7ba4425 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -13,8 +13,8 @@ servers: url: http://localhost:3000 tags: - - name: session - description: Session management. + - name: sessions + description: Sessions management. - name: room description: Room releated operations. - name: members @@ -61,13 +61,11 @@ paths: required: true # - # session + # current session # /api/login: post: - tags: - - session summary: login operationId: login security: [] @@ -90,8 +88,6 @@ paths: required: true /api/logout: post: - tags: - - session summary: logout operationId: logout responses: @@ -101,8 +97,6 @@ paths: $ref: '#/components/responses/Unauthorized' /api/whoami: get: - tags: - - session summary: whoami operationId: whoami responses: @@ -116,10 +110,15 @@ paths: $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' + + # + # sessions + # + /api/sessions: get: tags: - - session + - sessions summary: get sessions operationId: sessionsGet responses: @@ -135,6 +134,75 @@ paths: $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' + /api/sessions/{sessionId}: + get: + tags: + - sessions + summary: get session + operationId: sessionGet + parameters: + - in: path + name: sessionId + description: session identifier + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SessionData' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + delete: + tags: + - sessions + summary: remove session + operationId: sessionRemove + parameters: + - in: path + name: sessionId + description: session identifier + required: true + schema: + type: string + responses: + '204': + description: OK + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + /api/sessions/{sessionId}/disconnect: + post: + tags: + - sessions + summary: disconnect session + operationId: sessionDisconnect + parameters: + - in: path + name: sessionId + description: session identifier + required: true + schema: + type: string + responses: + '204': + description: OK + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' # # room @@ -1023,7 +1091,7 @@ components: type: integer # - # session + # sessions # SessionLogin: diff --git a/pkg/types/session.go b/pkg/types/session.go index 5ea67670..d3f69b2c 100644 --- a/pkg/types/session.go +++ b/pkg/types/session.go @@ -81,6 +81,7 @@ type SessionManager interface { Create(id string, profile MemberProfile) (Session, string, error) Update(id string, profile MemberProfile) error Delete(id string) error + Disconnect(id string) error Get(id string) (Session, bool) GetByToken(token string) (Session, bool) List() []Session