Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1b67b9909 | |||
| 651eabe995 | |||
| aeb12d9f50 | |||
| ab2884a8e9 | |||
| 691301dade | |||
| 2185bbe563 |
@@ -226,13 +226,14 @@ Built with Go `html/template` + htmx. Embedded via `//go:embed`.
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
listen_addr = ":8443"
|
listen_addr = ":8443" # REST API (HTTPS)
|
||||||
grpc_addr = ":9443"
|
grpc_addr = ":9443" # gRPC (TLS, exposed directly)
|
||||||
tls_cert = "/srv/eng-pad-server/certs/cert.pem"
|
grpc_plain_addr = "" # Optional plaintext gRPC for reverse proxy
|
||||||
tls_key = "/srv/eng-pad-server/certs/key.pem"
|
tls_cert = "/srv/eng-pad-server/certs/fullchain.pem"
|
||||||
|
tls_key = "/srv/eng-pad-server/certs/privkey.pem"
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
listen_addr = ":8080"
|
listen_addr = ":8080" # Web UI (plain HTTP behind nginx)
|
||||||
base_url = "https://pad.metacircular.net"
|
base_url = "https://pad.metacircular.net"
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
@@ -255,13 +256,32 @@ level = "info"
|
|||||||
|
|
||||||
## 9. Deployment
|
## 9. Deployment
|
||||||
|
|
||||||
|
### Production (deimos.wntrmute.net)
|
||||||
|
|
||||||
|
Docker container behind nginx on deimos:
|
||||||
|
|
||||||
|
- **Web UI**: `https://pad.metacircular.net` — nginx (port 443) → container:8080
|
||||||
|
- **gRPC sync**: `pad.metacircular.net:9443` — direct TLS, exposed via ufw
|
||||||
|
- **REST API**: container:8443 — not exposed externally
|
||||||
|
- **TLS**: Let's Encrypt cert for `pad.metacircular.net`, shared by nginx
|
||||||
|
and the container (copied to `/srv/eng-pad-server/certs/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
│
|
||||||
|
├── :443 → nginx (TLS termination) → container:8080 (Web UI, plain HTTP)
|
||||||
|
└── :9443 → container:9443 (gRPC, direct TLS)
|
||||||
|
```
|
||||||
|
|
||||||
### Container
|
### Container
|
||||||
|
|
||||||
Multi-stage Docker build:
|
Multi-stage Docker build:
|
||||||
1. Builder: `golang:1.25-alpine`, `CGO_ENABLED=0`, stripped binary
|
1. Builder: `golang:1.25-alpine`, `CGO_ENABLED=0`, stripped binary
|
||||||
2. Runtime: `alpine:latest`, non-root user
|
2. Runtime: `alpine:3.21`, non-root user (`engpad`, UID 1000)
|
||||||
|
|
||||||
### systemd
|
### systemd (alternative)
|
||||||
|
|
||||||
|
systemd units are provided for non-Docker deployments:
|
||||||
|
|
||||||
| Unit | Purpose |
|
| Unit | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
@@ -279,8 +299,8 @@ ReadWritePaths=/srv/eng-pad-server.
|
|||||||
├── eng-pad-server.toml
|
├── eng-pad-server.toml
|
||||||
├── eng-pad-server.db
|
├── eng-pad-server.db
|
||||||
├── certs/
|
├── certs/
|
||||||
│ ├── cert.pem
|
│ ├── fullchain.pem # Let's Encrypt cert chain
|
||||||
│ └── key.pem
|
│ └── privkey.pem # Let's Encrypt private key
|
||||||
└── backups/
|
└── backups/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -301,6 +321,7 @@ ReadWritePaths=/srv/eng-pad-server.
|
|||||||
|---------|---------|
|
|---------|---------|
|
||||||
| server | Start the service |
|
| server | Start the service |
|
||||||
| init | Create database, first user |
|
| init | Create database, first user |
|
||||||
|
| passwd | Reset a user's password |
|
||||||
| snapshot | Database backup (VACUUM INTO) |
|
| snapshot | Database backup (VACUUM INTO) |
|
||||||
| status | Health check |
|
| status | Health check |
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ eng-pad-server/
|
|||||||
│ └── eng-pad-server/ CLI entry point (cobra)
|
│ └── eng-pad-server/ CLI entry point (cobra)
|
||||||
│ ├── main.go
|
│ ├── main.go
|
||||||
│ ├── server.go server subcommand
|
│ ├── server.go server subcommand
|
||||||
│ └── init.go init subcommand
|
│ ├── init.go init subcommand
|
||||||
|
│ └── passwd.go password reset subcommand
|
||||||
├── internal/
|
├── internal/
|
||||||
│ ├── auth/
|
│ ├── auth/
|
||||||
│ │ ├── argon2.go Password hashing
|
│ │ ├── argon2.go Password hashing
|
||||||
|
|||||||
52
README.md
52
README.md
@@ -27,10 +27,10 @@ cp eng-pad-server.toml.example /srv/eng-pad-server/eng-pad-server.toml
|
|||||||
# Edit configuration (TLS certs, database path, etc.)
|
# Edit configuration (TLS certs, database path, etc.)
|
||||||
|
|
||||||
# Initialize (creates database, prompts for admin user)
|
# Initialize (creates database, prompts for admin user)
|
||||||
./eng-pad-server init
|
./eng-pad-server init -c /srv/eng-pad-server/eng-pad-server.toml
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
./eng-pad-server server
|
./eng-pad-server server -c /srv/eng-pad-server/eng-pad-server.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
@@ -43,6 +43,54 @@ make proto # regenerate gRPC code from .proto files
|
|||||||
make proto-lint # buf lint + breaking change detection
|
make proto-lint # buf lint + breaking change detection
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## User Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create initial user (interactive — prompts for username and password)
|
||||||
|
eng-pad-server init -c /srv/eng-pad-server/eng-pad-server.toml
|
||||||
|
|
||||||
|
# Reset a user's password
|
||||||
|
eng-pad-server passwd <username> -c /srv/eng-pad-server/eng-pad-server.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment (deimos.wntrmute.net)
|
||||||
|
|
||||||
|
The production instance runs as a Docker container on deimos behind nginx.
|
||||||
|
|
||||||
|
- **Web UI**: `https://pad.metacircular.net` (nginx → container:8080)
|
||||||
|
- **REST API**: `https://pad.metacircular.net:8443` (direct TLS)
|
||||||
|
- **gRPC sync**: `pad.metacircular.net:9443` (direct TLS, for Android app)
|
||||||
|
- **Data**: `/srv/eng-pad-server/` on deimos
|
||||||
|
- **TLS**: Let's Encrypt cert, shared by nginx and the container
|
||||||
|
|
||||||
|
### Deploy workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From local machine:
|
||||||
|
rsync -az --exclude='.git' --exclude='srv/' . deimos.wntrmute.net:/tmp/eng-pad-server-build/
|
||||||
|
ssh deimos.wntrmute.net "cd /tmp/eng-pad-server-build && \
|
||||||
|
docker build -t eng-pad-server . && \
|
||||||
|
docker stop eng-pad-server && docker rm eng-pad-server && \
|
||||||
|
docker run -d --name eng-pad-server --restart unless-stopped \
|
||||||
|
-p 127.0.0.1:8090:8080 -p 8443:8443 -p 9443:9443 \
|
||||||
|
-v /srv/eng-pad-server:/srv/eng-pad-server eng-pad-server"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View logs
|
||||||
|
ssh deimos.wntrmute.net "docker logs eng-pad-server"
|
||||||
|
|
||||||
|
# Create/reset user
|
||||||
|
ssh -t deimos.wntrmute.net "docker exec -it eng-pad-server \
|
||||||
|
eng-pad-server passwd kyle -c /srv/eng-pad-server/eng-pad-server.toml"
|
||||||
|
|
||||||
|
# Renew TLS certs (after certbot renews)
|
||||||
|
ssh deimos.wntrmute.net "sudo cp /etc/letsencrypt/live/pad.metacircular.net/{fullchain,privkey}.pem \
|
||||||
|
/srv/eng-pad-server/certs/ && docker restart eng-pad-server"
|
||||||
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — full system specification
|
- [ARCHITECTURE.md](ARCHITECTURE.md) — full system specification
|
||||||
|
|||||||
153
RUNBOOK.md
153
RUNBOOK.md
@@ -6,31 +6,29 @@ eng-pad-server receives engineering notebook data from the Engineering
|
|||||||
Pad Android app via gRPC, stores it in SQLite, and serves read-only
|
Pad Android app via gRPC, stores it in SQLite, and serves read-only
|
||||||
views through a web UI. Single authenticated user.
|
views through a web UI. Single authenticated user.
|
||||||
|
|
||||||
**Ports**: 8443 (REST/HTTPS), 9443 (gRPC/TLS), 8080 (Web UI)
|
**Host**: deimos.wntrmute.net
|
||||||
|
**URL**: https://pad.metacircular.net
|
||||||
|
**Ports**: 443 (nginx → 8080 web UI), 8443 (REST/TLS), 9443 (gRPC/TLS)
|
||||||
**Data**: `/srv/eng-pad-server/`
|
**Data**: `/srv/eng-pad-server/`
|
||||||
**Config**: `/srv/eng-pad-server/eng-pad-server.toml`
|
**Config**: `/srv/eng-pad-server/eng-pad-server.toml`
|
||||||
**Binary**: `/usr/local/bin/eng-pad-server`
|
**TLS**: Let's Encrypt (`/etc/letsencrypt/live/pad.metacircular.net/`), copied to `/srv/eng-pad-server/certs/`
|
||||||
|
**Container**: `eng-pad-server` (Docker, `--restart unless-stopped`)
|
||||||
|
|
||||||
## 2. Health Checks
|
## 2. Health Checks
|
||||||
|
|
||||||
1. Check service is running:
|
1. Check container is running:
|
||||||
```
|
```
|
||||||
systemctl status eng-pad-server
|
docker ps | grep eng-pad-server
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Check database health:
|
2. Check web UI responds:
|
||||||
```
|
```
|
||||||
eng-pad-server status -c /srv/eng-pad-server/eng-pad-server.toml
|
curl -s https://pad.metacircular.net/login | head -1
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Check web UI responds:
|
3. Check container logs:
|
||||||
```
|
```
|
||||||
curl -k https://localhost:8443/login
|
docker logs eng-pad-server --tail 20
|
||||||
```
|
|
||||||
|
|
||||||
4. Check gRPC responds:
|
|
||||||
```
|
|
||||||
grpcurl -insecure localhost:9443 list
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Common Operations
|
## 3. Common Operations
|
||||||
@@ -38,89 +36,74 @@ views through a web UI. Single authenticated user.
|
|||||||
### Start / Stop / Restart
|
### Start / Stop / Restart
|
||||||
|
|
||||||
```
|
```
|
||||||
systemctl start eng-pad-server
|
docker start eng-pad-server
|
||||||
systemctl stop eng-pad-server
|
docker stop eng-pad-server
|
||||||
systemctl restart eng-pad-server
|
docker restart eng-pad-server
|
||||||
```
|
```
|
||||||
|
|
||||||
### View Logs
|
### View Logs
|
||||||
|
|
||||||
```
|
```
|
||||||
journalctl -u eng-pad-server -f
|
docker logs eng-pad-server -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy New Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From local machine:
|
||||||
|
rsync -az --exclude='.git' --exclude='srv/' . deimos.wntrmute.net:/tmp/eng-pad-server-build/
|
||||||
|
ssh deimos.wntrmute.net "cd /tmp/eng-pad-server-build && \
|
||||||
|
docker build -t eng-pad-server . && \
|
||||||
|
docker stop eng-pad-server && docker rm eng-pad-server && \
|
||||||
|
docker run -d --name eng-pad-server --restart unless-stopped \
|
||||||
|
-p 127.0.0.1:8090:8080 -p 8443:8443 -p 9443:9443 \
|
||||||
|
-v /srv/eng-pad-server:/srv/eng-pad-server eng-pad-server"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create User
|
||||||
|
|
||||||
|
```
|
||||||
|
docker exec -it eng-pad-server \
|
||||||
|
eng-pad-server init -c /srv/eng-pad-server/eng-pad-server.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset User Password
|
||||||
|
|
||||||
|
```
|
||||||
|
docker exec -it eng-pad-server \
|
||||||
|
eng-pad-server passwd <username> -c /srv/eng-pad-server/eng-pad-server.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Backup
|
### Manual Backup
|
||||||
|
|
||||||
```
|
```
|
||||||
eng-pad-server snapshot -c /srv/eng-pad-server/eng-pad-server.toml
|
docker exec eng-pad-server \
|
||||||
|
eng-pad-server snapshot -c /srv/eng-pad-server/eng-pad-server.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
Backup saved to `/srv/eng-pad-server/backups/`.
|
Backup saved to `/srv/eng-pad-server/backups/`.
|
||||||
|
|
||||||
### Check Backup Timer
|
### Renew TLS Certificates
|
||||||
|
|
||||||
|
After certbot renews the Let's Encrypt cert:
|
||||||
```
|
```
|
||||||
systemctl list-timers eng-pad-server-backup.timer
|
sudo cp /etc/letsencrypt/live/pad.metacircular.net/{fullchain,privkey}.pem \
|
||||||
|
/srv/eng-pad-server/certs/
|
||||||
|
docker restart eng-pad-server
|
||||||
```
|
```
|
||||||
|
|
||||||
### Initialize (First Time)
|
|
||||||
|
|
||||||
1. Install the binary and config:
|
|
||||||
```
|
|
||||||
sudo deploy/scripts/install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Edit the config file:
|
|
||||||
```
|
|
||||||
sudo -u engpad vi /srv/eng-pad-server/eng-pad-server.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Generate TLS certificates (or copy existing ones):
|
|
||||||
```
|
|
||||||
# Self-signed for development:
|
|
||||||
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
|
|
||||||
-keyout /srv/eng-pad-server/certs/key.pem \
|
|
||||||
-out /srv/eng-pad-server/certs/cert.pem \
|
|
||||||
-days 3650 -nodes -subj '/CN=pad.metacircular.net'
|
|
||||||
chown engpad:engpad /srv/eng-pad-server/certs/*.pem
|
|
||||||
chmod 600 /srv/eng-pad-server/certs/key.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Create the admin user:
|
|
||||||
```
|
|
||||||
eng-pad-server init -c /srv/eng-pad-server/eng-pad-server.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Start the service:
|
|
||||||
```
|
|
||||||
systemctl enable --now eng-pad-server
|
|
||||||
systemctl enable --now eng-pad-server-backup.timer
|
|
||||||
```
|
|
||||||
|
|
||||||
### Register a FIDO2/U2F Security Key
|
### Register a FIDO2/U2F Security Key
|
||||||
|
|
||||||
1. Log in to the web UI with password.
|
1. Log in to the web UI at https://pad.metacircular.net with password.
|
||||||
2. Navigate to `/keys`.
|
2. Navigate to `/keys`.
|
||||||
3. Enter a name for the key (e.g., "YubiKey 5").
|
3. Enter a name for the key (e.g., "YubiKey 5").
|
||||||
4. Click "Register" and touch the key when prompted.
|
4. Click "Register" and touch the key when prompted.
|
||||||
|
|
||||||
### Docker Deployment
|
|
||||||
|
|
||||||
```
|
|
||||||
cd deploy/docker
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
First-time setup inside the container:
|
|
||||||
```
|
|
||||||
docker compose exec eng-pad-server eng-pad-server init -c /srv/eng-pad-server/eng-pad-server.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Alerting
|
## 4. Alerting
|
||||||
|
|
||||||
No automated alerting is configured. Monitor via:
|
No automated alerting is configured. Monitor via:
|
||||||
- `systemctl status eng-pad-server` — process health
|
- `docker ps | grep eng-pad-server` — container health
|
||||||
- `journalctl -u eng-pad-server --since "1 hour ago" | grep ERROR` — errors
|
- `docker logs eng-pad-server --since 1h 2>&1 | grep ERROR` — errors
|
||||||
- Backup age: `ls -lt /srv/eng-pad-server/backups/ | head`
|
- Backup age: `ls -lt /srv/eng-pad-server/backups/ | head`
|
||||||
|
|
||||||
## 5. Incident Procedures
|
## 5. Incident Procedures
|
||||||
@@ -129,19 +112,19 @@ No automated alerting is configured. Monitor via:
|
|||||||
|
|
||||||
1. Check logs:
|
1. Check logs:
|
||||||
```
|
```
|
||||||
journalctl -u eng-pad-server -n 50 --no-pager
|
docker logs eng-pad-server --tail 50
|
||||||
```
|
```
|
||||||
2. Common causes:
|
2. Common causes:
|
||||||
- Config file missing or invalid → fix config
|
- Config file missing or invalid → fix `/srv/eng-pad-server/eng-pad-server.toml`
|
||||||
- TLS cert/key missing → regenerate or copy
|
- TLS cert/key missing → re-copy from Let's Encrypt (see Renew TLS above)
|
||||||
- Port already in use → `ss -tlnp | grep 8443`
|
- Port already in use → `ss -tlnp | grep -E '8443|9443|8090'`
|
||||||
- Database locked → check for zombie processes: `fuser /srv/eng-pad-server/eng-pad-server.db`
|
- Database locked → check for zombie processes: `fuser /srv/eng-pad-server/eng-pad-server.db`
|
||||||
|
|
||||||
### Database Corruption
|
### Database Corruption
|
||||||
|
|
||||||
1. Stop the service:
|
1. Stop the container:
|
||||||
```
|
```
|
||||||
systemctl stop eng-pad-server
|
docker stop eng-pad-server
|
||||||
```
|
```
|
||||||
2. Check integrity:
|
2. Check integrity:
|
||||||
```
|
```
|
||||||
@@ -150,21 +133,20 @@ No automated alerting is configured. Monitor via:
|
|||||||
3. If corrupted, restore from backup:
|
3. If corrupted, restore from backup:
|
||||||
```
|
```
|
||||||
cp /srv/eng-pad-server/backups/eng-pad-server-LATEST.db /srv/eng-pad-server/eng-pad-server.db
|
cp /srv/eng-pad-server/backups/eng-pad-server-LATEST.db /srv/eng-pad-server/eng-pad-server.db
|
||||||
chown engpad:engpad /srv/eng-pad-server/eng-pad-server.db
|
|
||||||
```
|
```
|
||||||
4. Restart:
|
4. Restart:
|
||||||
```
|
```
|
||||||
systemctl start eng-pad-server
|
docker start eng-pad-server
|
||||||
```
|
```
|
||||||
|
|
||||||
### Certificate Expiry
|
### Certificate Expiry
|
||||||
|
|
||||||
1. Check expiry:
|
1. Check expiry:
|
||||||
```
|
```
|
||||||
openssl x509 -in /srv/eng-pad-server/certs/cert.pem -noout -dates
|
openssl x509 -in /srv/eng-pad-server/certs/fullchain.pem -noout -dates
|
||||||
```
|
```
|
||||||
2. Regenerate or renew the certificate.
|
2. Renew via certbot (see "Renew TLS Certificates" above).
|
||||||
3. Restart the service (picks up new certs on start).
|
3. Restart the container (picks up new certs on start).
|
||||||
|
|
||||||
### Disk Full
|
### Disk Full
|
||||||
|
|
||||||
@@ -184,11 +166,12 @@ No automated alerting is configured. Monitor via:
|
|||||||
|
|
||||||
### Sync Fails from Android App
|
### Sync Fails from Android App
|
||||||
|
|
||||||
1. Verify server is reachable from the device's network.
|
1. Verify the app has the correct server URL (`pad.metacircular.net:9443`).
|
||||||
2. Check gRPC port is open: `ss -tlnp | grep 9443`
|
2. Use "Test Connection" in the app's sync settings for a specific error.
|
||||||
3. Check TLS cert is valid and trusted by the device.
|
3. Check gRPC port is open: `ss -tlnp | grep 9443`
|
||||||
4. Check credentials: verify the user exists via `eng-pad-server status`.
|
4. Check firewall: `sudo ufw status | grep 9443` (must be ALLOW).
|
||||||
5. Check server logs for auth failures: `journalctl -u eng-pad-server | grep UNAUTHENTICATED`
|
5. Check TLS cert is valid: `openssl x509 -in /srv/eng-pad-server/certs/fullchain.pem -noout -dates`
|
||||||
|
6. Check server logs for auth failures: `docker logs eng-pad-server 2>&1 | grep -i error`
|
||||||
|
|
||||||
## 6. Escalation
|
## 6. Escalation
|
||||||
|
|
||||||
|
|||||||
77
cmd/eng-pad-server/passwd.go
Normal file
77
cmd/eng-pad-server/passwd.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/eng-pad-server/internal/auth"
|
||||||
|
"git.wntrmute.dev/kyle/eng-pad-server/internal/config"
|
||||||
|
"git.wntrmute.dev/kyle/eng-pad-server/internal/db"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
var passwdCmd = &cobra.Command{
|
||||||
|
Use: "passwd <username>",
|
||||||
|
Short: "Set password for a user",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runPasswd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(passwdCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPasswd(cmd *cobra.Command, args []string) error {
|
||||||
|
username := args[0]
|
||||||
|
|
||||||
|
cfg, err := config.Load(cfgFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
database, err := db.Open(cfg.Database.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = database.Close() }()
|
||||||
|
|
||||||
|
// Verify user exists.
|
||||||
|
var userID int64
|
||||||
|
err = database.QueryRow("SELECT id FROM users WHERE username = ?", username).Scan(&userID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("user %q not found", username)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("New password: ")
|
||||||
|
passBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||||
|
fmt.Println()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
password := strings.TrimSpace(string(passBytes))
|
||||||
|
if password == "" {
|
||||||
|
return fmt.Errorf("password cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
params := auth.Argon2Params{
|
||||||
|
Memory: cfg.Auth.Argon2Memory,
|
||||||
|
Time: cfg.Auth.Argon2Time,
|
||||||
|
Threads: cfg.Auth.Argon2Threads,
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := auth.HashPassword(password, params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("hash password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = database.Exec("UPDATE users SET password_hash = ?, updated_at = unixepoch() WHERE id = ?", hash, userID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Password updated for %q.\n", username)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ func runServer(cmd *cobra.Command, args []string) error {
|
|||||||
// Start gRPC server
|
// Start gRPC server
|
||||||
grpcSrv, err := grpcserver.Start(grpcserver.Config{
|
grpcSrv, err := grpcserver.Start(grpcserver.Config{
|
||||||
Addr: cfg.Server.GRPCAddr,
|
Addr: cfg.Server.GRPCAddr,
|
||||||
|
PlainAddr: cfg.Server.GRPCPlainAddr,
|
||||||
TLSCert: cfg.Server.TLSCert,
|
TLSCert: cfg.Server.TLSCert,
|
||||||
TLSKey: cfg.Server.TLSKey,
|
TLSKey: cfg.Server.TLSKey,
|
||||||
DB: database,
|
DB: database,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
[server]
|
[server]
|
||||||
listen_addr = ":8443"
|
listen_addr = ":8443"
|
||||||
grpc_addr = ":9443"
|
grpc_addr = ":9443"
|
||||||
tls_cert = "/srv/eng-pad-server/certs/cert.pem"
|
# grpc_plain_addr = "127.0.0.1:9444" # Optional: plaintext gRPC for reverse proxy
|
||||||
tls_key = "/srv/eng-pad-server/certs/key.pem"
|
tls_cert = "/srv/eng-pad-server/certs/fullchain.pem"
|
||||||
|
tls_key = "/srv/eng-pad-server/certs/privkey.pem"
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
listen_addr = ":8080"
|
listen_addr = ":8080"
|
||||||
|
|||||||
@@ -278,6 +278,134 @@ func (x *SyncNotebookResponse) GetSyncedAt() *timestamppb.Timestamp {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetNotebookRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
NotebookId int64 `protobuf:"varint,1,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` // Server-side notebook ID
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookRequest) Reset() {
|
||||||
|
*x = GetNotebookRequest{}
|
||||||
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetNotebookRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *GetNotebookRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[4]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use GetNotebookRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*GetNotebookRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookRequest) GetNotebookId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.NotebookId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetNotebookResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
ServerNotebookId int64 `protobuf:"varint,1,opt,name=server_notebook_id,json=serverNotebookId,proto3" json:"server_notebook_id,omitempty"`
|
||||||
|
RemoteId int64 `protobuf:"varint,2,opt,name=remote_id,json=remoteId,proto3" json:"remote_id,omitempty"`
|
||||||
|
Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"`
|
||||||
|
PageSize string `protobuf:"bytes,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
|
||||||
|
Pages []*PageData `protobuf:"bytes,5,rep,name=pages,proto3" json:"pages,omitempty"`
|
||||||
|
SyncedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=synced_at,json=syncedAt,proto3" json:"synced_at,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookResponse) Reset() {
|
||||||
|
*x = GetNotebookResponse{}
|
||||||
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetNotebookResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *GetNotebookResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[5]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use GetNotebookResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*GetNotebookResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookResponse) GetServerNotebookId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ServerNotebookId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookResponse) GetRemoteId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.RemoteId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookResponse) GetTitle() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Title
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookResponse) GetPageSize() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PageSize
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookResponse) GetPages() []*PageData {
|
||||||
|
if x != nil {
|
||||||
|
return x.Pages
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetNotebookResponse) GetSyncedAt() *timestamppb.Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.SyncedAt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteNotebookRequest struct {
|
type DeleteNotebookRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
NotebookId int64 `protobuf:"varint,1,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"`
|
NotebookId int64 `protobuf:"varint,1,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"`
|
||||||
@@ -287,7 +415,7 @@ type DeleteNotebookRequest struct {
|
|||||||
|
|
||||||
func (x *DeleteNotebookRequest) Reset() {
|
func (x *DeleteNotebookRequest) Reset() {
|
||||||
*x = DeleteNotebookRequest{}
|
*x = DeleteNotebookRequest{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[4]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[6]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -299,7 +427,7 @@ func (x *DeleteNotebookRequest) String() string {
|
|||||||
func (*DeleteNotebookRequest) ProtoMessage() {}
|
func (*DeleteNotebookRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *DeleteNotebookRequest) ProtoReflect() protoreflect.Message {
|
func (x *DeleteNotebookRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[4]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[6]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -312,7 +440,7 @@ func (x *DeleteNotebookRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use DeleteNotebookRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use DeleteNotebookRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*DeleteNotebookRequest) Descriptor() ([]byte, []int) {
|
func (*DeleteNotebookRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{4}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{6}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DeleteNotebookRequest) GetNotebookId() int64 {
|
func (x *DeleteNotebookRequest) GetNotebookId() int64 {
|
||||||
@@ -330,7 +458,7 @@ type DeleteNotebookResponse struct {
|
|||||||
|
|
||||||
func (x *DeleteNotebookResponse) Reset() {
|
func (x *DeleteNotebookResponse) Reset() {
|
||||||
*x = DeleteNotebookResponse{}
|
*x = DeleteNotebookResponse{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[5]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[7]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -342,7 +470,7 @@ func (x *DeleteNotebookResponse) String() string {
|
|||||||
func (*DeleteNotebookResponse) ProtoMessage() {}
|
func (*DeleteNotebookResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *DeleteNotebookResponse) ProtoReflect() protoreflect.Message {
|
func (x *DeleteNotebookResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[5]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[7]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -355,7 +483,7 @@ func (x *DeleteNotebookResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use DeleteNotebookResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use DeleteNotebookResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*DeleteNotebookResponse) Descriptor() ([]byte, []int) {
|
func (*DeleteNotebookResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{5}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{7}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListNotebooksRequest struct {
|
type ListNotebooksRequest struct {
|
||||||
@@ -366,7 +494,7 @@ type ListNotebooksRequest struct {
|
|||||||
|
|
||||||
func (x *ListNotebooksRequest) Reset() {
|
func (x *ListNotebooksRequest) Reset() {
|
||||||
*x = ListNotebooksRequest{}
|
*x = ListNotebooksRequest{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[6]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[8]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -378,7 +506,7 @@ func (x *ListNotebooksRequest) String() string {
|
|||||||
func (*ListNotebooksRequest) ProtoMessage() {}
|
func (*ListNotebooksRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListNotebooksRequest) ProtoReflect() protoreflect.Message {
|
func (x *ListNotebooksRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[6]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[8]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -391,7 +519,7 @@ func (x *ListNotebooksRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListNotebooksRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListNotebooksRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ListNotebooksRequest) Descriptor() ([]byte, []int) {
|
func (*ListNotebooksRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{6}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListNotebooksResponse struct {
|
type ListNotebooksResponse struct {
|
||||||
@@ -403,7 +531,7 @@ type ListNotebooksResponse struct {
|
|||||||
|
|
||||||
func (x *ListNotebooksResponse) Reset() {
|
func (x *ListNotebooksResponse) Reset() {
|
||||||
*x = ListNotebooksResponse{}
|
*x = ListNotebooksResponse{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[7]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -415,7 +543,7 @@ func (x *ListNotebooksResponse) String() string {
|
|||||||
func (*ListNotebooksResponse) ProtoMessage() {}
|
func (*ListNotebooksResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListNotebooksResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListNotebooksResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[7]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[9]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -428,7 +556,7 @@ func (x *ListNotebooksResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListNotebooksResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListNotebooksResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ListNotebooksResponse) Descriptor() ([]byte, []int) {
|
func (*ListNotebooksResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{7}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListNotebooksResponse) GetNotebooks() []*NotebookSummary {
|
func (x *ListNotebooksResponse) GetNotebooks() []*NotebookSummary {
|
||||||
@@ -452,7 +580,7 @@ type NotebookSummary struct {
|
|||||||
|
|
||||||
func (x *NotebookSummary) Reset() {
|
func (x *NotebookSummary) Reset() {
|
||||||
*x = NotebookSummary{}
|
*x = NotebookSummary{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[8]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[10]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -464,7 +592,7 @@ func (x *NotebookSummary) String() string {
|
|||||||
func (*NotebookSummary) ProtoMessage() {}
|
func (*NotebookSummary) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *NotebookSummary) ProtoReflect() protoreflect.Message {
|
func (x *NotebookSummary) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[8]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[10]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -477,7 +605,7 @@ func (x *NotebookSummary) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use NotebookSummary.ProtoReflect.Descriptor instead.
|
// Deprecated: Use NotebookSummary.ProtoReflect.Descriptor instead.
|
||||||
func (*NotebookSummary) Descriptor() ([]byte, []int) {
|
func (*NotebookSummary) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{8}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *NotebookSummary) GetServerId() int64 {
|
func (x *NotebookSummary) GetServerId() int64 {
|
||||||
@@ -532,7 +660,7 @@ type CreateShareLinkRequest struct {
|
|||||||
|
|
||||||
func (x *CreateShareLinkRequest) Reset() {
|
func (x *CreateShareLinkRequest) Reset() {
|
||||||
*x = CreateShareLinkRequest{}
|
*x = CreateShareLinkRequest{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[9]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -544,7 +672,7 @@ func (x *CreateShareLinkRequest) String() string {
|
|||||||
func (*CreateShareLinkRequest) ProtoMessage() {}
|
func (*CreateShareLinkRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *CreateShareLinkRequest) ProtoReflect() protoreflect.Message {
|
func (x *CreateShareLinkRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[9]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[11]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -557,7 +685,7 @@ func (x *CreateShareLinkRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use CreateShareLinkRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use CreateShareLinkRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*CreateShareLinkRequest) Descriptor() ([]byte, []int) {
|
func (*CreateShareLinkRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{9}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateShareLinkRequest) GetNotebookId() int64 {
|
func (x *CreateShareLinkRequest) GetNotebookId() int64 {
|
||||||
@@ -585,7 +713,7 @@ type CreateShareLinkResponse struct {
|
|||||||
|
|
||||||
func (x *CreateShareLinkResponse) Reset() {
|
func (x *CreateShareLinkResponse) Reset() {
|
||||||
*x = CreateShareLinkResponse{}
|
*x = CreateShareLinkResponse{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[10]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[12]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -597,7 +725,7 @@ func (x *CreateShareLinkResponse) String() string {
|
|||||||
func (*CreateShareLinkResponse) ProtoMessage() {}
|
func (*CreateShareLinkResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *CreateShareLinkResponse) ProtoReflect() protoreflect.Message {
|
func (x *CreateShareLinkResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[10]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[12]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -610,7 +738,7 @@ func (x *CreateShareLinkResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use CreateShareLinkResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use CreateShareLinkResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*CreateShareLinkResponse) Descriptor() ([]byte, []int) {
|
func (*CreateShareLinkResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{10}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{12}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateShareLinkResponse) GetToken() string {
|
func (x *CreateShareLinkResponse) GetToken() string {
|
||||||
@@ -643,7 +771,7 @@ type RevokeShareLinkRequest struct {
|
|||||||
|
|
||||||
func (x *RevokeShareLinkRequest) Reset() {
|
func (x *RevokeShareLinkRequest) Reset() {
|
||||||
*x = RevokeShareLinkRequest{}
|
*x = RevokeShareLinkRequest{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[11]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[13]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -655,7 +783,7 @@ func (x *RevokeShareLinkRequest) String() string {
|
|||||||
func (*RevokeShareLinkRequest) ProtoMessage() {}
|
func (*RevokeShareLinkRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RevokeShareLinkRequest) ProtoReflect() protoreflect.Message {
|
func (x *RevokeShareLinkRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[11]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[13]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -668,7 +796,7 @@ func (x *RevokeShareLinkRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RevokeShareLinkRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RevokeShareLinkRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*RevokeShareLinkRequest) Descriptor() ([]byte, []int) {
|
func (*RevokeShareLinkRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{11}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{13}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RevokeShareLinkRequest) GetToken() string {
|
func (x *RevokeShareLinkRequest) GetToken() string {
|
||||||
@@ -686,7 +814,7 @@ type RevokeShareLinkResponse struct {
|
|||||||
|
|
||||||
func (x *RevokeShareLinkResponse) Reset() {
|
func (x *RevokeShareLinkResponse) Reset() {
|
||||||
*x = RevokeShareLinkResponse{}
|
*x = RevokeShareLinkResponse{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[12]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[14]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -698,7 +826,7 @@ func (x *RevokeShareLinkResponse) String() string {
|
|||||||
func (*RevokeShareLinkResponse) ProtoMessage() {}
|
func (*RevokeShareLinkResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RevokeShareLinkResponse) ProtoReflect() protoreflect.Message {
|
func (x *RevokeShareLinkResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[12]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[14]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -711,7 +839,7 @@ func (x *RevokeShareLinkResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RevokeShareLinkResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RevokeShareLinkResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*RevokeShareLinkResponse) Descriptor() ([]byte, []int) {
|
func (*RevokeShareLinkResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{12}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{14}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListShareLinksRequest struct {
|
type ListShareLinksRequest struct {
|
||||||
@@ -723,7 +851,7 @@ type ListShareLinksRequest struct {
|
|||||||
|
|
||||||
func (x *ListShareLinksRequest) Reset() {
|
func (x *ListShareLinksRequest) Reset() {
|
||||||
*x = ListShareLinksRequest{}
|
*x = ListShareLinksRequest{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[13]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[15]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -735,7 +863,7 @@ func (x *ListShareLinksRequest) String() string {
|
|||||||
func (*ListShareLinksRequest) ProtoMessage() {}
|
func (*ListShareLinksRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListShareLinksRequest) ProtoReflect() protoreflect.Message {
|
func (x *ListShareLinksRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[13]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[15]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -748,7 +876,7 @@ func (x *ListShareLinksRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListShareLinksRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListShareLinksRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ListShareLinksRequest) Descriptor() ([]byte, []int) {
|
func (*ListShareLinksRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{13}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{15}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListShareLinksRequest) GetNotebookId() int64 {
|
func (x *ListShareLinksRequest) GetNotebookId() int64 {
|
||||||
@@ -767,7 +895,7 @@ type ListShareLinksResponse struct {
|
|||||||
|
|
||||||
func (x *ListShareLinksResponse) Reset() {
|
func (x *ListShareLinksResponse) Reset() {
|
||||||
*x = ListShareLinksResponse{}
|
*x = ListShareLinksResponse{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[14]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[16]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -779,7 +907,7 @@ func (x *ListShareLinksResponse) String() string {
|
|||||||
func (*ListShareLinksResponse) ProtoMessage() {}
|
func (*ListShareLinksResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListShareLinksResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListShareLinksResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[14]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[16]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -792,7 +920,7 @@ func (x *ListShareLinksResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListShareLinksResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListShareLinksResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ListShareLinksResponse) Descriptor() ([]byte, []int) {
|
func (*ListShareLinksResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{14}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{16}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListShareLinksResponse) GetLinks() []*ShareLinkInfo {
|
func (x *ListShareLinksResponse) GetLinks() []*ShareLinkInfo {
|
||||||
@@ -814,7 +942,7 @@ type ShareLinkInfo struct {
|
|||||||
|
|
||||||
func (x *ShareLinkInfo) Reset() {
|
func (x *ShareLinkInfo) Reset() {
|
||||||
*x = ShareLinkInfo{}
|
*x = ShareLinkInfo{}
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[15]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[17]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -826,7 +954,7 @@ func (x *ShareLinkInfo) String() string {
|
|||||||
func (*ShareLinkInfo) ProtoMessage() {}
|
func (*ShareLinkInfo) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ShareLinkInfo) ProtoReflect() protoreflect.Message {
|
func (x *ShareLinkInfo) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_engpad_v1_sync_proto_msgTypes[15]
|
mi := &file_proto_engpad_v1_sync_proto_msgTypes[17]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -839,7 +967,7 @@ func (x *ShareLinkInfo) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ShareLinkInfo.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ShareLinkInfo.ProtoReflect.Descriptor instead.
|
||||||
func (*ShareLinkInfo) Descriptor() ([]byte, []int) {
|
func (*ShareLinkInfo) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{15}
|
return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{17}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ShareLinkInfo) GetToken() string {
|
func (x *ShareLinkInfo) GetToken() string {
|
||||||
@@ -896,7 +1024,17 @@ const file_proto_engpad_v1_sync_proto_rawDesc = "" +
|
|||||||
"\fstroke_order\x18\x05 \x01(\x05R\vstrokeOrder\"}\n" +
|
"\fstroke_order\x18\x05 \x01(\x05R\vstrokeOrder\"}\n" +
|
||||||
"\x14SyncNotebookResponse\x12,\n" +
|
"\x14SyncNotebookResponse\x12,\n" +
|
||||||
"\x12server_notebook_id\x18\x01 \x01(\x03R\x10serverNotebookId\x127\n" +
|
"\x12server_notebook_id\x18\x01 \x01(\x03R\x10serverNotebookId\x127\n" +
|
||||||
"\tsynced_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\bsyncedAt\"8\n" +
|
"\tsynced_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\bsyncedAt\"5\n" +
|
||||||
|
"\x12GetNotebookRequest\x12\x1f\n" +
|
||||||
|
"\vnotebook_id\x18\x01 \x01(\x03R\n" +
|
||||||
|
"notebookId\"\xf7\x01\n" +
|
||||||
|
"\x13GetNotebookResponse\x12,\n" +
|
||||||
|
"\x12server_notebook_id\x18\x01 \x01(\x03R\x10serverNotebookId\x12\x1b\n" +
|
||||||
|
"\tremote_id\x18\x02 \x01(\x03R\bremoteId\x12\x14\n" +
|
||||||
|
"\x05title\x18\x03 \x01(\tR\x05title\x12\x1b\n" +
|
||||||
|
"\tpage_size\x18\x04 \x01(\tR\bpageSize\x12)\n" +
|
||||||
|
"\x05pages\x18\x05 \x03(\v2\x13.engpad.v1.PageDataR\x05pages\x127\n" +
|
||||||
|
"\tsynced_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\bsyncedAt\"8\n" +
|
||||||
"\x15DeleteNotebookRequest\x12\x1f\n" +
|
"\x15DeleteNotebookRequest\x12\x1f\n" +
|
||||||
"\vnotebook_id\x18\x01 \x01(\x03R\n" +
|
"\vnotebook_id\x18\x01 \x01(\x03R\n" +
|
||||||
"notebookId\"\x18\n" +
|
"notebookId\"\x18\n" +
|
||||||
@@ -935,9 +1073,10 @@ const file_proto_engpad_v1_sync_proto_rawDesc = "" +
|
|||||||
"\n" +
|
"\n" +
|
||||||
"created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
|
"created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt2\x9a\x04\n" +
|
"expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt2\xe8\x04\n" +
|
||||||
"\x11EngPadSyncService\x12O\n" +
|
"\x11EngPadSyncService\x12O\n" +
|
||||||
"\fSyncNotebook\x12\x1e.engpad.v1.SyncNotebookRequest\x1a\x1f.engpad.v1.SyncNotebookResponse\x12U\n" +
|
"\fSyncNotebook\x12\x1e.engpad.v1.SyncNotebookRequest\x1a\x1f.engpad.v1.SyncNotebookResponse\x12L\n" +
|
||||||
|
"\vGetNotebook\x12\x1d.engpad.v1.GetNotebookRequest\x1a\x1e.engpad.v1.GetNotebookResponse\x12U\n" +
|
||||||
"\x0eDeleteNotebook\x12 .engpad.v1.DeleteNotebookRequest\x1a!.engpad.v1.DeleteNotebookResponse\x12R\n" +
|
"\x0eDeleteNotebook\x12 .engpad.v1.DeleteNotebookRequest\x1a!.engpad.v1.DeleteNotebookResponse\x12R\n" +
|
||||||
"\rListNotebooks\x12\x1f.engpad.v1.ListNotebooksRequest\x1a .engpad.v1.ListNotebooksResponse\x12X\n" +
|
"\rListNotebooks\x12\x1f.engpad.v1.ListNotebooksRequest\x1a .engpad.v1.ListNotebooksResponse\x12X\n" +
|
||||||
"\x0fCreateShareLink\x12!.engpad.v1.CreateShareLinkRequest\x1a\".engpad.v1.CreateShareLinkResponse\x12X\n" +
|
"\x0fCreateShareLink\x12!.engpad.v1.CreateShareLinkRequest\x1a\".engpad.v1.CreateShareLinkResponse\x12X\n" +
|
||||||
@@ -957,53 +1096,59 @@ func file_proto_engpad_v1_sync_proto_rawDescGZIP() []byte {
|
|||||||
return file_proto_engpad_v1_sync_proto_rawDescData
|
return file_proto_engpad_v1_sync_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_proto_engpad_v1_sync_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
|
var file_proto_engpad_v1_sync_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
|
||||||
var file_proto_engpad_v1_sync_proto_goTypes = []any{
|
var file_proto_engpad_v1_sync_proto_goTypes = []any{
|
||||||
(*SyncNotebookRequest)(nil), // 0: engpad.v1.SyncNotebookRequest
|
(*SyncNotebookRequest)(nil), // 0: engpad.v1.SyncNotebookRequest
|
||||||
(*PageData)(nil), // 1: engpad.v1.PageData
|
(*PageData)(nil), // 1: engpad.v1.PageData
|
||||||
(*StrokeData)(nil), // 2: engpad.v1.StrokeData
|
(*StrokeData)(nil), // 2: engpad.v1.StrokeData
|
||||||
(*SyncNotebookResponse)(nil), // 3: engpad.v1.SyncNotebookResponse
|
(*SyncNotebookResponse)(nil), // 3: engpad.v1.SyncNotebookResponse
|
||||||
(*DeleteNotebookRequest)(nil), // 4: engpad.v1.DeleteNotebookRequest
|
(*GetNotebookRequest)(nil), // 4: engpad.v1.GetNotebookRequest
|
||||||
(*DeleteNotebookResponse)(nil), // 5: engpad.v1.DeleteNotebookResponse
|
(*GetNotebookResponse)(nil), // 5: engpad.v1.GetNotebookResponse
|
||||||
(*ListNotebooksRequest)(nil), // 6: engpad.v1.ListNotebooksRequest
|
(*DeleteNotebookRequest)(nil), // 6: engpad.v1.DeleteNotebookRequest
|
||||||
(*ListNotebooksResponse)(nil), // 7: engpad.v1.ListNotebooksResponse
|
(*DeleteNotebookResponse)(nil), // 7: engpad.v1.DeleteNotebookResponse
|
||||||
(*NotebookSummary)(nil), // 8: engpad.v1.NotebookSummary
|
(*ListNotebooksRequest)(nil), // 8: engpad.v1.ListNotebooksRequest
|
||||||
(*CreateShareLinkRequest)(nil), // 9: engpad.v1.CreateShareLinkRequest
|
(*ListNotebooksResponse)(nil), // 9: engpad.v1.ListNotebooksResponse
|
||||||
(*CreateShareLinkResponse)(nil), // 10: engpad.v1.CreateShareLinkResponse
|
(*NotebookSummary)(nil), // 10: engpad.v1.NotebookSummary
|
||||||
(*RevokeShareLinkRequest)(nil), // 11: engpad.v1.RevokeShareLinkRequest
|
(*CreateShareLinkRequest)(nil), // 11: engpad.v1.CreateShareLinkRequest
|
||||||
(*RevokeShareLinkResponse)(nil), // 12: engpad.v1.RevokeShareLinkResponse
|
(*CreateShareLinkResponse)(nil), // 12: engpad.v1.CreateShareLinkResponse
|
||||||
(*ListShareLinksRequest)(nil), // 13: engpad.v1.ListShareLinksRequest
|
(*RevokeShareLinkRequest)(nil), // 13: engpad.v1.RevokeShareLinkRequest
|
||||||
(*ListShareLinksResponse)(nil), // 14: engpad.v1.ListShareLinksResponse
|
(*RevokeShareLinkResponse)(nil), // 14: engpad.v1.RevokeShareLinkResponse
|
||||||
(*ShareLinkInfo)(nil), // 15: engpad.v1.ShareLinkInfo
|
(*ListShareLinksRequest)(nil), // 15: engpad.v1.ListShareLinksRequest
|
||||||
(*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp
|
(*ListShareLinksResponse)(nil), // 16: engpad.v1.ListShareLinksResponse
|
||||||
|
(*ShareLinkInfo)(nil), // 17: engpad.v1.ShareLinkInfo
|
||||||
|
(*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp
|
||||||
}
|
}
|
||||||
var file_proto_engpad_v1_sync_proto_depIdxs = []int32{
|
var file_proto_engpad_v1_sync_proto_depIdxs = []int32{
|
||||||
1, // 0: engpad.v1.SyncNotebookRequest.pages:type_name -> engpad.v1.PageData
|
1, // 0: engpad.v1.SyncNotebookRequest.pages:type_name -> engpad.v1.PageData
|
||||||
2, // 1: engpad.v1.PageData.strokes:type_name -> engpad.v1.StrokeData
|
2, // 1: engpad.v1.PageData.strokes:type_name -> engpad.v1.StrokeData
|
||||||
16, // 2: engpad.v1.SyncNotebookResponse.synced_at:type_name -> google.protobuf.Timestamp
|
18, // 2: engpad.v1.SyncNotebookResponse.synced_at:type_name -> google.protobuf.Timestamp
|
||||||
8, // 3: engpad.v1.ListNotebooksResponse.notebooks:type_name -> engpad.v1.NotebookSummary
|
1, // 3: engpad.v1.GetNotebookResponse.pages:type_name -> engpad.v1.PageData
|
||||||
16, // 4: engpad.v1.NotebookSummary.synced_at:type_name -> google.protobuf.Timestamp
|
18, // 4: engpad.v1.GetNotebookResponse.synced_at:type_name -> google.protobuf.Timestamp
|
||||||
16, // 5: engpad.v1.CreateShareLinkResponse.expires_at:type_name -> google.protobuf.Timestamp
|
10, // 5: engpad.v1.ListNotebooksResponse.notebooks:type_name -> engpad.v1.NotebookSummary
|
||||||
15, // 6: engpad.v1.ListShareLinksResponse.links:type_name -> engpad.v1.ShareLinkInfo
|
18, // 6: engpad.v1.NotebookSummary.synced_at:type_name -> google.protobuf.Timestamp
|
||||||
16, // 7: engpad.v1.ShareLinkInfo.created_at:type_name -> google.protobuf.Timestamp
|
18, // 7: engpad.v1.CreateShareLinkResponse.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
16, // 8: engpad.v1.ShareLinkInfo.expires_at:type_name -> google.protobuf.Timestamp
|
17, // 8: engpad.v1.ListShareLinksResponse.links:type_name -> engpad.v1.ShareLinkInfo
|
||||||
0, // 9: engpad.v1.EngPadSyncService.SyncNotebook:input_type -> engpad.v1.SyncNotebookRequest
|
18, // 9: engpad.v1.ShareLinkInfo.created_at:type_name -> google.protobuf.Timestamp
|
||||||
4, // 10: engpad.v1.EngPadSyncService.DeleteNotebook:input_type -> engpad.v1.DeleteNotebookRequest
|
18, // 10: engpad.v1.ShareLinkInfo.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
6, // 11: engpad.v1.EngPadSyncService.ListNotebooks:input_type -> engpad.v1.ListNotebooksRequest
|
0, // 11: engpad.v1.EngPadSyncService.SyncNotebook:input_type -> engpad.v1.SyncNotebookRequest
|
||||||
9, // 12: engpad.v1.EngPadSyncService.CreateShareLink:input_type -> engpad.v1.CreateShareLinkRequest
|
4, // 12: engpad.v1.EngPadSyncService.GetNotebook:input_type -> engpad.v1.GetNotebookRequest
|
||||||
11, // 13: engpad.v1.EngPadSyncService.RevokeShareLink:input_type -> engpad.v1.RevokeShareLinkRequest
|
6, // 13: engpad.v1.EngPadSyncService.DeleteNotebook:input_type -> engpad.v1.DeleteNotebookRequest
|
||||||
13, // 14: engpad.v1.EngPadSyncService.ListShareLinks:input_type -> engpad.v1.ListShareLinksRequest
|
8, // 14: engpad.v1.EngPadSyncService.ListNotebooks:input_type -> engpad.v1.ListNotebooksRequest
|
||||||
3, // 15: engpad.v1.EngPadSyncService.SyncNotebook:output_type -> engpad.v1.SyncNotebookResponse
|
11, // 15: engpad.v1.EngPadSyncService.CreateShareLink:input_type -> engpad.v1.CreateShareLinkRequest
|
||||||
5, // 16: engpad.v1.EngPadSyncService.DeleteNotebook:output_type -> engpad.v1.DeleteNotebookResponse
|
13, // 16: engpad.v1.EngPadSyncService.RevokeShareLink:input_type -> engpad.v1.RevokeShareLinkRequest
|
||||||
7, // 17: engpad.v1.EngPadSyncService.ListNotebooks:output_type -> engpad.v1.ListNotebooksResponse
|
15, // 17: engpad.v1.EngPadSyncService.ListShareLinks:input_type -> engpad.v1.ListShareLinksRequest
|
||||||
10, // 18: engpad.v1.EngPadSyncService.CreateShareLink:output_type -> engpad.v1.CreateShareLinkResponse
|
3, // 18: engpad.v1.EngPadSyncService.SyncNotebook:output_type -> engpad.v1.SyncNotebookResponse
|
||||||
12, // 19: engpad.v1.EngPadSyncService.RevokeShareLink:output_type -> engpad.v1.RevokeShareLinkResponse
|
5, // 19: engpad.v1.EngPadSyncService.GetNotebook:output_type -> engpad.v1.GetNotebookResponse
|
||||||
14, // 20: engpad.v1.EngPadSyncService.ListShareLinks:output_type -> engpad.v1.ListShareLinksResponse
|
7, // 20: engpad.v1.EngPadSyncService.DeleteNotebook:output_type -> engpad.v1.DeleteNotebookResponse
|
||||||
15, // [15:21] is the sub-list for method output_type
|
9, // 21: engpad.v1.EngPadSyncService.ListNotebooks:output_type -> engpad.v1.ListNotebooksResponse
|
||||||
9, // [9:15] is the sub-list for method input_type
|
12, // 22: engpad.v1.EngPadSyncService.CreateShareLink:output_type -> engpad.v1.CreateShareLinkResponse
|
||||||
9, // [9:9] is the sub-list for extension type_name
|
14, // 23: engpad.v1.EngPadSyncService.RevokeShareLink:output_type -> engpad.v1.RevokeShareLinkResponse
|
||||||
9, // [9:9] is the sub-list for extension extendee
|
16, // 24: engpad.v1.EngPadSyncService.ListShareLinks:output_type -> engpad.v1.ListShareLinksResponse
|
||||||
0, // [0:9] is the sub-list for field type_name
|
18, // [18:25] is the sub-list for method output_type
|
||||||
|
11, // [11:18] is the sub-list for method input_type
|
||||||
|
11, // [11:11] is the sub-list for extension type_name
|
||||||
|
11, // [11:11] is the sub-list for extension extendee
|
||||||
|
0, // [0:11] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_proto_engpad_v1_sync_proto_init() }
|
func init() { file_proto_engpad_v1_sync_proto_init() }
|
||||||
@@ -1017,7 +1162,7 @@ func file_proto_engpad_v1_sync_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_engpad_v1_sync_proto_rawDesc), len(file_proto_engpad_v1_sync_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_engpad_v1_sync_proto_rawDesc), len(file_proto_engpad_v1_sync_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 16,
|
NumMessages: 18,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
EngPadSyncService_SyncNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/SyncNotebook"
|
EngPadSyncService_SyncNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/SyncNotebook"
|
||||||
|
EngPadSyncService_GetNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/GetNotebook"
|
||||||
EngPadSyncService_DeleteNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/DeleteNotebook"
|
EngPadSyncService_DeleteNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/DeleteNotebook"
|
||||||
EngPadSyncService_ListNotebooks_FullMethodName = "/engpad.v1.EngPadSyncService/ListNotebooks"
|
EngPadSyncService_ListNotebooks_FullMethodName = "/engpad.v1.EngPadSyncService/ListNotebooks"
|
||||||
EngPadSyncService_CreateShareLink_FullMethodName = "/engpad.v1.EngPadSyncService/CreateShareLink"
|
EngPadSyncService_CreateShareLink_FullMethodName = "/engpad.v1.EngPadSyncService/CreateShareLink"
|
||||||
@@ -32,6 +33,7 @@ const (
|
|||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
type EngPadSyncServiceClient interface {
|
type EngPadSyncServiceClient interface {
|
||||||
SyncNotebook(ctx context.Context, in *SyncNotebookRequest, opts ...grpc.CallOption) (*SyncNotebookResponse, error)
|
SyncNotebook(ctx context.Context, in *SyncNotebookRequest, opts ...grpc.CallOption) (*SyncNotebookResponse, error)
|
||||||
|
GetNotebook(ctx context.Context, in *GetNotebookRequest, opts ...grpc.CallOption) (*GetNotebookResponse, error)
|
||||||
DeleteNotebook(ctx context.Context, in *DeleteNotebookRequest, opts ...grpc.CallOption) (*DeleteNotebookResponse, error)
|
DeleteNotebook(ctx context.Context, in *DeleteNotebookRequest, opts ...grpc.CallOption) (*DeleteNotebookResponse, error)
|
||||||
ListNotebooks(ctx context.Context, in *ListNotebooksRequest, opts ...grpc.CallOption) (*ListNotebooksResponse, error)
|
ListNotebooks(ctx context.Context, in *ListNotebooksRequest, opts ...grpc.CallOption) (*ListNotebooksResponse, error)
|
||||||
CreateShareLink(ctx context.Context, in *CreateShareLinkRequest, opts ...grpc.CallOption) (*CreateShareLinkResponse, error)
|
CreateShareLink(ctx context.Context, in *CreateShareLinkRequest, opts ...grpc.CallOption) (*CreateShareLinkResponse, error)
|
||||||
@@ -57,6 +59,16 @@ func (c *engPadSyncServiceClient) SyncNotebook(ctx context.Context, in *SyncNote
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *engPadSyncServiceClient) GetNotebook(ctx context.Context, in *GetNotebookRequest, opts ...grpc.CallOption) (*GetNotebookResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetNotebookResponse)
|
||||||
|
err := c.cc.Invoke(ctx, EngPadSyncService_GetNotebook_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *engPadSyncServiceClient) DeleteNotebook(ctx context.Context, in *DeleteNotebookRequest, opts ...grpc.CallOption) (*DeleteNotebookResponse, error) {
|
func (c *engPadSyncServiceClient) DeleteNotebook(ctx context.Context, in *DeleteNotebookRequest, opts ...grpc.CallOption) (*DeleteNotebookResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(DeleteNotebookResponse)
|
out := new(DeleteNotebookResponse)
|
||||||
@@ -112,6 +124,7 @@ func (c *engPadSyncServiceClient) ListShareLinks(ctx context.Context, in *ListSh
|
|||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
type EngPadSyncServiceServer interface {
|
type EngPadSyncServiceServer interface {
|
||||||
SyncNotebook(context.Context, *SyncNotebookRequest) (*SyncNotebookResponse, error)
|
SyncNotebook(context.Context, *SyncNotebookRequest) (*SyncNotebookResponse, error)
|
||||||
|
GetNotebook(context.Context, *GetNotebookRequest) (*GetNotebookResponse, error)
|
||||||
DeleteNotebook(context.Context, *DeleteNotebookRequest) (*DeleteNotebookResponse, error)
|
DeleteNotebook(context.Context, *DeleteNotebookRequest) (*DeleteNotebookResponse, error)
|
||||||
ListNotebooks(context.Context, *ListNotebooksRequest) (*ListNotebooksResponse, error)
|
ListNotebooks(context.Context, *ListNotebooksRequest) (*ListNotebooksResponse, error)
|
||||||
CreateShareLink(context.Context, *CreateShareLinkRequest) (*CreateShareLinkResponse, error)
|
CreateShareLink(context.Context, *CreateShareLinkRequest) (*CreateShareLinkResponse, error)
|
||||||
@@ -130,6 +143,9 @@ type UnimplementedEngPadSyncServiceServer struct{}
|
|||||||
func (UnimplementedEngPadSyncServiceServer) SyncNotebook(context.Context, *SyncNotebookRequest) (*SyncNotebookResponse, error) {
|
func (UnimplementedEngPadSyncServiceServer) SyncNotebook(context.Context, *SyncNotebookRequest) (*SyncNotebookResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method SyncNotebook not implemented")
|
return nil, status.Error(codes.Unimplemented, "method SyncNotebook not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedEngPadSyncServiceServer) GetNotebook(context.Context, *GetNotebookRequest) (*GetNotebookResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetNotebook not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedEngPadSyncServiceServer) DeleteNotebook(context.Context, *DeleteNotebookRequest) (*DeleteNotebookResponse, error) {
|
func (UnimplementedEngPadSyncServiceServer) DeleteNotebook(context.Context, *DeleteNotebookRequest) (*DeleteNotebookResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method DeleteNotebook not implemented")
|
return nil, status.Error(codes.Unimplemented, "method DeleteNotebook not implemented")
|
||||||
}
|
}
|
||||||
@@ -184,6 +200,24 @@ func _EngPadSyncService_SyncNotebook_Handler(srv interface{}, ctx context.Contex
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _EngPadSyncService_GetNotebook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetNotebookRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(EngPadSyncServiceServer).GetNotebook(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: EngPadSyncService_GetNotebook_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(EngPadSyncServiceServer).GetNotebook(ctx, req.(*GetNotebookRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _EngPadSyncService_DeleteNotebook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _EngPadSyncService_DeleteNotebook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(DeleteNotebookRequest)
|
in := new(DeleteNotebookRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -285,6 +319,10 @@ var EngPadSyncService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "SyncNotebook",
|
MethodName: "SyncNotebook",
|
||||||
Handler: _EngPadSyncService_SyncNotebook_Handler,
|
Handler: _EngPadSyncService_SyncNotebook_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetNotebook",
|
||||||
|
Handler: _EngPadSyncService_GetNotebook_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "DeleteNotebook",
|
MethodName: "DeleteNotebook",
|
||||||
Handler: _EngPadSyncService_DeleteNotebook_Handler,
|
Handler: _EngPadSyncService_DeleteNotebook_Handler,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type Config struct {
|
|||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
ListenAddr string `toml:"listen_addr"`
|
ListenAddr string `toml:"listen_addr"`
|
||||||
GRPCAddr string `toml:"grpc_addr"`
|
GRPCAddr string `toml:"grpc_addr"`
|
||||||
|
GRPCPlainAddr string `toml:"grpc_plain_addr"`
|
||||||
TLSCert string `toml:"tls_cert"`
|
TLSCert string `toml:"tls_cert"`
|
||||||
TLSKey string `toml:"tls_key"`
|
TLSKey string `toml:"tls_key"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ func AuthInterceptor(database *sql.DB) grpc.UnaryServerInterceptor {
|
|||||||
return nil, status.Error(codes.Unauthenticated, "missing metadata")
|
return nil, status.Error(codes.Unauthenticated, "missing metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
usernames := md.Get("username")
|
usernames := md.Get("x-engpad-username")
|
||||||
passwords := md.Get("password")
|
passwords := md.Get("x-engpad-password")
|
||||||
if len(usernames) == 0 || len(passwords) == 0 {
|
if len(usernames) == 0 || len(passwords) == 0 {
|
||||||
return nil, status.Error(codes.Unauthenticated, "missing credentials")
|
return nil, status.Error(codes.Unauthenticated, "missing credentials")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Addr string
|
Addr string
|
||||||
|
PlainAddr string
|
||||||
TLSCert string
|
TLSCert string
|
||||||
TLSKey string
|
TLSKey string
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
@@ -31,7 +32,7 @@ func Start(cfg Config) (*grpc.Server, error) {
|
|||||||
|
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
MinVersion: tls.VersionTLS13,
|
MinVersion: tls.VersionTLS12,
|
||||||
}
|
}
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", cfg.Addr)
|
lis, err := net.Listen("tcp", cfg.Addr)
|
||||||
@@ -50,5 +51,19 @@ func Start(cfg Config) (*grpc.Server, error) {
|
|||||||
slog.Info("gRPC server started", "addr", cfg.Addr)
|
slog.Info("gRPC server started", "addr", cfg.Addr)
|
||||||
go func() { _ = srv.Serve(lis) }()
|
go func() { _ = srv.Serve(lis) }()
|
||||||
|
|
||||||
|
// Optional plaintext listener for reverse proxy (e.g. nginx grpc_pass).
|
||||||
|
if cfg.PlainAddr != "" {
|
||||||
|
plainLis, err := net.Listen("tcp", cfg.PlainAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listen %s: %w", cfg.PlainAddr, err)
|
||||||
|
}
|
||||||
|
plainSrv := grpc.NewServer(
|
||||||
|
grpc.UnaryInterceptor(AuthInterceptor(cfg.DB)),
|
||||||
|
)
|
||||||
|
pb.RegisterEngPadSyncServiceServer(plainSrv, syncSvc)
|
||||||
|
slog.Info("gRPC plaintext server started", "addr", cfg.PlainAddr)
|
||||||
|
go func() { _ = plainSrv.Serve(plainLis) }()
|
||||||
|
}
|
||||||
|
|
||||||
return srv, nil
|
return srv, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,71 @@ func (s *SyncService) SyncNotebook(ctx context.Context, req *pb.SyncNotebookRequ
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SyncService) GetNotebook(ctx context.Context, req *pb.GetNotebookRequest) (*pb.GetNotebookResponse, error) {
|
||||||
|
userID, ok := UserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, status.Error(codes.Internal, "missing user context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp pb.GetNotebookResponse
|
||||||
|
var syncedAt int64
|
||||||
|
err := s.DB.QueryRowContext(ctx,
|
||||||
|
"SELECT id, remote_id, title, page_size, synced_at FROM notebooks WHERE id = ? AND user_id = ?",
|
||||||
|
req.NotebookId, userID,
|
||||||
|
).Scan(&resp.ServerNotebookId, &resp.RemoteId, &resp.Title, &resp.PageSize, &syncedAt)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, status.Error(codes.NotFound, "notebook not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "query notebook: %v", err)
|
||||||
|
}
|
||||||
|
resp.SyncedAt = timestamppb.New(time.UnixMilli(syncedAt))
|
||||||
|
|
||||||
|
pageRows, err := s.DB.QueryContext(ctx,
|
||||||
|
"SELECT id, remote_id, page_number FROM pages WHERE notebook_id = ? ORDER BY page_number",
|
||||||
|
resp.ServerNotebookId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "query pages: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = pageRows.Close() }()
|
||||||
|
|
||||||
|
for pageRows.Next() {
|
||||||
|
var pageID, remoteID int64
|
||||||
|
var pageNum int32
|
||||||
|
if err := pageRows.Scan(&pageID, &remoteID, &pageNum); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "scan page: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pd := &pb.PageData{
|
||||||
|
PageId: remoteID,
|
||||||
|
PageNumber: pageNum,
|
||||||
|
}
|
||||||
|
|
||||||
|
strokeRows, err := s.DB.QueryContext(ctx,
|
||||||
|
"SELECT pen_size, color, style, point_data, stroke_order FROM strokes WHERE page_id = ? ORDER BY stroke_order",
|
||||||
|
pageID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "query strokes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for strokeRows.Next() {
|
||||||
|
var sd pb.StrokeData
|
||||||
|
if err := strokeRows.Scan(&sd.PenSize, &sd.Color, &sd.Style, &sd.PointData, &sd.StrokeOrder); err != nil {
|
||||||
|
_ = strokeRows.Close()
|
||||||
|
return nil, status.Errorf(codes.Internal, "scan stroke: %v", err)
|
||||||
|
}
|
||||||
|
pd.Strokes = append(pd.Strokes, &sd)
|
||||||
|
}
|
||||||
|
_ = strokeRows.Close()
|
||||||
|
|
||||||
|
resp.Pages = append(resp.Pages, pd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SyncService) DeleteNotebook(ctx context.Context, req *pb.DeleteNotebookRequest) (*pb.DeleteNotebookResponse, error) {
|
func (s *SyncService) DeleteNotebook(ctx context.Context, req *pb.DeleteNotebookRequest) (*pb.DeleteNotebookResponse, error) {
|
||||||
userID, ok := UserIDFromContext(ctx)
|
userID, ok := UserIDFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func (ws *WebServer) handleLoginSubmit(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
userID, err := auth.AuthenticateUser(ws.db, username, password)
|
userID, err := auth.AuthenticateUser(ws.db, username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
slog.Error("login failed", "username", username, "error", err)
|
||||||
ws.render(w, "login.html", map[string]string{"Error": "Invalid credentials"})
|
ws.render(w, "login.html", map[string]string{"Error": "Invalid credentials"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -127,15 +128,21 @@ func (ws *WebServer) handleNotebook(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
pages = append(pages, pageInfo{
|
pages = append(pages, pageInfo{
|
||||||
Number: num,
|
Number: num,
|
||||||
SVGLink: fmt.Sprintf("/v1/notebooks/%d/pages/%d/svg", id, num),
|
SVGLink: fmt.Sprintf("/notebooks/%d/pages/%d/svg", id, num),
|
||||||
ViewLink: fmt.Sprintf("/notebooks/%d/pages/%d", id, num),
|
ViewLink: fmt.Sprintf("/notebooks/%d/pages/%d", id, num),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load share links for this notebook.
|
||||||
|
shareLinks, _ := share.ListLinks(ws.db, id, ws.baseURL)
|
||||||
|
|
||||||
ws.render(w, "notebook.html", map[string]any{
|
ws.render(w, "notebook.html", map[string]any{
|
||||||
|
"ID": id,
|
||||||
"Title": title,
|
"Title": title,
|
||||||
"Pages": pages,
|
"Pages": pages,
|
||||||
"PDFLink": fmt.Sprintf("/v1/notebooks/%d/pdf", id),
|
"PDFLink": fmt.Sprintf("/notebooks/%d/pdf", id),
|
||||||
|
"ShareLinks": shareLinks,
|
||||||
|
"BaseURL": ws.baseURL,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,9 +161,9 @@ func (ws *WebServer) handlePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
"NotebookTitle": title,
|
"NotebookTitle": title,
|
||||||
"PageNumber": num,
|
"PageNumber": num,
|
||||||
"BackLink": fmt.Sprintf("/notebooks/%d", id),
|
"BackLink": fmt.Sprintf("/notebooks/%d", id),
|
||||||
"SVGLink": fmt.Sprintf("/v1/notebooks/%d/pages/%d/svg", id, num),
|
"SVGLink": fmt.Sprintf("/notebooks/%d/pages/%d/svg", id, num),
|
||||||
"JPGLink": fmt.Sprintf("/v1/notebooks/%d/pages/%d/jpg", id, num),
|
"JPGLink": fmt.Sprintf("/notebooks/%d/pages/%d/jpg", id, num),
|
||||||
"PDFLink": fmt.Sprintf("/v1/notebooks/%d/pdf", id),
|
"PDFLink": fmt.Sprintf("/notebooks/%d/pdf", id),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +208,7 @@ func (ws *WebServer) handleShareNotebook(w http.ResponseWriter, r *http.Request)
|
|||||||
"Title": title,
|
"Title": title,
|
||||||
"Pages": pages,
|
"Pages": pages,
|
||||||
"PDFLink": fmt.Sprintf("/s/%s/pdf", token),
|
"PDFLink": fmt.Sprintf("/s/%s/pdf", token),
|
||||||
|
"Shared": true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +235,45 @@ func (ws *WebServer) handleSharePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Share management ---
|
||||||
|
|
||||||
|
func (ws *WebServer) handleCreateShare(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
_, _, err := share.CreateLink(ws.db, id, 0, ws.baseURL) // no expiry
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("create share link", "error", err)
|
||||||
|
http.Error(w, "Failed to create share link", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("/notebooks/%d", id), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) handleDeleteNotebook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := r.Context().Value(userIDKey).(int64)
|
||||||
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
|
||||||
|
// Verify ownership before deleting.
|
||||||
|
var ownerID int64
|
||||||
|
err := ws.db.QueryRow("SELECT user_id FROM notebooks WHERE id = ?", id).Scan(&ownerID)
|
||||||
|
if err != nil || ownerID != userID {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASCADE deletes pages, strokes, and share_links.
|
||||||
|
_, _ = ws.db.Exec("DELETE FROM notebooks WHERE id = ?", id)
|
||||||
|
http.Redirect(w, r, "/notebooks", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) handleRevokeShare(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
token := r.FormValue("token")
|
||||||
|
if token != "" {
|
||||||
|
_ = share.RevokeLink(ws.db, token)
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("/notebooks/%d", id), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
// --- auth middleware ---
|
// --- auth middleware ---
|
||||||
|
|
||||||
type ctxKey string
|
type ctxKey string
|
||||||
@@ -254,8 +301,14 @@ func (ws *WebServer) authMiddleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebServer) render(w http.ResponseWriter, name string, data any) {
|
func (ws *WebServer) render(w http.ResponseWriter, name string, data any) {
|
||||||
|
t, ok := ws.tmpls[name]
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Template not found", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
if err := ws.tmpl.ExecuteTemplate(w, name, data); err != nil {
|
if err := t.ExecuteTemplate(w, "layout.html", data); err != nil {
|
||||||
|
slog.Error("render template", "name", name, "error", err)
|
||||||
http.Error(w, "Template error", http.StatusInternalServerError)
|
http.Error(w, "Template error", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
232
internal/webserver/render.go
Normal file
232
internal/webserver/render.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package webserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/eng-pad-server/internal/render"
|
||||||
|
"git.wntrmute.dev/kyle/eng-pad-server/internal/share"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Authenticated render endpoints ---
|
||||||
|
|
||||||
|
func (ws *WebServer) handlePageSVG(w http.ResponseWriter, r *http.Request) {
|
||||||
|
notebookID, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
pageNum, _ := strconv.Atoi(chi.URLParam(r, "num"))
|
||||||
|
|
||||||
|
strokes, pageSize, err := ws.loadPageStrokes(notebookID, pageNum)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svg, err := render.RenderSVG(pageSize, strokes)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Render error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "image/svg+xml")
|
||||||
|
_, _ = w.Write([]byte(svg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) handlePageJPG(w http.ResponseWriter, r *http.Request) {
|
||||||
|
notebookID, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
pageNum, _ := strconv.Atoi(chi.URLParam(r, "num"))
|
||||||
|
|
||||||
|
strokes, pageSize, err := ws.loadPageStrokes(notebookID, pageNum)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := render.RenderJPG(pageSize, strokes, 95)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Render error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "image/jpeg")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=page-%d.jpg", pageNum))
|
||||||
|
_, _ = w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) handleNotebookPDF(w http.ResponseWriter, r *http.Request) {
|
||||||
|
notebookID, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
|
||||||
|
pages, pageSize, err := ws.loadNotebookPages(notebookID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := render.RenderPDF(pageSize, pages)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Render error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/pdf")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=notebook.pdf")
|
||||||
|
_, _ = w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Share render endpoints ---
|
||||||
|
|
||||||
|
func (ws *WebServer) handleSharePageSVG(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := chi.URLParam(r, "token")
|
||||||
|
notebookID, err := share.ValidateLink(ws.db, token)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Link not found or expired", http.StatusGone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pageNum, _ := strconv.Atoi(chi.URLParam(r, "num"))
|
||||||
|
|
||||||
|
strokes, pageSize, err := ws.loadPageStrokes(notebookID, pageNum)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svg, err := render.RenderSVG(pageSize, strokes)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Render error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "image/svg+xml")
|
||||||
|
_, _ = w.Write([]byte(svg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) handleSharePageJPG(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := chi.URLParam(r, "token")
|
||||||
|
notebookID, err := share.ValidateLink(ws.db, token)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Link not found or expired", http.StatusGone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pageNum, _ := strconv.Atoi(chi.URLParam(r, "num"))
|
||||||
|
|
||||||
|
strokes, pageSize, err := ws.loadPageStrokes(notebookID, pageNum)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := render.RenderJPG(pageSize, strokes, 95)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Render error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "image/jpeg")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=page-%d.jpg", pageNum))
|
||||||
|
_, _ = w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) handleSharePDF(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := chi.URLParam(r, "token")
|
||||||
|
notebookID, err := share.ValidateLink(ws.db, token)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Link not found or expired", http.StatusGone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pages, pageSize, err := ws.loadNotebookPages(notebookID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := render.RenderPDF(pageSize, pages)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Render error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/pdf")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=notebook.pdf")
|
||||||
|
_, _ = w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DB helpers ---
|
||||||
|
|
||||||
|
func (ws *WebServer) loadPageStrokes(notebookID int64, pageNum int) ([]render.Stroke, string, error) {
|
||||||
|
var pageSize string
|
||||||
|
err := ws.db.QueryRow("SELECT page_size FROM notebooks WHERE id = ?", notebookID).Scan(&pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageID int64
|
||||||
|
err = ws.db.QueryRow(
|
||||||
|
"SELECT id FROM pages WHERE notebook_id = ? AND page_number = ?",
|
||||||
|
notebookID, pageNum,
|
||||||
|
).Scan(&pageID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := ws.db.Query(
|
||||||
|
"SELECT pen_size, color, style, point_data, stroke_order FROM strokes WHERE page_id = ? ORDER BY stroke_order",
|
||||||
|
pageID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var strokes []render.Stroke
|
||||||
|
for rows.Next() {
|
||||||
|
var s render.Stroke
|
||||||
|
if err := rows.Scan(&s.PenSize, &s.Color, &s.Style, &s.PointData, &s.StrokeOrder); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
strokes = append(strokes, s)
|
||||||
|
}
|
||||||
|
return strokes, pageSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) loadNotebookPages(notebookID int64) ([]render.Page, string, error) {
|
||||||
|
var pageSize string
|
||||||
|
err := ws.db.QueryRow("SELECT page_size FROM notebooks WHERE id = ?", notebookID).Scan(&pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := ws.db.Query(
|
||||||
|
"SELECT id, page_number FROM pages WHERE notebook_id = ? ORDER BY page_number",
|
||||||
|
notebookID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var pages []render.Page
|
||||||
|
for rows.Next() {
|
||||||
|
var pageID int64
|
||||||
|
var pageNum int
|
||||||
|
if err := rows.Scan(&pageID, &pageNum); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
strokeRows, err := ws.db.Query(
|
||||||
|
"SELECT pen_size, color, style, point_data, stroke_order FROM strokes WHERE page_id = ? ORDER BY stroke_order",
|
||||||
|
pageID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var strokes []render.Stroke
|
||||||
|
for strokeRows.Next() {
|
||||||
|
var s render.Stroke
|
||||||
|
if err := strokeRows.Scan(&s.PenSize, &s.Color, &s.Style, &s.PointData, &s.StrokeOrder); err != nil {
|
||||||
|
_ = strokeRows.Close()
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
strokes = append(strokes, s)
|
||||||
|
}
|
||||||
|
_ = strokeRows.Close()
|
||||||
|
|
||||||
|
pages = append(pages, render.Page{PageNumber: pageNum, Strokes: strokes})
|
||||||
|
}
|
||||||
|
return pages, pageSize, nil
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ type Config struct {
|
|||||||
type WebServer struct {
|
type WebServer struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
baseURL string
|
baseURL string
|
||||||
tmpl *template.Template
|
tmpls map[string]*template.Template
|
||||||
webauthn *webauthn.WebAuthn
|
webauthn *webauthn.WebAuthn
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
sessions map[string]*webauthn.SessionData
|
sessions map[string]*webauthn.SessionData
|
||||||
@@ -43,15 +43,32 @@ func Start(cfg Config) (*http.Server, error) {
|
|||||||
return nil, fmt.Errorf("template fs: %w", err)
|
return nil, fmt.Errorf("template fs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl, err := template.ParseFS(templateFS, "*.html")
|
layoutData, err := fs.ReadFile(templateFS, "layout.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse templates: %w", err)
|
return nil, fmt.Errorf("read layout: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pages := []string{"login.html", "notebooks.html", "notebook.html", "page.html", "keys.html"}
|
||||||
|
tmpls := make(map[string]*template.Template, len(pages))
|
||||||
|
for _, page := range pages {
|
||||||
|
t, err := template.New("layout.html").Parse(string(layoutData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse layout: %w", err)
|
||||||
|
}
|
||||||
|
pageData, err := fs.ReadFile(templateFS, page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read %s: %w", page, err)
|
||||||
|
}
|
||||||
|
if _, err := t.Parse(string(pageData)); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse %s: %w", page, err)
|
||||||
|
}
|
||||||
|
tmpls[page] = t
|
||||||
}
|
}
|
||||||
|
|
||||||
ws := &WebServer{
|
ws := &WebServer{
|
||||||
db: cfg.DB,
|
db: cfg.DB,
|
||||||
baseURL: cfg.BaseURL,
|
baseURL: cfg.BaseURL,
|
||||||
tmpl: tmpl,
|
tmpls: tmpls,
|
||||||
sessions: make(map[string]*webauthn.SessionData),
|
sessions: make(map[string]*webauthn.SessionData),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +98,9 @@ func Start(cfg Config) (*http.Server, error) {
|
|||||||
// Share routes (no auth)
|
// Share routes (no auth)
|
||||||
r.Get("/s/{token}", ws.handleShareNotebook)
|
r.Get("/s/{token}", ws.handleShareNotebook)
|
||||||
r.Get("/s/{token}/pages/{num}", ws.handleSharePage)
|
r.Get("/s/{token}/pages/{num}", ws.handleSharePage)
|
||||||
|
r.Get("/s/{token}/pages/{num}/svg", ws.handleSharePageSVG)
|
||||||
|
r.Get("/s/{token}/pages/{num}/jpg", ws.handleSharePageJPG)
|
||||||
|
r.Get("/s/{token}/pdf", ws.handleSharePDF)
|
||||||
|
|
||||||
// Authenticated routes
|
// Authenticated routes
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
@@ -89,6 +109,12 @@ func Start(cfg Config) (*http.Server, error) {
|
|||||||
r.Get("/notebooks", ws.handleNotebooks)
|
r.Get("/notebooks", ws.handleNotebooks)
|
||||||
r.Get("/notebooks/{id}", ws.handleNotebook)
|
r.Get("/notebooks/{id}", ws.handleNotebook)
|
||||||
r.Get("/notebooks/{id}/pages/{num}", ws.handlePage)
|
r.Get("/notebooks/{id}/pages/{num}", ws.handlePage)
|
||||||
|
r.Get("/notebooks/{id}/pages/{num}/svg", ws.handlePageSVG)
|
||||||
|
r.Get("/notebooks/{id}/pages/{num}/jpg", ws.handlePageJPG)
|
||||||
|
r.Get("/notebooks/{id}/pdf", ws.handleNotebookPDF)
|
||||||
|
r.Post("/notebooks/{id}/delete", ws.handleDeleteNotebook)
|
||||||
|
r.Post("/notebooks/{id}/share", ws.handleCreateShare)
|
||||||
|
r.Post("/notebooks/{id}/share/revoke", ws.handleRevokeShare)
|
||||||
r.Get("/logout", ws.handleLogout)
|
r.Get("/logout", ws.handleLogout)
|
||||||
|
|
||||||
// WebAuthn authenticated routes (registration + key management)
|
// WebAuthn authenticated routes (registration + key management)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import "google/protobuf/timestamp.proto";
|
|||||||
|
|
||||||
service EngPadSyncService {
|
service EngPadSyncService {
|
||||||
rpc SyncNotebook(SyncNotebookRequest) returns (SyncNotebookResponse);
|
rpc SyncNotebook(SyncNotebookRequest) returns (SyncNotebookResponse);
|
||||||
|
rpc GetNotebook(GetNotebookRequest) returns (GetNotebookResponse);
|
||||||
rpc DeleteNotebook(DeleteNotebookRequest) returns (DeleteNotebookResponse);
|
rpc DeleteNotebook(DeleteNotebookRequest) returns (DeleteNotebookResponse);
|
||||||
rpc ListNotebooks(ListNotebooksRequest) returns (ListNotebooksResponse);
|
rpc ListNotebooks(ListNotebooksRequest) returns (ListNotebooksResponse);
|
||||||
rpc CreateShareLink(CreateShareLinkRequest) returns (CreateShareLinkResponse);
|
rpc CreateShareLink(CreateShareLinkRequest) returns (CreateShareLinkResponse);
|
||||||
@@ -42,6 +43,19 @@ message SyncNotebookResponse {
|
|||||||
google.protobuf.Timestamp synced_at = 2;
|
google.protobuf.Timestamp synced_at = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GetNotebookRequest {
|
||||||
|
int64 notebook_id = 1; // Server-side notebook ID
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetNotebookResponse {
|
||||||
|
int64 server_notebook_id = 1;
|
||||||
|
int64 remote_id = 2;
|
||||||
|
string title = 3;
|
||||||
|
string page_size = 4;
|
||||||
|
repeated PageData pages = 5;
|
||||||
|
google.protobuf.Timestamp synced_at = 6;
|
||||||
|
}
|
||||||
|
|
||||||
message DeleteNotebookRequest {
|
message DeleteNotebookRequest {
|
||||||
int64 notebook_id = 1;
|
int64 notebook_id = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
|
||||||
.page-thumb { border: 1px solid #ccc; background: #fff; aspect-ratio: 0.773; display: flex; align-items: center; justify-content: center; }
|
.page-thumb { border: 1px solid #ccc; background: #fff; aspect-ratio: 0.773; display: flex; align-items: center; justify-content: center; }
|
||||||
.page-thumb img { width: 100%; height: 100%; object-fit: contain; }
|
.page-thumb img { width: 100%; height: 100%; object-fit: contain; }
|
||||||
.btn { display: inline-block; padding: 0.5rem 1rem; border: 1px solid #111; border-radius: 4px; text-decoration: none; color: #111; background: #fff; cursor: pointer; }
|
.btn { display: inline-flex; align-items: center; padding: 0.5rem 1rem; border: 1px solid #111; border-radius: 4px; text-decoration: none; color: #111; background: #fff; cursor: pointer; font: inherit; font-size: 1rem; line-height: 1.5; box-sizing: border-box; }
|
||||||
.btn:hover { background: #f0f0f0; }
|
.btn:hover { background: #f0f0f0; }
|
||||||
.actions { display: flex; gap: 0.5rem; margin: 1rem 0; }
|
.actions { display: flex; align-items: center; gap: 0.5rem; margin: 1rem 0; }
|
||||||
input[type="text"], input[type="password"] { padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; width: 100%; max-width: 300px; }
|
input[type="text"], input[type="password"] { padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; width: 100%; max-width: 300px; }
|
||||||
label { display: block; margin-bottom: 0.25rem; font-weight: bold; }
|
label { display: block; margin-bottom: 0.25rem; font-weight: bold; }
|
||||||
.form-group { margin-bottom: 1rem; }
|
.form-group { margin-bottom: 1rem; }
|
||||||
|
|||||||
@@ -4,7 +4,32 @@
|
|||||||
<h1>{{.Title}}</h1>
|
<h1>{{.Title}}</h1>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a href="{{.PDFLink}}" class="btn">Download PDF</a>
|
<a href="{{.PDFLink}}" class="btn">Download PDF</a>
|
||||||
|
{{if not .Shared}}
|
||||||
|
<form method="POST" action="/notebooks/{{.ID}}/share" style="display:inline;">
|
||||||
|
<button type="submit" class="btn">Share</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/notebooks/{{.ID}}/delete" style="display:inline;"
|
||||||
|
onsubmit="return confirm('Delete this notebook from the server?');">
|
||||||
|
<button type="submit" class="btn" style="color: #c62828; border-color: #c62828;">Delete</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if .ShareLinks}}
|
||||||
|
<div style="margin: 1rem 0; padding: 0.75rem; border: 1px solid #ccc; border-radius: 4px;">
|
||||||
|
<strong>Share Links</strong>
|
||||||
|
{{range .ShareLinks}}
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
<code style="font-size: 0.85rem; flex: 1;"><a href="{{.URL}}">{{.URL}}</a></code>
|
||||||
|
<form method="POST" action="/notebooks/{{$.ID}}/share/revoke" style="margin:0;">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<button type="submit" class="btn" style="font-size: 0.8rem; padding: 0.25rem 0.5rem;">Revoke</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{{range .Pages}}
|
{{range .Pages}}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user