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)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UserInfo is the display identity the WebView store needs (mirrors the web
|
||||
// app's User shape: id / name / avatar).
|
||||
type UserInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
// LoginResult is the full result Go keeps internally: tokens + identity. The
|
||||
// refresh_token never crosses into the WebView — see SessionView and the
|
||||
// credential policy in session.go.
|
||||
type LoginResult struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
User UserInfo `json:"user"`
|
||||
}
|
||||
|
||||
// SessionView is the refresh-token-free projection handed to the WebView (login
|
||||
// emit, refresh return, boot injection). The refresh_token — the long-lived
|
||||
// secret — is deliberately omitted: it lives only in the Go process and the
|
||||
// on-disk session, never in JS or the injected HTML.
|
||||
type SessionView struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
User UserInfo `json:"user"`
|
||||
}
|
||||
|
||||
// View projects a LoginResult to the refresh-free SessionView.
|
||||
func (r LoginResult) View() SessionView {
|
||||
return SessionView{AccessToken: r.AccessToken, ExpiresIn: r.ExpiresIn, User: r.User}
|
||||
}
|
||||
|
||||
// UserFromToken extracts the display identity from OIDC tokens WITHOUT verifying
|
||||
// the signature. This mirrors the backend's extractUser (internal/httpapi/auth.go)
|
||||
// exactly: prefer the id_token's richer claims, fall back to the access_token;
|
||||
// name priority preferred_username → name → email → sub; avatar avatar → picture.
|
||||
//
|
||||
// Skipping verification is safe for the same reason it is on the backend: the
|
||||
// auth middleware verifies the access_token on every API call via JWKS, so any
|
||||
// tamper surfaces there. These claims only drive local display.
|
||||
func UserFromToken(idToken, accessToken string) (UserInfo, error) {
|
||||
pick := idToken
|
||||
if pick == "" {
|
||||
pick = accessToken
|
||||
}
|
||||
claims, err := decodeJWTClaims(pick)
|
||||
if err != nil {
|
||||
return UserInfo{}, err
|
||||
}
|
||||
|
||||
u := UserInfo{ID: claimString(claims, "sub")}
|
||||
for _, k := range []string{"preferred_username", "name", "email"} {
|
||||
if v := claimString(claims, k); v != "" {
|
||||
u.Name = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if u.Name == "" {
|
||||
u.Name = u.ID
|
||||
}
|
||||
for _, k := range []string{"avatar", "picture"} {
|
||||
if v := claimString(claims, k); v != "" {
|
||||
u.Avatar = v
|
||||
break
|
||||
}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// decodeJWTClaims base64url-decodes the JWT payload segment and unmarshals it.
|
||||
// No signature check (see UserFromToken).
|
||||
func decodeJWTClaims(token string) (map[string]any, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) < 2 {
|
||||
return nil, errors.New("oauth: token is not a JWT")
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth: decode jwt payload: %w", err)
|
||||
}
|
||||
var claims map[string]any
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return nil, fmt.Errorf("oauth: parse jwt claims: %w", err)
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func claimString(m map[string]any, key string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user