Files
Commilitia-Drop/desktop/platform/devicesession.go
T

90 lines
3.1 KiB
Go

package platform
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
)
// 代铸 (proxy-mint) on the desktop. The device-authorization flow yields a bootstrap
// machine token (scope app:commilitia-drop, no meta) that proves the user's identity but is not a
// cdrop-managed device. This call exchanges it for a managed device session bound to this
// device's stable device_id, so the desktop joins cdrop's unified device list and is
// managed exactly like a browser — the same model, not a separate native-only track.
// DeviceSessionResult is the managed device session minted by 代铸: the device tokens that
// supersede the bootstrap token, plus the verified identity (so a native client, which only
// ever sees nameless machine tokens, shows the real display name instead of the sub UUID).
type DeviceSessionResult struct {
AccessToken string
RefreshToken string
ExpiresIn int
DeviceID string
UserID string
Name string
Avatar string
}
// MintDeviceSession POSTs to {apiBase}/api/auth/device-session with the bootstrap token as
// Bearer; cdrop vouches for the verified subject and has the broker mint (R2-idempotent by
// user+app+meta) a session bound to deviceID. Re-login with the same deviceID rotates the
// one session rather than piling up duplicates.
func MintDeviceSession(ctx context.Context, apiBase, bootstrapToken, deviceID, deviceName, deviceType string) (*DeviceSessionResult, error) {
body, err := json.Marshal(map[string]string{
"device_id": deviceID,
"device_name": deviceName,
"device_type": deviceType,
})
if err != nil {
return nil, fmt.Errorf("device-session: marshal: %w", err)
}
endpoint := strings.TrimRight(apiBase, "/") + "/api/auth/device-session"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("device-session: build request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+bootstrapToken)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("device-session: request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("device-session: status %d", resp.StatusCode)
}
var dr struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
DeviceID string `json:"device_id"`
UserID string `json:"user_id"`
Name string `json:"name"`
Avatar string `json:"avatar"`
}
if err := json.NewDecoder(resp.Body).Decode(&dr); err != nil {
return nil, fmt.Errorf("device-session: decode: %w", err)
}
if dr.AccessToken == "" {
return nil, errors.New("device-session: response missing access token")
}
return &DeviceSessionResult{
AccessToken: dr.AccessToken,
RefreshToken: dr.RefreshToken,
ExpiresIn: dr.ExpiresIn,
DeviceID: dr.DeviceID,
UserID: dr.UserID,
Name: dr.Name,
Avatar: dr.Avatar,
}, nil
}