90 lines
3.1 KiB
Go
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
|
|
}
|