Move users, calendars, and address books from config.yaml into the database
BREAKING CHANGE: the users:/config-based collection setup is gone. All
user, calendar, and address-book data now lives in the SQLite DB
(internal/db) and is managed exclusively via nidusctl or the web UI.
Existing deployments must recreate their users after upgrading:
nidusctl user create <username>
nidusctl calendar create <username> <name>
nidusctl addressbook create <username> <name>
- internal/db: new users, calendars, addressbooks tables with FK cascade
delete; foreign_keys pragma enabled; internal/db/users.go implements
full CRUD + bcrypt auth (CreateUser, VerifyPassword, ListUsers,
CreateCalendar/AddressBook, etc).
- internal/config: removed Users/UserConfig entirely.
- internal/auth: Basic Auth now checks credentials via db.DB instead of
cfg.Users.
- internal/caldav, internal/carddav: ListCalendars/ListAddressBooks and
Create/Delete now backed by the DB.
- internal/web: login uses db.VerifyPassword; new resources.go adds
create/delete handlers for calendars/address books at
/web/resources/{calendar,addressbook}; dashboard gained create forms
and per-card delete buttons (templ + htmx, no hyperscript).
- tools/nidusctl: new user create/delete/list/passwd commands (masked
interactive password prompt via golang.org/x/term) plus create/delete/
list subcommands for calendar/addressbook.
- cmd/server/main.go: pre-creates on-disk collections from the DB at
startup instead of cfg.Users; warns when no users exist yet.
- Updated tests to seed data via the DB; added resources_test.go for the
new web UI handlers.
- README.md and .github/copilot-instructions.md updated to document the
new nidusctl commands and the DB-backed architecture.
Verified end-to-end against a live test server: nidusctl user/calendar/
addressbook create, DAV Basic Auth PROPFIND, web login, dashboard
rendering, and web UI create/delete of resources all confirmed working.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -28,12 +28,48 @@ type SharedWithMeItem struct {
|
||||
templ Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedWithMeItem) {
|
||||
@Layout("Dashboard", username) {
|
||||
<h1 class="text-2xl font-semibold mb-6">Your calendars & address books</h1>
|
||||
<div id="resources" class="space-y-6">
|
||||
for _, r := range resources {
|
||||
@ResourceCardView(r)
|
||||
}
|
||||
|
||||
<div class="flex gap-4 mb-6">
|
||||
<form
|
||||
class="flex items-end gap-2 bg-white rounded-lg border border-gray-200 p-4"
|
||||
hx-post="/web/resources/calendar"
|
||||
hx-target="#resources"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::after-request="if(event.detail.successful) this.reset()"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">New calendar</label>
|
||||
<input name="name" type="text" required pattern="[a-zA-Z0-9_-]{1,64}"
|
||||
placeholder="e.g. work"
|
||||
class="rounded-md border-gray-300 border px-2 py-1.5 text-sm"/>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
<form
|
||||
class="flex items-end gap-2 bg-white rounded-lg border border-gray-200 p-4"
|
||||
hx-post="/web/resources/addressbook"
|
||||
hx-target="#resources"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::after-request="if(event.detail.successful) this.reset()"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">New address book</label>
|
||||
<input name="name" type="text" required pattern="[a-zA-Z0-9_-]{1,64}"
|
||||
placeholder="e.g. contacts"
|
||||
class="rounded-md border-gray-300 border px-2 py-1.5 text-sm"/>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ResourceList(resources)
|
||||
|
||||
if len(sharedWithMe) > 0 {
|
||||
<h2 class="text-xl font-semibold mt-10 mb-4">Shared with you</h2>
|
||||
<ul class="divide-y divide-gray-200 bg-white rounded-lg border border-gray-200">
|
||||
@@ -51,6 +87,22 @@ templ Dashboard(username string, resources []ResourceCard, sharedWithMe []Shared
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceList renders the #resources container. It's re-rendered as a
|
||||
// whole after a create/delete (which changes the set of cards), whereas a
|
||||
// share update only swaps a single ResourceCardView.
|
||||
templ ResourceList(resources []ResourceCard) {
|
||||
<div id="resources" class="space-y-6">
|
||||
for _, r := range resources {
|
||||
@ResourceCardView(r)
|
||||
}
|
||||
if len(resources) == 0 {
|
||||
<p class="text-sm text-gray-400">
|
||||
You don't have any calendars or address books yet — add one above.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ResourceCardView(r ResourceCard) {
|
||||
<div id={ "resource-" + r.Kind + "-" + r.Name } class="bg-white rounded-lg border border-gray-200 p-5">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
@@ -58,6 +110,16 @@ templ ResourceCardView(r ResourceCard) {
|
||||
{ r.Name }
|
||||
<span class="text-xs uppercase tracking-wide text-gray-400 ml-2">{ r.Kind }</span>
|
||||
</h2>
|
||||
<button
|
||||
class="text-red-600 hover:underline text-xs"
|
||||
hx-delete={ resourceEndpoint(r.Kind) }
|
||||
hx-vals={ resourceVals(r.Name) }
|
||||
hx-target="#resources"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm={ "Delete " + r.Kind + " " + r.Name + "? This removes all its data and cannot be undone." }
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-gray-100 mb-4">
|
||||
@@ -121,3 +183,15 @@ func shareEndpoint(kind string) string {
|
||||
func shareVals(resource, sharedWith string) string {
|
||||
return `{"resource": "` + resource + `", "shared_with": "` + sharedWith + `"}`
|
||||
}
|
||||
|
||||
func resourceEndpoint(kind string) string {
|
||||
if kind == "calendar" {
|
||||
return "/web/resources/calendar"
|
||||
}
|
||||
return "/web/resources/addressbook"
|
||||
}
|
||||
|
||||
func resourceVals(name string) string {
|
||||
return `{"name": "` + name + `"}`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user