mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
Authentication (previous phase, now landed together with its follow-ups): - Email+password+name accounts behind an AuthProvider interface; the OSS server ships BuiltinAuth only (file-backed auth.json: bcrypt password hashes + SHA-256 token digests, plaintext never stored; server-owned /auth/* pages; managed deployments can swap in another provider). - bdrive login: loopback-callback browser flow (sign-up on the page, the terminal finishes itself) with a device-code fallback for headless machines; long-lived revocable device tokens in settings.json. - Password reset via plain SMTP (stdlib) with a log-link fallback when no SMTP is configured. Move-proof projects: - .bdrive is now a directory; config.json carries a stable mount id. The volume store (~/.bdrive/volumes/<mount-id>/) and registry are keyed by that id — never the folder path — so renames/moves are free. - The daemon re-reads the project config each tick and exits cleanly (propagating nothing) when its folder vanishes; the registry self-heals and the next bdrive command at the new location resumes with zero spurious changes. bdrive init is the front door (mnt/umnt removed; bdrive stop pauses): - Interactive on a TTY (create new / connect existing project from the server's list; whole folder / shared subfolder via the include list), full flag bypass (--name/--project/--shared/--yes), never prompts without a TTY. Runs the login flow first when there is no session. Default server: beardrive.ai (config.DefaultServer). Web history (revert-ready): - Hubs now always require auth; journal ops carry the signed-in account (user/user_name) alongside the git/OS fallback author. - File-backed device registry: per-device name, OS, account, and the public IP the server observed, joined into history at read time. - GET /api/p/<id>/history?path=|prefix= (newest first) and GET /api/p/<id>/blob?sha= stream any exact version — blobs are retained forever, so the next phase's revert is re-putting an old blob. - UI: History button (file versions or project feed), per-folder history shortcut, view/download of any past version. Tests: auth flows (callback, device-code, reset single-use, persistence, gating), history API + device registry, folder-move survival, registry self-heal, ops-carry-account; docs (README/SKILL/CLAUDE) updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
172 lines
3.9 KiB
Go
172 lines
3.9 KiB
Go
// Package config manages beardrive's global state under the beardrive home directory
|
|
// (default ~/.bdrive, overridable with $BDRIVE_HOME): the device identity and the
|
|
// registry of mounted folders.
|
|
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Home returns the beardrive home directory ($BDRIVE_HOME or ~/.bdrive).
|
|
func Home() (string, error) {
|
|
if h := os.Getenv("BDRIVE_HOME"); h != "" {
|
|
return h, nil
|
|
}
|
|
uh, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(uh, ".bdrive"), nil
|
|
}
|
|
|
|
// Device identifies this machine and its operator in journals.
|
|
type Device struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Author string `json:"author"`
|
|
}
|
|
|
|
// LoadDevice loads the device identity, creating one on first use.
|
|
func LoadDevice() (Device, error) {
|
|
home, err := Home()
|
|
if err != nil {
|
|
return Device{}, err
|
|
}
|
|
p := filepath.Join(home, "device.json")
|
|
if data, err := os.ReadFile(p); err == nil {
|
|
var d Device
|
|
if err := json.Unmarshal(data, &d); err == nil && d.ID != "" {
|
|
return d, nil
|
|
}
|
|
}
|
|
d := Device{ID: randID(), Name: hostname(), Author: detectAuthor()}
|
|
if err := os.MkdirAll(home, 0o755); err != nil {
|
|
return Device{}, err
|
|
}
|
|
if err := writeJSON(p, d); err != nil {
|
|
return Device{}, err
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func randID() string {
|
|
b := make([]byte, 6)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "device000000"
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func hostname() string {
|
|
h, _ := os.Hostname()
|
|
h = strings.TrimSuffix(h, ".local")
|
|
if h == "" {
|
|
h = "device"
|
|
}
|
|
return h
|
|
}
|
|
|
|
func detectAuthor() string {
|
|
if out, err := exec.Command("git", "config", "--get", "user.email").Output(); err == nil {
|
|
if s := strings.TrimSpace(string(out)); s != "" {
|
|
return s
|
|
}
|
|
}
|
|
u := os.Getenv("USER")
|
|
if u == "" {
|
|
if cu, err := user.Current(); err == nil {
|
|
u = cu.Username
|
|
}
|
|
}
|
|
if u == "" {
|
|
u = "unknown"
|
|
}
|
|
return u + "@" + hostname()
|
|
}
|
|
|
|
// MountInfo is the registry's view of one mount: where the folder currently
|
|
// lives. The source of truth for identity/settings is the folder's own
|
|
// .bdrive/config.json; the registry only remembers the last-known path (for
|
|
// `bdrive status` and the daemon) and self-heals when the folder moves.
|
|
type MountInfo struct {
|
|
Path string `json:"path"`
|
|
Volume string `json:"volume,omitempty"`
|
|
Remote string `json:"remote,omitempty"`
|
|
}
|
|
|
|
func mountsPath() (string, error) {
|
|
home, err := Home()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, "mounts.json"), nil
|
|
}
|
|
|
|
// LoadMounts returns the mount-ID → mount registry.
|
|
func LoadMounts() (map[string]MountInfo, error) {
|
|
p, err := mountsPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]MountInfo{}
|
|
data, err := os.ReadFile(p)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return out, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func SaveMounts(m map[string]MountInfo) error {
|
|
p, err := mountsPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
return err
|
|
}
|
|
return writeJSON(p, m)
|
|
}
|
|
|
|
// VolumeDir returns the local store dir of a mount, keyed by its stable
|
|
// mount ID (never the folder path — that's what makes renames/moves free).
|
|
func VolumeDir(mountID string) (string, error) {
|
|
home, err := Home()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, "volumes", mountID), nil
|
|
}
|
|
|
|
func writeJSON(path string, v any) error {
|
|
data, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp(filepath.Dir(path), ".bdrive-tmp-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
if _, err := tmp.Write(data); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp.Name(), path)
|
|
}
|