Add Android client, packaging, Makefile entrypoint and handover docs
Why: the set has to be installable and publishable, not just buildable, and the next person needs the reasoning, not only the code. What: - android/: Kotlin client with no third-party dependencies. A DocumentsProvider publishes the mirrored databases to every file picker, so KeePassDX opens them as ordinary documents; closing a file it wrote triggers a sync. A foreground service holds the event stream open. Mirror.decide transcribes the Go merge table, so both platforms resolve a conflict identically. - Makefile as the single entrypoint, with file targets so only what changed is rebuilt: host and cross builds, both .deb packages, the macOS DMG with its /Applications symlink, the Windows zip, the APK under its F-Droid publishing name, and manual-only android-publish. - versions.mk holds one version per platform plus the Android versionCode; `make bump` and `make changelog` move them. Debian changelog is tracked. - systemd units (hardened system unit for the server, user unit for the agent), macOS bundle and launch agent, Windows Task Scheduler job. - docs/HANDOVER.md on architecture and every decision that shaped it, docs/PROTOCOL.md for the API, README for the quick start. Proved: make check and make test-race both pass; built both .deb packages and inspected their contents, built the Windows zip, cross-compiled all five desktop targets, and exercised make bump. The Android module is written but not compiled: no JDK or Android SDK in this environment, which HANDOVER.md states plainly.
This commit is contained in:
+24
-24
@@ -113,10 +113,10 @@ func (s *Server) routes() {
|
||||
|
||||
// Authentication.
|
||||
m.HandleFunc("POST /api/v1/auth/login", s.handleLogin)
|
||||
m.HandleFunc("POST /api/v1/auth/logout", s.authed(store.PermNone, s.handleLogout))
|
||||
m.HandleFunc("GET /api/v1/auth/whoami", s.authed(store.PermNone, s.handleWhoami))
|
||||
m.HandleFunc("POST /api/v1/auth/totp/enroll", s.authed(store.PermNone, s.handleTOTPEnroll))
|
||||
m.HandleFunc("POST /api/v1/auth/totp/confirm", s.authed(store.PermNone, s.handleTOTPConfirm))
|
||||
m.HandleFunc("POST /api/v1/auth/logout", s.authed(s.handleLogout))
|
||||
m.HandleFunc("GET /api/v1/auth/whoami", s.authed(s.handleWhoami))
|
||||
m.HandleFunc("POST /api/v1/auth/totp/enroll", s.authed(s.handleTOTPEnroll))
|
||||
m.HandleFunc("POST /api/v1/auth/totp/confirm", s.authed(s.handleTOTPConfirm))
|
||||
m.HandleFunc("POST /api/v1/auth/device/start", s.handleDeviceStart)
|
||||
m.HandleFunc("POST /api/v1/auth/device/poll", s.handleDevicePoll)
|
||||
|
||||
@@ -129,25 +129,25 @@ func (s *Server) routes() {
|
||||
m.HandleFunc("GET /oidc/callback", s.handleOIDCCallback)
|
||||
|
||||
// Synchronization.
|
||||
m.HandleFunc("GET /api/v1/databases", s.authed(store.PermNone, s.handleListDatabases))
|
||||
m.HandleFunc("GET /api/v1/databases/{id}/blob", s.authed(store.PermNone, s.handleGetBlob))
|
||||
m.HandleFunc("PUT /api/v1/databases/{id}/blob", s.authed(store.PermNone, s.handlePutBlob))
|
||||
m.HandleFunc("GET /api/v1/events", s.authed(store.PermNone, s.handleEvents))
|
||||
m.HandleFunc("GET /api/v1/databases", s.authed(s.handleListDatabases))
|
||||
m.HandleFunc("GET /api/v1/databases/{id}/blob", s.authed(s.handleGetBlob))
|
||||
m.HandleFunc("PUT /api/v1/databases/{id}/blob", s.authed(s.handlePutBlob))
|
||||
m.HandleFunc("GET /api/v1/events", s.authed(s.handleEvents))
|
||||
|
||||
// Administration.
|
||||
m.HandleFunc("POST /api/v1/databases", s.authed(store.PermNone, s.handleCreateDatabase))
|
||||
m.HandleFunc("PATCH /api/v1/databases/{id}", s.authed(store.PermNone, s.handlePatchDatabase))
|
||||
m.HandleFunc("DELETE /api/v1/databases/{id}", s.authed(store.PermNone, s.handleDeleteDatabase))
|
||||
m.HandleFunc("GET /api/v1/groups", s.authed(store.PermNone, s.handleListGroups))
|
||||
m.HandleFunc("POST /api/v1/groups", s.authed(store.PermNone, s.handleCreateGroup))
|
||||
m.HandleFunc("DELETE /api/v1/groups/{id}", s.authed(store.PermNone, s.handleDeleteGroup))
|
||||
m.HandleFunc("POST /api/v1/groups/{id}/members", s.authed(store.PermNone, s.handleSetMember))
|
||||
m.HandleFunc("DELETE /api/v1/groups/{id}/members/{user}", s.authed(store.PermNone, s.handleRemoveMember))
|
||||
m.HandleFunc("POST /api/v1/grants", s.authed(store.PermNone, s.handleSetGrant))
|
||||
m.HandleFunc("DELETE /api/v1/grants", s.authed(store.PermNone, s.handleDeleteGrant))
|
||||
m.HandleFunc("POST /api/v1/invites", s.authed(store.PermNone, s.handleCreateInvite))
|
||||
m.HandleFunc("GET /api/v1/invites", s.authed(store.PermNone, s.handleListInvites))
|
||||
m.HandleFunc("GET /api/v1/users", s.authed(store.PermNone, s.handleListUsers))
|
||||
m.HandleFunc("POST /api/v1/databases", s.authed(s.handleCreateDatabase))
|
||||
m.HandleFunc("PATCH /api/v1/databases/{id}", s.authed(s.handlePatchDatabase))
|
||||
m.HandleFunc("DELETE /api/v1/databases/{id}", s.authed(s.handleDeleteDatabase))
|
||||
m.HandleFunc("GET /api/v1/groups", s.authed(s.handleListGroups))
|
||||
m.HandleFunc("POST /api/v1/groups", s.authed(s.handleCreateGroup))
|
||||
m.HandleFunc("DELETE /api/v1/groups/{id}", s.authed(s.handleDeleteGroup))
|
||||
m.HandleFunc("POST /api/v1/groups/{id}/members", s.authed(s.handleSetMember))
|
||||
m.HandleFunc("DELETE /api/v1/groups/{id}/members/{user}", s.authed(s.handleRemoveMember))
|
||||
m.HandleFunc("POST /api/v1/grants", s.authed(s.handleSetGrant))
|
||||
m.HandleFunc("DELETE /api/v1/grants", s.authed(s.handleDeleteGrant))
|
||||
m.HandleFunc("POST /api/v1/invites", s.authed(s.handleCreateInvite))
|
||||
m.HandleFunc("GET /api/v1/invites", s.authed(s.handleListInvites))
|
||||
m.HandleFunc("GET /api/v1/users", s.authed(s.handleListUsers))
|
||||
}
|
||||
|
||||
// ---- request plumbing ----
|
||||
@@ -185,9 +185,9 @@ func readJSON(w http.ResponseWriter, r *http.Request, v any) bool {
|
||||
type handler func(http.ResponseWriter, *http.Request, *store.User)
|
||||
|
||||
// authed resolves the bearer token to a live, enabled user before calling fn.
|
||||
// Per-object permissions are checked inside each handler, where the object is
|
||||
// known; min is accepted for documentation of the minimum account level.
|
||||
func (s *Server) authed(_ store.Perm, fn handler) http.HandlerFunc {
|
||||
// Per-object permission is then checked inside the handler, where the object
|
||||
// being acted on is known.
|
||||
func (s *Server) authed(fn handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
u, err := s.userFromRequest(r)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user