Files
Commilitia-Drop/internal/httpapi/devices.go
T
admin f21fa5b5e8 cdrop — 跨 OS 剪贴板与文件传输服务
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。

- 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。
- 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。
- 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。
- 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。

架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。

本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
2026-06-15 21:38:28 +08:00

73 lines
2.2 KiB
Go

package httpapi
import (
"log/slog"
"net/http"
"github.com/go-chi/chi/v5"
"commilitia.net/cdrop/internal/db"
"commilitia.net/cdrop/internal/jwtauth"
)
type deviceItem struct {
Name string `json:"name"`
Type string `json:"type"`
Online bool `json:"online"`
LastSeen int64 `json:"last_seen"`
}
// handleDevices returns the calling user's known devices with live online flags.
// Online = currently connected to /api/events; LastSeen = epoch from devices.last_seen.
func (s *Server) handleDevices(w http.ResponseWriter, r *http.Request) {
claims, _ := jwtauth.ClaimsFromContext(r.Context())
devs, err := s.queries.ListDevicesByUser(r.Context(), claims.UserID)
if err != nil {
slog.Error("list devices failed", "err", err, "user", claims.UserID)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
return
}
out := make([]deviceItem, 0, len(devs))
for _, d := range devs {
out = append(out, deviceItem{
Name: d.Name,
Type: d.Type,
Online: s.hub.Online(claims.UserID, d.Name),
LastSeen: d.LastSeen,
})
}
writeJSON(w, http.StatusOK, map[string]any{"devices": out})
}
// handleDeleteDevice 注销设备:删除 (user, name) 行 + Kick 在线 SSE + 立刻广播
// 新的 presence。请求方若注销的是自己当前设备,前端需同步调用 logout,否则
// 任何后续带 X-Device-Name 的认证请求都会被中间件再次 UPSERT 回来。
func (s *Server) handleDeleteDevice(w http.ResponseWriter, r *http.Request) {
claims, _ := jwtauth.ClaimsFromContext(r.Context())
name := chi.URLParam(r, "name")
if name == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing device name"})
return
}
rows, err := s.queries.DeleteDevice(r.Context(), db.DeleteDeviceParams{
UserID: claims.UserID,
Name: name,
})
if err != nil {
slog.Error("delete device failed", "err", err, "user", claims.UserID, "name", name)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
return
}
if rows == 0 {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "device not found"})
return
}
s.hub.Kick(claims.UserID, name)
s.hub.PublishPresence(r.Context(), claims.UserID)
w.WriteHeader(http.StatusNoContent)
}