feat: add logout endpoint and users/me endpoint
- POST /api/v1/logout: deletes client token, returns {status: ok}
- GET /api/v1/users/me: returns session info (delegates to HandleState)
- Add DeleteClient, GetSessionCount, ClientCountForSession, DeleteStaleSessions to db layer
- Add user count to GET /api/v1/server response
- Extract setupAPIv1 to fix funlen lint issue
This commit is contained in:
@@ -779,6 +779,96 @@ func (database *Database) DeleteSession(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteClient removes a single client record by ID.
|
||||||
|
func (database *Database) DeleteClient(
|
||||||
|
ctx context.Context,
|
||||||
|
clientID int64,
|
||||||
|
) error {
|
||||||
|
_, err := database.conn.ExecContext(
|
||||||
|
ctx,
|
||||||
|
"DELETE FROM clients WHERE id = ?",
|
||||||
|
clientID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSessionCount returns the number of active sessions.
|
||||||
|
func (database *Database) GetSessionCount(
|
||||||
|
ctx context.Context,
|
||||||
|
) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err := database.conn.QueryRowContext(
|
||||||
|
ctx,
|
||||||
|
"SELECT COUNT(*) FROM sessions",
|
||||||
|
).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"get session count: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientCountForSession returns the number of clients
|
||||||
|
// belonging to a session.
|
||||||
|
func (database *Database) ClientCountForSession(
|
||||||
|
ctx context.Context,
|
||||||
|
sessionID int64,
|
||||||
|
) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err := database.conn.QueryRowContext(
|
||||||
|
ctx,
|
||||||
|
`SELECT COUNT(*) FROM clients
|
||||||
|
WHERE session_id = ?`,
|
||||||
|
sessionID,
|
||||||
|
).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"client count for session: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteStaleSessions removes clients not seen since the
|
||||||
|
// cutoff and cleans up orphaned sessions.
|
||||||
|
func (database *Database) DeleteStaleSessions(
|
||||||
|
ctx context.Context,
|
||||||
|
cutoff time.Time,
|
||||||
|
) (int64, error) {
|
||||||
|
res, err := database.conn.ExecContext(ctx,
|
||||||
|
"DELETE FROM clients WHERE last_seen < ?",
|
||||||
|
cutoff,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"delete stale clients: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleted, _ := res.RowsAffected()
|
||||||
|
|
||||||
|
_, err = database.conn.ExecContext(ctx,
|
||||||
|
`DELETE FROM sessions WHERE id NOT IN
|
||||||
|
(SELECT DISTINCT session_id FROM clients)`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return deleted, fmt.Errorf(
|
||||||
|
"delete orphan sessions: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetSessionChannels returns channels a session
|
// GetSessionChannels returns channels a session
|
||||||
// belongs to.
|
// belongs to.
|
||||||
func (database *Database) GetSessionChannels(
|
func (database *Database) GetSessionChannels(
|
||||||
|
|||||||
@@ -1361,20 +1361,71 @@ func (hdlr *Handlers) canAccessChannelHistory(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleServerInfo returns server metadata.
|
// HandleLogout deletes the authenticated client's token.
|
||||||
func (hdlr *Handlers) HandleServerInfo() http.HandlerFunc {
|
func (hdlr *Handlers) HandleLogout() http.HandlerFunc {
|
||||||
type infoResponse struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
MOTD string `json:"motd"`
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(
|
return func(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
request *http.Request,
|
request *http.Request,
|
||||||
) {
|
) {
|
||||||
hdlr.respondJSON(writer, request, &infoResponse{
|
_, clientID, _, ok :=
|
||||||
Name: hdlr.params.Config.ServerName,
|
hdlr.requireAuth(writer, request)
|
||||||
MOTD: hdlr.params.Config.MOTD,
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := hdlr.params.Database.DeleteClient(
|
||||||
|
request.Context(), clientID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
hdlr.log.Error(
|
||||||
|
"delete client failed", "error", err,
|
||||||
|
)
|
||||||
|
hdlr.respondError(
|
||||||
|
writer, request,
|
||||||
|
"internal error",
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hdlr.respondJSON(writer, request,
|
||||||
|
map[string]string{"status": "ok"},
|
||||||
|
http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleUsersMe returns the current user's session info.
|
||||||
|
func (hdlr *Handlers) HandleUsersMe() http.HandlerFunc {
|
||||||
|
return hdlr.HandleState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleServerInfo returns server metadata.
|
||||||
|
func (hdlr *Handlers) HandleServerInfo() http.HandlerFunc {
|
||||||
|
return func(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
request *http.Request,
|
||||||
|
) {
|
||||||
|
users, err := hdlr.params.Database.GetSessionCount(
|
||||||
|
request.Context(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
hdlr.log.Error(
|
||||||
|
"get session count failed", "error", err,
|
||||||
|
)
|
||||||
|
hdlr.respondError(
|
||||||
|
writer, request,
|
||||||
|
"internal error",
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hdlr.respondJSON(writer, request, map[string]any{
|
||||||
|
"name": hdlr.params.Config.ServerName,
|
||||||
|
"motd": hdlr.params.Config.MOTD,
|
||||||
|
"users": users,
|
||||||
}, http.StatusOK)
|
}, http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,48 +59,55 @@ func (srv *Server) SetupRoutes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// API v1.
|
// API v1.
|
||||||
srv.router.Route(
|
srv.router.Route("/api/v1", srv.setupAPIv1)
|
||||||
"/api/v1",
|
|
||||||
func(router chi.Router) {
|
|
||||||
router.Get(
|
|
||||||
"/server",
|
|
||||||
srv.handlers.HandleServerInfo(),
|
|
||||||
)
|
|
||||||
router.Post(
|
|
||||||
"/session",
|
|
||||||
srv.handlers.HandleCreateSession(),
|
|
||||||
)
|
|
||||||
router.Get(
|
|
||||||
"/state",
|
|
||||||
srv.handlers.HandleState(),
|
|
||||||
)
|
|
||||||
router.Get(
|
|
||||||
"/messages",
|
|
||||||
srv.handlers.HandleGetMessages(),
|
|
||||||
)
|
|
||||||
router.Post(
|
|
||||||
"/messages",
|
|
||||||
srv.handlers.HandleSendCommand(),
|
|
||||||
)
|
|
||||||
router.Get(
|
|
||||||
"/history",
|
|
||||||
srv.handlers.HandleGetHistory(),
|
|
||||||
)
|
|
||||||
router.Get(
|
|
||||||
"/channels",
|
|
||||||
srv.handlers.HandleListAllChannels(),
|
|
||||||
)
|
|
||||||
router.Get(
|
|
||||||
"/channels/{channel}/members",
|
|
||||||
srv.handlers.HandleChannelMembers(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Serve embedded SPA.
|
// Serve embedded SPA.
|
||||||
srv.setupSPA()
|
srv.setupSPA()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *Server) setupAPIv1(router chi.Router) {
|
||||||
|
router.Get(
|
||||||
|
"/server",
|
||||||
|
srv.handlers.HandleServerInfo(),
|
||||||
|
)
|
||||||
|
router.Post(
|
||||||
|
"/session",
|
||||||
|
srv.handlers.HandleCreateSession(),
|
||||||
|
)
|
||||||
|
router.Get(
|
||||||
|
"/state",
|
||||||
|
srv.handlers.HandleState(),
|
||||||
|
)
|
||||||
|
router.Post(
|
||||||
|
"/logout",
|
||||||
|
srv.handlers.HandleLogout(),
|
||||||
|
)
|
||||||
|
router.Get(
|
||||||
|
"/users/me",
|
||||||
|
srv.handlers.HandleUsersMe(),
|
||||||
|
)
|
||||||
|
router.Get(
|
||||||
|
"/messages",
|
||||||
|
srv.handlers.HandleGetMessages(),
|
||||||
|
)
|
||||||
|
router.Post(
|
||||||
|
"/messages",
|
||||||
|
srv.handlers.HandleSendCommand(),
|
||||||
|
)
|
||||||
|
router.Get(
|
||||||
|
"/history",
|
||||||
|
srv.handlers.HandleGetHistory(),
|
||||||
|
)
|
||||||
|
router.Get(
|
||||||
|
"/channels",
|
||||||
|
srv.handlers.HandleListAllChannels(),
|
||||||
|
)
|
||||||
|
router.Get(
|
||||||
|
"/channels/{channel}/members",
|
||||||
|
srv.handlers.HandleChannelMembers(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (srv *Server) setupSPA() {
|
func (srv *Server) setupSPA() {
|
||||||
distFS, err := fs.Sub(web.Dist, "dist")
|
distFS, err := fs.Sub(web.Dist, "dist")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user