Compare commits
6
Commits
d3780e9e32
...
e8c4dadfea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8c4dadfea | ||
|
|
2e43e32a9f | ||
|
|
9b9cbe2f6f | ||
|
|
0ee0874458 | ||
|
|
b364bee265 | ||
|
|
dd77a61b18 |
+9
-6
@@ -18,13 +18,16 @@ WORKDIR /app
|
|||||||
COPY --from=builder /bin/davserver /usr/local/bin/davserver
|
COPY --from=builder /bin/davserver /usr/local/bin/davserver
|
||||||
COPY --from=builder /bin/nidusctl /usr/local/bin/nidusctl
|
COPY --from=builder /bin/nidusctl /usr/local/bin/nidusctl
|
||||||
|
|
||||||
# Default data and config locations
|
# Default data location
|
||||||
VOLUME ["/app/data"]
|
VOLUME ["/app/data"]
|
||||||
# config.yaml itself is git-ignored (it holds real secrets), so the image
|
|
||||||
# ships the example config as a working default; mount your own
|
# environment variables (override as needed)
|
||||||
# config.yaml over /app/config.yaml (see docker-compose.yaml) to override it.
|
ENV NIDUS_DATA_DIR=/app/data
|
||||||
COPY config.example.yaml /app/config.yaml
|
ENV NIDUS_HOST=0.0.0.0
|
||||||
|
ENV NIDUS_PORT=8080
|
||||||
|
ENV NIDUS_LOG_LEVEL=warn
|
||||||
|
ENV NIDUS_LOG_FORMAT=text
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
ENTRYPOINT ["davserver", "-config", "/app/config.yaml"]
|
ENTRYPOINT ["davserver"]
|
||||||
|
|||||||
-150
@@ -1,150 +0,0 @@
|
|||||||
# Data Directory Migration Guide
|
|
||||||
|
|
||||||
This migration unifies the user data directory structure from a fragmented layout to a consistent nested format.
|
|
||||||
|
|
||||||
## Before (Old Structure)
|
|
||||||
|
|
||||||
```
|
|
||||||
data/
|
|
||||||
├── files/
|
|
||||||
│ └── alice/ # WebDAV files
|
|
||||||
│ └── documents/
|
|
||||||
│ └── file.txt
|
|
||||||
├── alice/
|
|
||||||
│ ├── cal-personal/ # CalDAV calendar
|
|
||||||
│ │ └── event1.ics
|
|
||||||
│ └── card-contacts/ # CardDAV address book
|
|
||||||
│ └── contact1.vcf
|
|
||||||
└── bob/
|
|
||||||
├── cal-work/
|
|
||||||
│ └── meeting.ics
|
|
||||||
└── card-addressbook/
|
|
||||||
└── address.vcf
|
|
||||||
```
|
|
||||||
|
|
||||||
## After (New Unified Structure)
|
|
||||||
|
|
||||||
```
|
|
||||||
data/
|
|
||||||
├── alice/
|
|
||||||
│ ├── files/ # WebDAV files
|
|
||||||
│ │ └── documents/
|
|
||||||
│ │ └── file.txt
|
|
||||||
│ ├── calendars/ # CalDAV calendars
|
|
||||||
│ │ └── personal/
|
|
||||||
│ │ └── event1.ics
|
|
||||||
│ └── addressbooks/ # CardDAV address books
|
|
||||||
│ └── contacts/
|
|
||||||
│ └── contact1.vcf
|
|
||||||
└── bob/
|
|
||||||
├── files/
|
|
||||||
├── calendars/
|
|
||||||
│ └── work/
|
|
||||||
└── addressbooks/
|
|
||||||
└── addressbook/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Details
|
|
||||||
|
|
||||||
### What Changed
|
|
||||||
|
|
||||||
1. **WebDAV files**: `data/files/<username>/` → `data/<username>/files/`
|
|
||||||
2. **CalDAV calendars**: `data/<username>/cal-<name>/` → `data/<username>/calendars/<name>/`
|
|
||||||
3. **CardDAV address books**: `data/<username>/card-<name>/` → `data/<username>/addressbooks/<name>/`
|
|
||||||
|
|
||||||
### Migration Tool
|
|
||||||
|
|
||||||
A migration tool is provided that automatically restructures the data directory. It is **idempotent** and can be run multiple times safely.
|
|
||||||
|
|
||||||
#### Using nidusctl (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./tools/nidusctl -config config.yaml migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Using standalone migrate tool
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./tools/migrate -config config.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Verbose output
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./tools/nidusctl -config config.yaml migrate --verbose
|
|
||||||
```
|
|
||||||
|
|
||||||
### What the Migration Does
|
|
||||||
|
|
||||||
1. For each user directory in the data directory:
|
|
||||||
- Creates `<username>/` if it doesn't exist
|
|
||||||
- Moves `files/<username>/` → `<username>/files/` (if exists)
|
|
||||||
- Moves `cal-<name>/` → `<username>/calendars/<name>/`
|
|
||||||
- Moves `card-<name>/` → `<username>/addressbooks/<name>/`
|
|
||||||
|
|
||||||
2. The old `files/` directory is left in place (can be manually removed after verification)
|
|
||||||
|
|
||||||
### Backward Compatibility
|
|
||||||
|
|
||||||
The migration is **fully backward compatible**:
|
|
||||||
- The migration tool handles both old and new structures
|
|
||||||
- If files are already in the new location, they are not moved
|
|
||||||
- Running migration multiple times is safe (idempotent)
|
|
||||||
|
|
||||||
### Server Integration
|
|
||||||
|
|
||||||
The server automatically runs migration on startup (if enabled in config). No manual migration is required for new installations.
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
After migration, verify:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check data structure
|
|
||||||
ls -la data/
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
# Start server to verify WebDAV, CalDAV, CardDAV work correctly
|
|
||||||
go run ./cmd/server -config config.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Verification
|
|
||||||
|
|
||||||
After migration, you should see:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ ls -la data/alice/
|
|
||||||
calendars/
|
|
||||||
addressbooks/
|
|
||||||
files/
|
|
||||||
```
|
|
||||||
|
|
||||||
Each calendar should be in `data/<user>/calendars/<name>/` format, not `cal-<name>/`.
|
|
||||||
|
|
||||||
### Rollback (if needed)
|
|
||||||
|
|
||||||
If you need to rollback:
|
|
||||||
1. Stop the server
|
|
||||||
2. Restore the data directory from backup
|
|
||||||
3. Re-run the migration after fixing any issues
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**Q: Migration reports "directory already exists" warnings**
|
|
||||||
A: These are normal if files were already migrated. The migration is idempotent.
|
|
||||||
|
|
||||||
**Q: Old `files/` directory still exists**
|
|
||||||
A: This is expected. You can manually remove it after verifying migration success.
|
|
||||||
|
|
||||||
**Q: Some calendars/address books not visible after migration**
|
|
||||||
A: Check the migration logs and verify directory structure. Run `find data/ -type d -name "cal-*"` to find unmigrated calendars.
|
|
||||||
|
|
||||||
### Support
|
|
||||||
|
|
||||||
If you encounter issues:
|
|
||||||
1. Check logs for detailed error messages
|
|
||||||
2. Run migration with `--verbose` flag
|
|
||||||
3. Ensure no server processes are running during migration
|
|
||||||
4. Make backup before migrating in production
|
|
||||||
@@ -7,7 +7,7 @@ build:
|
|||||||
|
|
||||||
## run: run the server locally
|
## run: run the server locally
|
||||||
run: build
|
run: build
|
||||||
./bin/davserver -config config.yaml
|
./bin/davserver
|
||||||
|
|
||||||
## test: run all tests
|
## test: run all tests
|
||||||
test:
|
test:
|
||||||
@@ -37,7 +37,7 @@ hash-password:
|
|||||||
## nidusctl: build and run the sharing-grant admin CLI
|
## nidusctl: build and run the sharing-grant admin CLI
|
||||||
## Usage: make nidusctl ARGS="calendar share alice work bob write"
|
## Usage: make nidusctl ARGS="calendar share alice work bob write"
|
||||||
nidusctl: build
|
nidusctl: build
|
||||||
./bin/nidusctl -config config.yaml $(ARGS)
|
./bin/nidusctl $(ARGS)
|
||||||
|
|
||||||
## templ-generate: regenerate *_templ.go files from internal/web/templates/*.templ
|
## templ-generate: regenerate *_templ.go files from internal/web/templates/*.templ
|
||||||
templ-generate:
|
templ-generate:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# DAV Server
|
# nidus
|
||||||
|
|
||||||
A self-hosted **CalDAV**, **CardDAV**, and **WebDAV** server written in Go.
|
A self-hosted **CalDAV**, **CardDAV**, and **WebDAV** server written in Go.
|
||||||
|
|
||||||
@@ -23,56 +23,61 @@ and files, all kept under your own roof instead of a third-party cloud.
|
|||||||
- **Web UI** — a mobile-friendly app at `/web/` for managing calendars,
|
- **Web UI** — a mobile-friendly app at `/web/` for managing calendars,
|
||||||
contacts, files, and account settings (see [Web UI](#web-ui) below),
|
contacts, files, and account settings (see [Web UI](#web-ui) below),
|
||||||
built with templ + Tailwind + htmx
|
built with templ + Tailwind + htmx
|
||||||
|
- **ICSSubscriptions** — add remote ICS/webcal calendars
|
||||||
|
- **Birthdays calendar** — auto-computed from contacts' BDAY fields
|
||||||
- Auto-discovery via `/.well-known/caldav` and `/.well-known/carddav`
|
- Auto-discovery via `/.well-known/caldav` and `/.well-known/carddav`
|
||||||
- Optional **TLS** (or use a reverse proxy)
|
|
||||||
- Structured logging (text or JSON)
|
- Structured logging (text or JSON)
|
||||||
- Graceful shutdown
|
- Graceful shutdown
|
||||||
- Docker & Docker Compose support
|
- Docker & Docker Compose support
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
### 1. Install dependencies
|
### 1. Run the server
|
||||||
|
|
||||||
```bash
|
|
||||||
go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Create your `config.yaml`
|
|
||||||
|
|
||||||
Copy the example config and edit it — `config.yaml` is git-ignored so your
|
|
||||||
real settings never get committed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp config.example.yaml config.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
Users, calendars, and address books are **no longer configured in
|
|
||||||
`config.yaml`** — they live in the SQLite database and are managed with
|
|
||||||
`nidusctl` (see below).
|
|
||||||
|
|
||||||
### 3. Run the server
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make run
|
make run
|
||||||
# or
|
# or
|
||||||
go run ./cmd/server -config config.yaml
|
go run ./cmd/server
|
||||||
```
|
```
|
||||||
|
|
||||||
The server starts at **http://localhost:8080**.
|
The server starts at **http://localhost:8080**.
|
||||||
|
|
||||||
### 4. Create a user and their resources
|
### 2. Configure the server via environment variables
|
||||||
|
|
||||||
|
The server is configured via environment variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run ./tools/nidusctl -config config.yaml user create alice \
|
# Required: Set the data directory
|
||||||
|
export NIDUS_DATA_DIR="./data"
|
||||||
|
|
||||||
|
# Optional: Set port, host, and base URL
|
||||||
|
export NIDUS_PORT="8080"
|
||||||
|
export NIDUS_HOST="0.0.0.0"
|
||||||
|
export NIDUS_BASE_URL="https://dav.example.com"
|
||||||
|
|
||||||
|
# Optional: Set auth realm
|
||||||
|
export NIDUS_AUTH_REALM="My DAV Server"
|
||||||
|
|
||||||
|
# Optional: Set logging
|
||||||
|
export NIDUS_LOG_LEVEL="info"
|
||||||
|
export NIDUS_LOG_FORMAT="text"
|
||||||
|
```
|
||||||
|
|
||||||
|
This server does **not** handle TLS — use a reverse proxy (e.g. Nginx, Caddy,
|
||||||
|
Traefik) to terminate TLS and forward requests to the server.
|
||||||
|
|
||||||
|
### 3. Create a user and their resources
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./tools/nidusctl user create alice \
|
||||||
--display-name "Alice Smith" --email alice@example.com
|
--display-name "Alice Smith" --email alice@example.com
|
||||||
# (prompts for a password; use --password to skip the prompt, e.g. in scripts)
|
# (prompts for a password; use --password to skip the prompt, e.g. in scripts)
|
||||||
|
|
||||||
go run ./tools/nidusctl -config config.yaml calendar create alice personal
|
go run ./tools/nidusctl calendar create alice personal
|
||||||
go run ./tools/nidusctl -config config.yaml addressbook create alice contacts
|
go run ./tools/nidusctl addressbook create alice contacts
|
||||||
```
|
```
|
||||||
|
|
||||||
Users can also be created/removed via the web UI (`/web/`) once logged in
|
Or use the web UI (`/web/`) once logged in — see **Web UI** below.
|
||||||
as an existing user — see **Web UI** below.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -83,27 +88,27 @@ as an existing user — see **Web UI** below.
|
|||||||
docker compose up --build
|
docker compose up --build
|
||||||
|
|
||||||
# Or build manually
|
# Or build manually
|
||||||
docker build -t davserver .
|
docker build -t nidus .
|
||||||
docker run -p 8080:8080 \
|
docker run -p 8080:8080 \
|
||||||
-v ./config.yaml:/app/config.yaml:ro \
|
-v nidus-data:/app/data \
|
||||||
-v dav-data:/app/data \
|
-e NIDUS_DATA_DIR=/app/data \
|
||||||
davserver
|
nidus
|
||||||
```
|
```
|
||||||
|
|
||||||
The image also ships `nidusctl`, so once the container is running you can
|
The image also ships `nidusctl`, so once the container is running you can
|
||||||
create your first user (and their calendars/address books) with
|
create your first user (and their calendars/address books) with
|
||||||
`docker compose exec` — no need to install Go locally:
|
`docker compose exec`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose exec davserver nidusctl -config /app/config.yaml user create alice \
|
docker compose exec nidus nidusctl user create alice \
|
||||||
--display-name "Alice Smith" --email alice@example.com
|
--display-name "Alice Smith" --email alice@example.com
|
||||||
# (prompts for a password; use --password to skip the prompt, e.g. in scripts)
|
# (prompts for a password; use --password to skip the prompt)
|
||||||
|
|
||||||
docker compose exec davserver nidusctl -config /app/config.yaml calendar create alice personal
|
docker compose exec nidus nidusctl calendar create alice personal
|
||||||
docker compose exec davserver nidusctl -config /app/config.yaml addressbook create alice contacts
|
docker compose exec nidus nidusctl addressbook create alice contacts
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pre-built images
|
### Using pre-built images
|
||||||
|
|
||||||
Pushing a version tag (e.g. `v1.2.3`) or publishing a release triggers
|
Pushing a version tag (e.g. `v1.2.3`) or publishing a release triggers
|
||||||
[`.github/workflows/docker-release.yml`](.github/workflows/docker-release.yml),
|
[`.github/workflows/docker-release.yml`](.github/workflows/docker-release.yml),
|
||||||
@@ -118,25 +123,29 @@ building from a checkout — just fetch `config.example.yaml`, copy it to
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
davserver:
|
nidus:
|
||||||
image: git.arnef.de/arnef/nidus:latest
|
image: git.arnef.de/arnef/nidus:latest
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yaml:/app/config.yaml:ro
|
- nidus-data:/app/data
|
||||||
- dav-data:/app/data
|
environment:
|
||||||
|
- NIDUS_DATA_DIR=/app/data
|
||||||
|
# Optional: other environment variables
|
||||||
|
# - NIDUS_PORT=8080
|
||||||
|
# - NIDUS_HOST=0.0.0.0
|
||||||
|
# - NIDUS_BASE_URL=https://dav.example.com
|
||||||
|
# - NIDUS_AUTH_REALM="My DAV Server"
|
||||||
|
# - NIDUS_LOG_LEVEL=info
|
||||||
|
# - NIDUS_LOG_FORMAT=text
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dav-data:
|
nidus-data:
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Note: This server does **not** handle TLS. Use a reverse proxy (e.g. Nginx,
|
||||||
|
Caddy, Traefik) to terminate TLS and forward requests to the server.
|
||||||
|
|
||||||
## API endpoints
|
## API endpoints
|
||||||
|
|
||||||
@@ -208,10 +217,10 @@ mobile browsers:
|
|||||||
`nidus.db` (`web_sessions` table), independent of DAV Basic Auth. Includes
|
`nidus.db` (`web_sessions` table), independent of DAV Basic Auth. Includes
|
||||||
a "Show/Hide" password toggle to rule out typos before submitting.
|
a "Show/Hide" password toggle to rule out typos before submitting.
|
||||||
- **Dashboard** (`/web/`) — create/delete your own calendars, address
|
- **Dashboard** (`/web/`) — create/delete your own calendars, address
|
||||||
books, and ICS/webcal subscriptions; see who your resources are shared
|
books, ICS/webcal subscriptions, and the Birthdays calendar; see who your
|
||||||
with and what others have shared with you; manage sharing grants
|
resources are shared with and what others have shared with you; manage
|
||||||
directly (same effect as `nidusctl`) — updates happen in place via
|
sharing grants directly (same effect as `nidusctl`) — updates happen in
|
||||||
[htmx](https://htmx.org/) without a full page reload.
|
place via [htmx](https://htmx.org/) without a full page reload.
|
||||||
- **Files** (`/web/files/`) — a browser for the same storage the WebDAV
|
- **Files** (`/web/files/`) — a browser for the same storage the WebDAV
|
||||||
endpoint (`/files/`) serves: navigate folders, create new folders,
|
endpoint (`/files/`) serves: navigate folders, create new folders,
|
||||||
upload files/folders (including via drag & drop), download, and delete
|
upload files/folders (including via drag & drop), download, and delete
|
||||||
@@ -223,8 +232,9 @@ mobile browsers:
|
|||||||
delete contacts (name, organization, birthday, phone numbers, emails,
|
delete contacts (name, organization, birthday, phone numbers, emails,
|
||||||
addresses, photo), and import/export vCards (`.vcf`).
|
addresses, photo), and import/export vCards (`.vcf`).
|
||||||
- **Calendar** (`/web/calendar`) — month and week views across all your
|
- **Calendar** (`/web/calendar`) — month and week views across all your
|
||||||
own and shared calendars, create/edit/delete events, per-calendar
|
own and shared calendars (including ICS/webcal subscriptions and the
|
||||||
colors, and import/export `.ics` files.
|
Birthdays calendar), create/edit/delete events, per-calendar colors,
|
||||||
|
and import/export `.ics` files.
|
||||||
- **Account** (`/web/account`) — update your display name/email and
|
- **Account** (`/web/account`) — update your display name/email and
|
||||||
change your password.
|
change your password.
|
||||||
- **Logout** (`/web/logout`).
|
- **Logout** (`/web/logout`).
|
||||||
@@ -248,35 +258,34 @@ make web-assets # regenerate templ code + rebuild web/static/app.css and web/st
|
|||||||
|
|
||||||
## Configuration reference
|
## Configuration reference
|
||||||
|
|
||||||
```yaml
|
Configuration is done via environment variables:
|
||||||
server:
|
|
||||||
host: "0.0.0.0"
|
|
||||||
port: 8080
|
|
||||||
base_url: "https://dav.example.com" # used in DAV responses
|
|
||||||
|
|
||||||
auth:
|
| Variable | Default | Description |
|
||||||
realm: "My DAV Server"
|
|----------|---------|-------------|
|
||||||
|
| `NIDUS_HOST` | `0.0.0.0` | Server listen host |
|
||||||
|
| `NIDUS_PORT` | `8080` | Server listen port |
|
||||||
|
| `NIDUS_BASE_URL` | (auto) | Public URL for DAV responses (e.g. https://dav.example.com) |
|
||||||
|
| `NIDUS_AUTH_REALM` | `DAV Server` | HTTP Basic Auth realm |
|
||||||
|
| `NIDUS_DATA_DIR` | `./data` | Data directory for all user data |
|
||||||
|
| `NIDUS_LOG_LEVEL` | `info` | Log level: debug, info, warn, error |
|
||||||
|
| `NIDUS_LOG_FORMAT` | `text` | Log format: text, json |
|
||||||
|
|
||||||
storage:
|
Example:
|
||||||
data_dir: "./data" # all user data lives here
|
|
||||||
|
|
||||||
logging:
|
```bash
|
||||||
level: "info" # debug | info | warn | error
|
export NIDUS_DATA_DIR="./data"
|
||||||
format: "text" # text | json
|
export NIDUS_PORT="8080"
|
||||||
|
export NIDUS_BASE_URL="https://dav.example.com"
|
||||||
tls:
|
export NIDUS_LOG_LEVEL="info"
|
||||||
enabled: false
|
|
||||||
cert_file: ""
|
|
||||||
key_file: ""
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Users, calendars, and address books are managed via `nidusctl`, not
|
This server does **not** handle TLS — use a reverse proxy (e.g. Nginx, Caddy,
|
||||||
`config.yaml` — see **Managing users** below.
|
Traefik) to terminate TLS and forward requests to the server.
|
||||||
|
|
||||||
## Managing users
|
## Managing users
|
||||||
|
|
||||||
All user/calendar/address-book management is done with `nidusctl` (or the
|
All user/calendar/address-book management is done with `nidusctl` or the
|
||||||
web UI). Nothing is stored in `config.yaml` anymore.
|
web UI (`/web/`). Nothing is stored in `config.yaml` anymore.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Users
|
# Users
|
||||||
@@ -303,9 +312,10 @@ nidusctl addressbook unshare <owner> <book> <user>
|
|||||||
nidusctl addressbook shares <owner> <book>
|
nidusctl addressbook shares <owner> <book>
|
||||||
```
|
```
|
||||||
|
|
||||||
Passwords are prompted for interactively (masked, double-entry) when
|
Password are prompted for interactively (masked, double-entry) when
|
||||||
`--password` is omitted. The web UI (`/web/`) also lets a logged-in user
|
`--password` is omitted. The web UI (`/web/`) also lets a logged-in user
|
||||||
create/delete their own calendars and address books from the dashboard.
|
create/delete their own calendars, address books, and ICS/webcal subscriptions
|
||||||
|
from the dashboard.
|
||||||
|
|
||||||
> **Upgrading from an older version?** The `users:` section in
|
> **Upgrading from an older version?** The `users:` section in
|
||||||
> `config.yaml` is no longer read. Recreate your users with
|
> `config.yaml` is no longer read. Recreate your users with
|
||||||
@@ -317,22 +327,22 @@ create/delete their own calendars and address books from the dashboard.
|
|||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
```
|
```
|
||||||
caldav-server/
|
nidus/
|
||||||
├── cmd/server/ # main entrypoint
|
├── cmd/server/ # main entrypoint
|
||||||
├── internal/
|
├── internal/
|
||||||
│ ├── auth/ # HTTP Basic Auth middleware (DAV endpoints)
|
│ ├── auth/ # HTTP Basic Auth middleware (DAV endpoints)
|
||||||
│ ├── caldav/ # CalDAV backend
|
│ ├── caldav/ # CalDAV backend
|
||||||
│ ├── carddav/ # CardDAV backend
|
│ ├── carddav/ # CardDAV backend
|
||||||
│ ├── config/ # YAML config loader
|
│ ├── config/ # YAML config loader
|
||||||
│ ├── db/ # SQLite store (shares, web UI sessions)
|
│ ├── db/ # SQLite store (users, calendars, shares, sessions)
|
||||||
│ ├── store/ # filesystem storage layer
|
│ ├── store/ # filesystem storage layer
|
||||||
│ ├── web/ # web UI (cookie sessions, dashboard, share mgmt)
|
|
||||||
│ │ └── templates/ # templ templates (+ generated *_templ.go)
|
|
||||||
│ └── webdav/ # WebDAV file handler
|
│ └── webdav/ # WebDAV file handler
|
||||||
├── tools/hashpwd/ # bcrypt password hasher CLI
|
├── internal/web/ # web UI (templ, dashboard, share mgmt, sessions)
|
||||||
├── tools/nidusctl/ # sharing-grant admin CLI
|
│ └── templates/ # templ templates (+ generated *_templ.go)
|
||||||
|
├── cmd/nidusctl/ # admin CLI (users, calendars, address books, sharing)
|
||||||
├── web/ # front-end assets: Tailwind input/config, static/
|
├── web/ # front-end assets: Tailwind input/config, static/
|
||||||
│ └── static/ # compiled app.css + htmx.min.js (embedded into the binary)
|
│ └── static/ # compiled app.css + htmx.min.js (embedded into the binary)
|
||||||
|
├── tools/migrate/ # data directory migration tool
|
||||||
├── config.example.yaml # sample configuration (copy to config.yaml)
|
├── config.example.yaml # sample configuration (copy to config.yaml)
|
||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
├── docker-compose.yaml
|
├── docker-compose.yaml
|
||||||
|
|||||||
+3
-13
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
@@ -25,12 +24,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var cfgPath string
|
|
||||||
flag.StringVar(&cfgPath, "config", "config.yaml", "path to configuration file")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
// ---- Configuration ----
|
// ---- Configuration ----
|
||||||
cfg, err := config.Load(cfgPath)
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error loading config: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error loading config: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -41,8 +36,7 @@ func main() {
|
|||||||
logger.Info("starting DAV server",
|
logger.Info("starting DAV server",
|
||||||
"host", cfg.Server.Host,
|
"host", cfg.Server.Host,
|
||||||
"port", cfg.Server.Port,
|
"port", cfg.Server.Port,
|
||||||
"base_url", cfg.Server.BaseURL,
|
"base_url", cfg.Server.BaseURL)
|
||||||
"tls", cfg.TLS.Enabled)
|
|
||||||
|
|
||||||
// ---- Storage ----
|
// ---- Storage ----
|
||||||
st, err := store.NewStore(cfg.Storage.DataDir)
|
st, err := store.NewStore(cfg.Storage.DataDir)
|
||||||
@@ -143,11 +137,7 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
logger.Info("server ready", "addr", addr)
|
logger.Info("server ready", "addr", addr)
|
||||||
if cfg.TLS.Enabled {
|
err = srv.ListenAndServe()
|
||||||
err = srv.ListenAndServeTLS(cfg.TLS.CertFile, cfg.TLS.KeyFile)
|
|
||||||
} else {
|
|
||||||
err = srv.ListenAndServe()
|
|
||||||
}
|
|
||||||
if err != nil && err != http.ErrServerClosed {
|
if err != nil && err != http.ErrServerClosed {
|
||||||
logger.Error("server error", "error", err)
|
logger.Error("server error", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
+28
-29
@@ -1,31 +1,30 @@
|
|||||||
server:
|
# nidus configuration via environment variables
|
||||||
host: "0.0.0.0"
|
|
||||||
port: 8080
|
|
||||||
# Set this to your public-facing URL so discovery responses are correct.
|
|
||||||
# base_url: "https://dav.example.com"
|
|
||||||
|
|
||||||
auth:
|
|
||||||
realm: "My DAV Server"
|
|
||||||
|
|
||||||
storage:
|
|
||||||
data_dir: "./data"
|
|
||||||
|
|
||||||
logging:
|
|
||||||
level: "debug" # debug | info | warn | error
|
|
||||||
format: "text" # text | json
|
|
||||||
|
|
||||||
tls:
|
|
||||||
enabled: false
|
|
||||||
# cert_file: "/etc/ssl/certs/dav.crt"
|
|
||||||
# key_file: "/etc/ssl/private/dav.key"
|
|
||||||
|
|
||||||
# Users, calendars, and address books are no longer configured here — they
|
|
||||||
# are stored in the database (<data_dir>/nidus.db) and managed with
|
|
||||||
# nidusctl or the web UI (/web/):
|
|
||||||
#
|
#
|
||||||
# nidusctl user create alice --password mysecretpassword --display-name "Alice Smith" --email alice@example.com
|
# Environment variables override any defaults shown below.
|
||||||
# nidusctl calendar create alice personal
|
# Set them before starting the server.
|
||||||
# nidusctl calendar create alice work
|
|
||||||
# nidusctl addressbook create alice contacts
|
|
||||||
#
|
#
|
||||||
# Run `nidusctl help` for the full command list.
|
# Server configuration:
|
||||||
|
# NIDUS_HOST - listen host (default: 0.0.0.0)
|
||||||
|
# NIDUS_PORT - listen port (default: 8080)
|
||||||
|
# NIDUS_BASE_URL - public URL for DAV responses (default: auto-generated from host:port)
|
||||||
|
#
|
||||||
|
# Authentication:
|
||||||
|
# NIDUS_AUTH_REALM - HTTP Basic Auth realm (default: "DAV Server")
|
||||||
|
#
|
||||||
|
# Storage:
|
||||||
|
# NIDUS_DATA_DIR - data directory (default: ./data)
|
||||||
|
#
|
||||||
|
# Logging:
|
||||||
|
# NIDUS_LOG_LEVEL - log level: debug, info, warn, error (default: info)
|
||||||
|
# NIDUS_LOG_FORMAT - log format: text, json (default: text)
|
||||||
|
|
||||||
|
# Example (uncomment and modify as needed):
|
||||||
|
# export NIDUS_HOST="0.0.0.0"
|
||||||
|
# export NIDUS_PORT="8080"
|
||||||
|
# export NIDUS_BASE_URL="https://dav.example.com"
|
||||||
|
# export NIDUS_DATA_DIR="./data"
|
||||||
|
# export NIDUS_LOG_LEVEL="info"
|
||||||
|
# export NIDUS_LOG_FORMAT="text"
|
||||||
|
|
||||||
|
# Note: The server does not handle TLS - use a reverse proxy (e.g. Nginx, Caddy,
|
||||||
|
# Traefik) to terminate TLS and forward requests to the server.
|
||||||
|
|||||||
+12
-4
@@ -1,11 +1,19 @@
|
|||||||
services:
|
services:
|
||||||
davserver:
|
nidus:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
# Optional: other environment variables
|
||||||
|
# - NIDUS_DATA_DIR=/app/data
|
||||||
|
# - NIDUS_HOST=0.0.0.0
|
||||||
|
# - NIDUS_PORT=8080
|
||||||
|
# - NIDUS_BASE_URL=https://dav.example.com
|
||||||
|
# - NIDUS_AUTH_REALM="My DAV Server"
|
||||||
|
# - NIDUS_LOG_LEVEL=info
|
||||||
|
# - NIDUS_LOG_FORMAT=text
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yaml:/app/config.yaml:ro
|
- nidus-data:/app/data
|
||||||
- dav-data:/app/data
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
|
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
|
||||||
@@ -14,4 +22,4 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dav-data:
|
nidus-data:
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
ical "github.com/emersion/go-ical"
|
|
||||||
"github.com/emersion/go-webdav"
|
|
||||||
"github.com/emersion/go-webdav/caldav"
|
|
||||||
"git.arnef.de/arnef/nidus/internal/auth"
|
"git.arnef.de/arnef/nidus/internal/auth"
|
||||||
"git.arnef.de/arnef/nidus/internal/config"
|
"git.arnef.de/arnef/nidus/internal/config"
|
||||||
"git.arnef.de/arnef/nidus/internal/db"
|
"git.arnef.de/arnef/nidus/internal/db"
|
||||||
"git.arnef.de/arnef/nidus/internal/icalfix"
|
"git.arnef.de/arnef/nidus/internal/icalfix"
|
||||||
"git.arnef.de/arnef/nidus/internal/icssub"
|
"git.arnef.de/arnef/nidus/internal/icssub"
|
||||||
"git.arnef.de/arnef/nidus/internal/store"
|
"git.arnef.de/arnef/nidus/internal/store"
|
||||||
|
ical "github.com/emersion/go-ical"
|
||||||
|
"github.com/emersion/go-webdav"
|
||||||
|
"github.com/emersion/go-webdav/caldav"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sharedNameSep separates the owner from the calendar name in the
|
// sharedNameSep separates the owner from the calendar name in the
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
ical "github.com/emersion/go-ical"
|
|
||||||
"git.arnef.de/arnef/nidus/internal/auth"
|
"git.arnef.de/arnef/nidus/internal/auth"
|
||||||
"git.arnef.de/arnef/nidus/internal/config"
|
"git.arnef.de/arnef/nidus/internal/config"
|
||||||
"git.arnef.de/arnef/nidus/internal/db"
|
"git.arnef.de/arnef/nidus/internal/db"
|
||||||
"git.arnef.de/arnef/nidus/internal/store"
|
"git.arnef.de/arnef/nidus/internal/store"
|
||||||
|
ical "github.com/emersion/go-ical"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestBackend(t *testing.T) (*Backend, *db.DB) {
|
func newTestBackend(t *testing.T) (*Backend, *db.DB) {
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
vcard "github.com/emersion/go-vcard"
|
|
||||||
"github.com/emersion/go-webdav"
|
|
||||||
"github.com/emersion/go-webdav/carddav"
|
|
||||||
"git.arnef.de/arnef/nidus/internal/auth"
|
"git.arnef.de/arnef/nidus/internal/auth"
|
||||||
"git.arnef.de/arnef/nidus/internal/config"
|
"git.arnef.de/arnef/nidus/internal/config"
|
||||||
"git.arnef.de/arnef/nidus/internal/db"
|
"git.arnef.de/arnef/nidus/internal/db"
|
||||||
"git.arnef.de/arnef/nidus/internal/store"
|
"git.arnef.de/arnef/nidus/internal/store"
|
||||||
|
vcard "github.com/emersion/go-vcard"
|
||||||
|
"github.com/emersion/go-webdav"
|
||||||
|
"github.com/emersion/go-webdav/carddav"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sharedNameSep separates the owner from the address book name in the
|
// sharedNameSep separates the owner from the address book name in the
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
vcard "github.com/emersion/go-vcard"
|
|
||||||
"git.arnef.de/arnef/nidus/internal/auth"
|
"git.arnef.de/arnef/nidus/internal/auth"
|
||||||
"git.arnef.de/arnef/nidus/internal/config"
|
"git.arnef.de/arnef/nidus/internal/config"
|
||||||
"git.arnef.de/arnef/nidus/internal/db"
|
"git.arnef.de/arnef/nidus/internal/db"
|
||||||
"git.arnef.de/arnef/nidus/internal/store"
|
"git.arnef.de/arnef/nidus/internal/store"
|
||||||
|
vcard "github.com/emersion/go-vcard"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestBackend(t *testing.T) (*Backend, *db.DB) {
|
func newTestBackend(t *testing.T) (*Backend, *db.DB) {
|
||||||
|
|||||||
+37
-64
@@ -3,97 +3,70 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the top-level server configuration.
|
// Config is the top-level server configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `yaml:"server"`
|
Server ServerConfig
|
||||||
Auth AuthConfig `yaml:"auth"`
|
Auth AuthConfig
|
||||||
Storage StorageConfig `yaml:"storage"`
|
Storage StorageConfig
|
||||||
TLS TLSConfig `yaml:"tls"`
|
Logging LoggingConfig
|
||||||
Logging LoggingConfig `yaml:"logging"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Host string `yaml:"host"`
|
Host string
|
||||||
Port int `yaml:"port"`
|
Port int
|
||||||
// Base URL used in DAV responses (e.g. https://dav.example.com)
|
BaseURL string
|
||||||
BaseURL string `yaml:"base_url"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
// Realm shown in WWW-Authenticate header
|
Realm string
|
||||||
Realm string `yaml:"realm"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageConfig struct {
|
type StorageConfig struct {
|
||||||
// Root directory for all data
|
DataDir string
|
||||||
DataDir string `yaml:"data_dir"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TLSConfig struct {
|
|
||||||
Enabled bool `yaml:"enabled"`
|
|
||||||
CertFile string `yaml:"cert_file"`
|
|
||||||
KeyFile string `yaml:"key_file"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoggingConfig struct {
|
type LoggingConfig struct {
|
||||||
Level string `yaml:"level"` // debug | info | warn | error
|
Level string
|
||||||
Format string `yaml:"format"` // text | json
|
Format string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads and parses a YAML config file.
|
// Load reads and parses environment variables to create the configuration.
|
||||||
func Load(path string) (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("reading config %q: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &Config{}
|
cfg := &Config{}
|
||||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing config %q: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.applyDefaults()
|
cfg.applyDefaults()
|
||||||
|
return cfg, nil
|
||||||
return cfg, cfg.validate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) applyDefaults() {
|
func (c *Config) applyDefaults() {
|
||||||
if c.Server.Host == "" {
|
c.Server.Host = getEnv("NIDUS_HOST", "0.0.0.0")
|
||||||
c.Server.Host = "0.0.0.0"
|
c.Server.Port = getEnvInt("NIDUS_PORT", 8080)
|
||||||
}
|
c.Server.BaseURL = getEnv("NIDUS_BASE_URL", "")
|
||||||
if c.Server.Port == 0 {
|
|
||||||
c.Server.Port = 8080
|
|
||||||
}
|
|
||||||
if c.Server.BaseURL == "" {
|
if c.Server.BaseURL == "" {
|
||||||
scheme := "http"
|
c.Server.BaseURL = fmt.Sprintf("http://%s:%d", c.Server.Host, c.Server.Port)
|
||||||
if c.TLS.Enabled {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
c.Server.BaseURL = fmt.Sprintf("%s://%s:%d", scheme, c.Server.Host, c.Server.Port)
|
|
||||||
}
|
|
||||||
if c.Auth.Realm == "" {
|
|
||||||
c.Auth.Realm = "DAV Server"
|
|
||||||
}
|
|
||||||
if c.Storage.DataDir == "" {
|
|
||||||
c.Storage.DataDir = "./data"
|
|
||||||
}
|
|
||||||
if c.Logging.Level == "" {
|
|
||||||
c.Logging.Level = "info"
|
|
||||||
}
|
|
||||||
if c.Logging.Format == "" {
|
|
||||||
c.Logging.Format = "text"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Auth.Realm = getEnv("NIDUS_AUTH_REALM", "DAV Server")
|
||||||
|
c.Storage.DataDir = getEnv("NIDUS_DATA_DIR", "./data")
|
||||||
|
c.Logging.Level = getEnv("NIDUS_LOG_LEVEL", "info")
|
||||||
|
c.Logging.Format = getEnv("NIDUS_LOG_FORMAT", "text")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) validate() error {
|
func getEnv(key string, defaultValue string) string {
|
||||||
if c.TLS.Enabled {
|
if val := os.Getenv(key); val != "" {
|
||||||
if c.TLS.CertFile == "" || c.TLS.KeyFile == "" {
|
return val
|
||||||
return fmt.Errorf("tls.cert_file and tls.key_file are required when tls is enabled")
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvInt(key string, defaultValue int) int {
|
||||||
|
if val := os.Getenv(key); val != "" {
|
||||||
|
if intVal, err := strconv.Atoi(val); err == nil {
|
||||||
|
return intVal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
ical "github.com/emersion/go-ical"
|
|
||||||
"git.arnef.de/arnef/nidus/internal/birthdays"
|
"git.arnef.de/arnef/nidus/internal/birthdays"
|
||||||
"git.arnef.de/arnef/nidus/internal/db"
|
"git.arnef.de/arnef/nidus/internal/db"
|
||||||
"git.arnef.de/arnef/nidus/internal/icalfix"
|
"git.arnef.de/arnef/nidus/internal/icalfix"
|
||||||
"git.arnef.de/arnef/nidus/internal/web/templates"
|
"git.arnef.de/arnef/nidus/internal/web/templates"
|
||||||
|
ical "github.com/emersion/go-ical"
|
||||||
)
|
)
|
||||||
|
|
||||||
// eventIDRe validates an event's object ID as it appears in a URL path
|
// eventIDRe validates an event's object ID as it appears in a URL path
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
vcard "github.com/emersion/go-vcard"
|
|
||||||
"git.arnef.de/arnef/nidus/internal/store"
|
"git.arnef.de/arnef/nidus/internal/store"
|
||||||
"git.arnef.de/arnef/nidus/internal/web/templates"
|
"git.arnef.de/arnef/nidus/internal/web/templates"
|
||||||
|
vcard "github.com/emersion/go-vcard"
|
||||||
)
|
)
|
||||||
|
|
||||||
// contactIDRe validates a contact's object ID as it appears in a URL path
|
// contactIDRe validates a contact's object ID as it appears in a URL path
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func (s *Server) setSessionCookie(w http.ResponseWriter, token string) {
|
|||||||
Value: token,
|
Value: token,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: s.cfg.TLS.Enabled,
|
Secure: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
Expires: time.Now().Add(7 * 24 * time.Hour),
|
Expires: time.Now().Add(7 * 24 * time.Hour),
|
||||||
})
|
})
|
||||||
@@ -58,7 +58,7 @@ func (s *Server) clearSessionCookie(w http.ResponseWriter) {
|
|||||||
Value: "",
|
Value: "",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: s.cfg.TLS.Enabled,
|
Secure: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,13 +18,12 @@ func main() {
|
|||||||
|
|
||||||
func run(args []string) int {
|
func run(args []string) int {
|
||||||
fs := flag.NewFlagSet("migrate", flag.ContinueOnError)
|
fs := flag.NewFlagSet("migrate", flag.ContinueOnError)
|
||||||
cfgPath := fs.String("config", "config.yaml", "path to configuration file")
|
|
||||||
verbose := fs.Bool("verbose", false, "enable verbose output")
|
verbose := fs.Bool("verbose", false, "enable verbose output")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.Load(*cfgPath)
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error loading config: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error loading config: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
+44
-57
@@ -8,7 +8,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -23,18 +22,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run(args []string) int {
|
func run(args []string) int {
|
||||||
fs := flag.NewFlagSet("nidusctl", flag.ContinueOnError)
|
if len(args) < 1 {
|
||||||
cfgPath := fs.String("config", "config.yaml", "path to configuration file")
|
|
||||||
if err := fs.Parse(args); err != nil {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
rest := fs.Args()
|
|
||||||
if len(rest) < 1 {
|
|
||||||
usage()
|
usage()
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.Load(*cfgPath)
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error loading config: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error loading config: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
@@ -54,20 +47,20 @@ func run(args []string) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
switch rest[0] {
|
switch args[0] {
|
||||||
case "user":
|
case "user":
|
||||||
return runUser(dbase, rest[1:])
|
return runUser(dbase, args[1:])
|
||||||
case "calendar", "cal":
|
case "calendar", "cal":
|
||||||
return runCalendar(dbase, st, rest[1:])
|
return runCalendar(dbase, st, args[1:])
|
||||||
case "addressbook", "card":
|
case "addressbook", "card":
|
||||||
return runAddressBook(dbase, st, rest[1:])
|
return runAddressBook(dbase, st, args[1:])
|
||||||
case "migrate":
|
case "migrate":
|
||||||
return runMigrate(st, rest[1:])
|
return runMigrate(st, args[1:])
|
||||||
case "help", "-h", "--help":
|
case "help", "-h", "--help":
|
||||||
usage()
|
usage()
|
||||||
return 0
|
return 0
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", rest[0])
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
|
||||||
usage()
|
usage()
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
@@ -77,43 +70,41 @@ func usage() {
|
|||||||
fmt.Fprint(os.Stderr, `nidusctl - manage nidus DAV server users, resources, and sharing grants
|
fmt.Fprint(os.Stderr, `nidusctl - manage nidus DAV server users, resources, and sharing grants
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
nidusctl [-config config.yaml] user create <username> [--display-name NAME] [--email EMAIL] [--password PW]
|
nidusctl user create <username> [--display-name NAME] [--email EMAIL] [--password PW]
|
||||||
nidusctl [-config config.yaml] user delete <username>
|
nidusctl user delete <username>
|
||||||
nidusctl [-config config.yaml] user list
|
nidusctl user list
|
||||||
nidusctl [-config config.yaml] user passwd <username> [--password PW]
|
nidusctl user passwd <username> [--password PW]
|
||||||
|
|
||||||
nidusctl [-config config.yaml] calendar create <owner> <calendar> [--color '#RRGGBB']
|
nidusctl calendar create <owner> <calendar> [--color '#RRGGBB']
|
||||||
nidusctl [-config config.yaml] calendar color <owner> <calendar> <hex-color>
|
nidusctl calendar color <owner> <calendar> <hex-color>
|
||||||
nidusctl [-config config.yaml] calendar delete <owner> <calendar>
|
nidusctl calendar delete <owner> <calendar>
|
||||||
nidusctl [-config config.yaml] calendar list <owner>
|
nidusctl calendar list <owner>
|
||||||
nidusctl [-config config.yaml] calendar share <owner> <calendar> <user> <read|write>
|
nidusctl calendar share <owner> <calendar> <user> <read|write>
|
||||||
nidusctl [-config config.yaml] calendar unshare <owner> <calendar> <user>
|
nidusctl calendar unshare <owner> <calendar> <user>
|
||||||
nidusctl [-config config.yaml] calendar shares <owner> <calendar>
|
nidusctl calendar shares <owner> <calendar>
|
||||||
|
|
||||||
nidusctl [-config config.yaml] addressbook create <owner> <book>
|
nidusctl addressbook create <owner> <book>
|
||||||
nidusctl [-config config.yaml] addressbook delete <owner> <book>
|
nidusctl addressbook delete <owner> <book>
|
||||||
nidusctl [-config config.yaml] addressbook list <owner>
|
nidusctl addressbook list <owner>
|
||||||
nidusctl [-config config.yaml] addressbook share <owner> <book> <user> <read|write>
|
nidusctl addressbook share <owner> <book> <user> <read|write>
|
||||||
nidusctl [-config config.yaml] addressbook unshare <owner> <book> <user>
|
nidusctl addressbook unshare <owner> <book> <user>
|
||||||
nidusctl [-config config.yaml] addressbook shares <owner> <book>
|
nidusctl addressbook shares <owner> <book>
|
||||||
|
|
||||||
nidusctl [-config config.yaml] migrate [--verbose]
|
nidusctl migrate [--verbose]
|
||||||
|
|
||||||
|
nidusctl help
|
||||||
|
|
||||||
|
Configuration is done via environment variables:
|
||||||
|
NIDUS_DATA_DIR - data directory (default: ./data)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
nidusctl user create alice --display-name "Alice Smith" --email alice@example.com
|
nidusctl user create alice --display-name "Alice Smith" --email alice@example.com
|
||||||
nidusctl calendar create alice work
|
nidusctl calendar create alice work
|
||||||
nidusctl calendar share alice work bob write
|
nidusctl calendar share alice work bob write
|
||||||
nidusctl calendar shares alice work
|
nidusctl calendar shares alice work
|
||||||
nidusctl calendar unshare alice work bob
|
nidusctl calendar unshare alice work bob
|
||||||
nidusctl migrate
|
nidusctl migrate
|
||||||
`)
|
`)
|
||||||
}
|
|
||||||
|
|
||||||
// newFlagSet creates a flag.FlagSet configured for subcommand parsing
|
|
||||||
// (flags may appear before or after positional args, since callers parse
|
|
||||||
// flags first with fs.Parse then read fs.Args() for the rest).
|
|
||||||
func newFlagSet(name string) *flag.FlagSet {
|
|
||||||
return flag.NewFlagSet(name, flag.ContinueOnError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCalendar(dbase *db.DB, st *store.Store, args []string) int {
|
func runCalendar(dbase *db.DB, st *store.Store, args []string) int {
|
||||||
@@ -123,17 +114,17 @@ func runCalendar(dbase *db.DB, st *store.Store, args []string) int {
|
|||||||
}
|
}
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "create":
|
case "create":
|
||||||
fs := newFlagSet("calendar create")
|
color := ""
|
||||||
color := fs.String("color", "", "hex color like #3b82f6 (optional)")
|
if len(args) > 2 && args[1] == "--color" {
|
||||||
if err := fs.Parse(args[1:]); err != nil {
|
color = args[2]
|
||||||
return 2
|
args = args[:1]
|
||||||
}
|
}
|
||||||
if fs.NArg() != 2 {
|
if len(args) != 3 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: nidusctl calendar create <owner> <calendar> [--color '#RRGGBB']")
|
fmt.Fprintln(os.Stderr, "usage: nidusctl calendar create <owner> <calendar> [--color '#RRGGBB']")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
owner, calName := fs.Arg(0), fs.Arg(1)
|
owner, calName := args[1], args[2]
|
||||||
if err := dbase.CreateCalendarWithColor(owner, calName, *color); err != nil {
|
if err := dbase.CreateCalendarWithColor(owner, calName, color); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -365,11 +356,7 @@ func runAddressBook(dbase *db.DB, st *store.Store, args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runMigrate(st *store.Store, args []string) int {
|
func runMigrate(st *store.Store, args []string) int {
|
||||||
fs := newFlagSet("migrate")
|
// Ignore args for now (could add --verbose flag in future if needed)
|
||||||
if err := fs.Parse(args); err != nil {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := st.Migrate(); err != nil {
|
if err := st.Migrate(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "migration failed: %v\n", err)
|
fmt.Fprintf(os.Stderr, "migration failed: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
+18
-38
@@ -12,25 +12,6 @@ import (
|
|||||||
"git.arnef.de/arnef/nidus/internal/db"
|
"git.arnef.de/arnef/nidus/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// writeTestConfig creates a minimal config.yaml in dir and returns its path.
|
|
||||||
func writeTestConfig(t *testing.T, dir string) string {
|
|
||||||
t.Helper()
|
|
||||||
cfgPath := filepath.Join(dir, "config.yaml")
|
|
||||||
dataDir := filepath.Join(dir, "data")
|
|
||||||
content := "storage:\n data_dir: " + dataDir + "\n" +
|
|
||||||
"users:\n" +
|
|
||||||
" alice:\n" +
|
|
||||||
" password: \"$2a$10$9.WEs0uz5TaNJLQbSTLrX.Te.BIe8XTTykVRzZbSHQMEkyFb8Sq/O\"\n" +
|
|
||||||
" bob:\n" +
|
|
||||||
" password: \"$2a$10$9.WEs0uz5TaNJLQbSTLrX.Te.BIe8XTTykVRzZbSHQMEkyFb8Sq/O\"\n"
|
|
||||||
if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
|
|
||||||
t.Fatalf("writing test config: %v", err)
|
|
||||||
}
|
|
||||||
return cfgPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// runCLI runs the CLI's run() function, capturing stdout/stderr, and
|
|
||||||
// returns (exit code, combined stdout+stderr).
|
|
||||||
func runCLI(t *testing.T, args ...string) (int, string) {
|
func runCLI(t *testing.T, args ...string) (int, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -56,9 +37,9 @@ func runCLI(t *testing.T, args ...string) (int, string) {
|
|||||||
|
|
||||||
func TestCalendarShareUnshareLifecycle(t *testing.T) {
|
func TestCalendarShareUnshareLifecycle(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
cfgPath := writeTestConfig(t, dir)
|
t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data"))
|
||||||
|
|
||||||
code, out := runCLI(t, "-config", cfgPath, "calendar", "share", "alice", "work", "bob", "write")
|
code, out := runCLI(t, "calendar", "share", "alice", "work", "bob", "write")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("share exit code = %d, output: %s", code, out)
|
t.Fatalf("share exit code = %d, output: %s", code, out)
|
||||||
}
|
}
|
||||||
@@ -66,7 +47,7 @@ func TestCalendarShareUnshareLifecycle(t *testing.T) {
|
|||||||
t.Errorf("output = %q, want to contain 'shared'", out)
|
t.Errorf("output = %q, want to contain 'shared'", out)
|
||||||
}
|
}
|
||||||
|
|
||||||
code, out = runCLI(t, "-config", cfgPath, "calendar", "shares", "alice", "work")
|
code, out = runCLI(t, "calendar", "shares", "alice", "work")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("shares exit code = %d, output: %s", code, out)
|
t.Fatalf("shares exit code = %d, output: %s", code, out)
|
||||||
}
|
}
|
||||||
@@ -74,12 +55,12 @@ func TestCalendarShareUnshareLifecycle(t *testing.T) {
|
|||||||
t.Errorf("output = %q, want to contain bob/write", out)
|
t.Errorf("output = %q, want to contain bob/write", out)
|
||||||
}
|
}
|
||||||
|
|
||||||
code, out = runCLI(t, "-config", cfgPath, "calendar", "unshare", "alice", "work", "bob")
|
code, out = runCLI(t, "calendar", "unshare", "alice", "work", "bob")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("unshare exit code = %d, output: %s", code, out)
|
t.Fatalf("unshare exit code = %d, output: %s", code, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
code, out = runCLI(t, "-config", cfgPath, "calendar", "shares", "alice", "work")
|
code, out = runCLI(t, "calendar", "shares", "alice", "work")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("shares (after unshare) exit code = %d, output: %s", code, out)
|
t.Fatalf("shares (after unshare) exit code = %d, output: %s", code, out)
|
||||||
}
|
}
|
||||||
@@ -90,9 +71,9 @@ func TestCalendarShareUnshareLifecycle(t *testing.T) {
|
|||||||
|
|
||||||
func TestCalendarShareInvalidPermission(t *testing.T) {
|
func TestCalendarShareInvalidPermission(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
cfgPath := writeTestConfig(t, dir)
|
t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data"))
|
||||||
|
|
||||||
code, out := runCLI(t, "-config", cfgPath, "calendar", "share", "alice", "work", "bob", "admin")
|
code, out := runCLI(t, "calendar", "share", "alice", "work", "bob", "admin")
|
||||||
if code != 2 {
|
if code != 2 {
|
||||||
t.Errorf("exit code = %d, want 2; output: %s", code, out)
|
t.Errorf("exit code = %d, want 2; output: %s", code, out)
|
||||||
}
|
}
|
||||||
@@ -103,9 +84,9 @@ func TestCalendarShareInvalidPermission(t *testing.T) {
|
|||||||
|
|
||||||
func TestCalendarUnshareNotFound(t *testing.T) {
|
func TestCalendarUnshareNotFound(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
cfgPath := writeTestConfig(t, dir)
|
t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data"))
|
||||||
|
|
||||||
code, out := runCLI(t, "-config", cfgPath, "calendar", "unshare", "alice", "ghost", "bob")
|
code, out := runCLI(t, "calendar", "unshare", "alice", "ghost", "bob")
|
||||||
if code != 1 {
|
if code != 1 {
|
||||||
t.Errorf("exit code = %d, want 1; output: %s", code, out)
|
t.Errorf("exit code = %d, want 1; output: %s", code, out)
|
||||||
}
|
}
|
||||||
@@ -116,14 +97,14 @@ func TestCalendarUnshareNotFound(t *testing.T) {
|
|||||||
|
|
||||||
func TestAddressBookShareUnshareLifecycle(t *testing.T) {
|
func TestAddressBookShareUnshareLifecycle(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
cfgPath := writeTestConfig(t, dir)
|
t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data"))
|
||||||
|
|
||||||
code, out := runCLI(t, "-config", cfgPath, "addressbook", "share", "alice", "contacts", "bob", "read")
|
code, out := runCLI(t, "addressbook", "share", "alice", "contacts", "bob", "read")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("share exit code = %d, output: %s", code, out)
|
t.Fatalf("share exit code = %d, output: %s", code, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
code, out = runCLI(t, "-config", cfgPath, "addressbook", "shares", "alice", "contacts")
|
code, out = runCLI(t, "addressbook", "shares", "alice", "contacts")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("shares exit code = %d, output: %s", code, out)
|
t.Fatalf("shares exit code = %d, output: %s", code, out)
|
||||||
}
|
}
|
||||||
@@ -131,7 +112,7 @@ func TestAddressBookShareUnshareLifecycle(t *testing.T) {
|
|||||||
t.Errorf("output = %q, want to contain bob/read", out)
|
t.Errorf("output = %q, want to contain bob/read", out)
|
||||||
}
|
}
|
||||||
|
|
||||||
code, out = runCLI(t, "-config", cfgPath, "addressbook", "unshare", "alice", "contacts", "bob")
|
code, out = runCLI(t, "addressbook", "unshare", "alice", "contacts", "bob")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("unshare exit code = %d, output: %s", code, out)
|
t.Fatalf("unshare exit code = %d, output: %s", code, out)
|
||||||
}
|
}
|
||||||
@@ -139,9 +120,9 @@ func TestAddressBookShareUnshareLifecycle(t *testing.T) {
|
|||||||
|
|
||||||
func TestUnknownUserWarningDoesNotBlockShare(t *testing.T) {
|
func TestUnknownUserWarningDoesNotBlockShare(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
cfgPath := writeTestConfig(t, dir)
|
t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data"))
|
||||||
|
|
||||||
code, out := runCLI(t, "-config", cfgPath, "calendar", "share", "alice", "work", "carol", "read")
|
code, out := runCLI(t, "calendar", "share", "alice", "work", "carol", "read")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, output: %s", code, out)
|
t.Fatalf("exit code = %d, output: %s", code, out)
|
||||||
}
|
}
|
||||||
@@ -154,10 +135,9 @@ func TestUnknownUserWarningDoesNotBlockShare(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNoArgsShowsUsage(t *testing.T) {
|
func TestNoArgsShowsUsage(t *testing.T) {
|
||||||
dir := t.TempDir()
|
|
||||||
// No config needed since usage() is printed before config.Load for
|
// No config needed since usage() is printed before config.Load for
|
||||||
// missing subcommands.
|
// missing subcommands.
|
||||||
code, out := runCLI(t, "-config", filepath.Join(dir, "missing.yaml"))
|
code, out := runCLI(t)
|
||||||
if code != 2 {
|
if code != 2 {
|
||||||
t.Errorf("exit code = %d, want 2", code)
|
t.Errorf("exit code = %d, want 2", code)
|
||||||
}
|
}
|
||||||
@@ -168,9 +148,9 @@ func TestNoArgsShowsUsage(t *testing.T) {
|
|||||||
|
|
||||||
func TestUnknownCommand(t *testing.T) {
|
func TestUnknownCommand(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
cfgPath := writeTestConfig(t, dir)
|
t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data"))
|
||||||
|
|
||||||
code, out := runCLI(t, "-config", cfgPath, "bogus")
|
code, out := runCLI(t, "bogus")
|
||||||
if code != 2 {
|
if code != 2 {
|
||||||
t.Errorf("exit code = %d, want 2", code)
|
t.Errorf("exit code = %d, want 2", code)
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-20
@@ -31,31 +31,47 @@ func runUser(dbase *db.DB, args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func userCreate(dbase *db.DB, args []string) int {
|
func userCreate(dbase *db.DB, args []string) int {
|
||||||
fs := newFlagSet("nidusctl user create")
|
var displayName, email, password string
|
||||||
displayName := fs.String("display-name", "", "display name shown in DAV clients")
|
var rest []string
|
||||||
email := fs.String("email", "", "email address")
|
|
||||||
password := fs.String("password", "", "password (omit to be prompted, recommended)")
|
for i := 0; i < len(args); i++ {
|
||||||
if err := fs.Parse(args); err != nil {
|
switch args[i] {
|
||||||
return 2
|
case "--display-name":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
displayName = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case "--email":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
email = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case "--password":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
password = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
rest = append(rest, args[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rest := fs.Args()
|
|
||||||
if len(rest) != 1 {
|
if len(rest) != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: nidusctl user create <username> [--display-name NAME] [--email EMAIL] [--password PASSWORD]")
|
fmt.Fprintln(os.Stderr, "usage: nidusctl user create <username> [--display-name NAME] [--email EMAIL] [--password PASSWORD]")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
username := rest[0]
|
username := rest[0]
|
||||||
|
|
||||||
pw := *password
|
if password == "" {
|
||||||
if pw == "" {
|
|
||||||
var err error
|
var err error
|
||||||
pw, err = promptPassword(username)
|
password, err = promptPassword(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error reading password: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error reading password: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbase.CreateUser(username, pw, *displayName, *email); err != nil {
|
if err := dbase.CreateUser(username, password, displayName, email); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -98,29 +114,37 @@ func userList(dbase *db.DB, args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func userPasswd(dbase *db.DB, args []string) int {
|
func userPasswd(dbase *db.DB, args []string) int {
|
||||||
fs := newFlagSet("nidusctl user passwd")
|
var password string
|
||||||
password := fs.String("password", "", "new password (omit to be prompted, recommended)")
|
var rest []string
|
||||||
if err := fs.Parse(args); err != nil {
|
|
||||||
return 2
|
for i := 0; i < len(args); i++ {
|
||||||
|
switch args[i] {
|
||||||
|
case "--password":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
password = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
rest = append(rest, args[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rest := fs.Args()
|
|
||||||
if len(rest) != 1 {
|
if len(rest) != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: nidusctl user passwd <username> [--password PASSWORD]")
|
fmt.Fprintln(os.Stderr, "usage: nidusctl user passwd <username> [--password PASSWORD]")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
username := rest[0]
|
username := rest[0]
|
||||||
|
|
||||||
pw := *password
|
if password == "" {
|
||||||
if pw == "" {
|
|
||||||
var err error
|
var err error
|
||||||
pw, err = promptPassword(username)
|
password, err = promptPassword(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error reading password: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error reading password: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbase.SetPassword(username, pw); err != nil {
|
if err := dbase.SetPassword(username, password); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user