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
|
||||
[server]
|
||||
listen_addr = ":8443"
|
||||
grpc_addr = ":9443"
|
||||
tls_cert = "/srv/eng-pad-server/certs/cert.pem"
|
||||
tls_key = "/srv/eng-pad-server/certs/key.pem"
|
||||
listen_addr = ":8443" # REST API (HTTPS)
|
||||
grpc_addr = ":9443" # gRPC (TLS, exposed directly)
|
||||
grpc_plain_addr = "" # Optional plaintext gRPC for reverse proxy
|
||||
tls_cert = "/srv/eng-pad-server/certs/fullchain.pem"
|
||||
tls_key = "/srv/eng-pad-server/certs/privkey.pem"
|
||||
|
||||
[web]
|
||||
listen_addr = ":8080"
|
||||
listen_addr = ":8080" # Web UI (plain HTTP behind nginx)
|
||||
base_url = "https://pad.metacircular.net"
|
||||
|
||||
[database]
|
||||
@@ -255,13 +256,32 @@ level = "info"
|
||||
|
||||
## 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
|
||||
|
||||
Multi-stage Docker build:
|
||||
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 |
|
||||
|------|---------|
|
||||
@@ -279,8 +299,8 @@ ReadWritePaths=/srv/eng-pad-server.
|
||||
├── eng-pad-server.toml
|
||||
├── eng-pad-server.db
|
||||
├── certs/
|
||||
│ ├── cert.pem
|
||||
│ └── key.pem
|
||||
│ ├── fullchain.pem # Let's Encrypt cert chain
|
||||
│ └── privkey.pem # Let's Encrypt private key
|
||||
└── backups/
|
||||
```
|
||||
|
||||
@@ -301,6 +321,7 @@ ReadWritePaths=/srv/eng-pad-server.
|
||||
|---------|---------|
|
||||
| server | Start the service |
|
||||
| init | Create database, first user |
|
||||
| passwd | Reset a user's password |
|
||||
| snapshot | Database backup (VACUUM INTO) |
|
||||
| status | Health check |
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ eng-pad-server/
|
||||
│ └── eng-pad-server/ CLI entry point (cobra)
|
||||
│ ├── main.go
|
||||
│ ├── server.go server subcommand
|
||||
│ └── init.go init subcommand
|
||||
│ ├── init.go init subcommand
|
||||
│ └── passwd.go password reset subcommand
|
||||
├── internal/
|
||||
│ ├── auth/
|
||||
│ │ ├── 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.)
|
||||
|
||||
# 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
|
||||
./eng-pad-server server
|
||||
./eng-pad-server server -c /srv/eng-pad-server/eng-pad-server.toml
|
||||
```
|
||||
|
||||
## Build
|
||||
@@ -43,6 +43,54 @@ make proto # regenerate gRPC code from .proto files
|
||||
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
|
||||
|
||||
- [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
|
||||
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/`
|
||||
**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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
4. Check gRPC responds:
|
||||
```
|
||||
grpcurl -insecure localhost:9443 list
|
||||
docker logs eng-pad-server --tail 20
|
||||
```
|
||||
|
||||
## 3. Common Operations
|
||||
@@ -38,89 +36,74 @@ views through a web UI. Single authenticated user.
|
||||
### Start / Stop / Restart
|
||||
|
||||
```
|
||||
systemctl start eng-pad-server
|
||||
systemctl stop eng-pad-server
|
||||
systemctl restart eng-pad-server
|
||||
docker start eng-pad-server
|
||||
docker stop eng-pad-server
|
||||
docker restart eng-pad-server
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
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/`.
|
||||
|
||||
### 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
|
||||
|
||||
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`.
|
||||
3. Enter a name for the key (e.g., "YubiKey 5").
|
||||
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
|
||||
|
||||
No automated alerting is configured. Monitor via:
|
||||
- `systemctl status eng-pad-server` — process health
|
||||
- `journalctl -u eng-pad-server --since "1 hour ago" | grep ERROR` — errors
|
||||
- `docker ps | grep eng-pad-server` — container health
|
||||
- `docker logs eng-pad-server --since 1h 2>&1 | grep ERROR` — errors
|
||||
- Backup age: `ls -lt /srv/eng-pad-server/backups/ | head`
|
||||
|
||||
## 5. Incident Procedures
|
||||
@@ -129,19 +112,19 @@ No automated alerting is configured. Monitor via:
|
||||
|
||||
1. Check logs:
|
||||
```
|
||||
journalctl -u eng-pad-server -n 50 --no-pager
|
||||
docker logs eng-pad-server --tail 50
|
||||
```
|
||||
2. Common causes:
|
||||
- Config file missing or invalid → fix config
|
||||
- TLS cert/key missing → regenerate or copy
|
||||
- Port already in use → `ss -tlnp | grep 8443`
|
||||
- Config file missing or invalid → fix `/srv/eng-pad-server/eng-pad-server.toml`
|
||||
- TLS cert/key missing → re-copy from Let's Encrypt (see Renew TLS above)
|
||||
- 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 Corruption
|
||||
|
||||
1. Stop the service:
|
||||
1. Stop the container:
|
||||
```
|
||||
systemctl stop eng-pad-server
|
||||
docker stop eng-pad-server
|
||||
```
|
||||
2. Check integrity:
|
||||
```
|
||||
@@ -150,21 +133,20 @@ No automated alerting is configured. Monitor via:
|
||||
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
|
||||
chown engpad:engpad /srv/eng-pad-server/eng-pad-server.db
|
||||
```
|
||||
4. Restart:
|
||||
```
|
||||
systemctl start eng-pad-server
|
||||
docker start eng-pad-server
|
||||
```
|
||||
|
||||
### Certificate 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.
|
||||
3. Restart the service (picks up new certs on start).
|
||||
2. Renew via certbot (see "Renew TLS Certificates" above).
|
||||
3. Restart the container (picks up new certs on start).
|
||||
|
||||
### Disk Full
|
||||
|
||||
@@ -184,11 +166,12 @@ No automated alerting is configured. Monitor via:
|
||||
|
||||
### Sync Fails from Android App
|
||||
|
||||
1. Verify server is reachable from the device's network.
|
||||
2. Check gRPC port is open: `ss -tlnp | grep 9443`
|
||||
3. Check TLS cert is valid and trusted by the device.
|
||||
4. Check credentials: verify the user exists via `eng-pad-server status`.
|
||||
5. Check server logs for auth failures: `journalctl -u eng-pad-server | grep UNAUTHENTICATED`
|
||||
1. Verify the app has the correct server URL (`pad.metacircular.net:9443`).
|
||||
2. Use "Test Connection" in the app's sync settings for a specific error.
|
||||
3. Check gRPC port is open: `ss -tlnp | grep 9443`
|
||||
4. Check firewall: `sudo ufw status | grep 9443` (must be ALLOW).
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
@@ -50,11 +50,12 @@ func runServer(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Start gRPC server
|
||||
grpcSrv, err := grpcserver.Start(grpcserver.Config{
|
||||
Addr: cfg.Server.GRPCAddr,
|
||||
TLSCert: cfg.Server.TLSCert,
|
||||
TLSKey: cfg.Server.TLSKey,
|
||||
DB: database,
|
||||
BaseURL: cfg.Web.BaseURL,
|
||||
Addr: cfg.Server.GRPCAddr,
|
||||
PlainAddr: cfg.Server.GRPCPlainAddr,
|
||||
TLSCert: cfg.Server.TLSCert,
|
||||
TLSKey: cfg.Server.TLSKey,
|
||||
DB: database,
|
||||
BaseURL: cfg.Web.BaseURL,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("start grpc: %w", err)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[server]
|
||||
listen_addr = ":8443"
|
||||
grpc_addr = ":9443"
|
||||
tls_cert = "/srv/eng-pad-server/certs/cert.pem"
|
||||
tls_key = "/srv/eng-pad-server/certs/key.pem"
|
||||
listen_addr = ":8443"
|
||||
grpc_addr = ":9443"
|
||||
# grpc_plain_addr = "127.0.0.1:9444" # Optional: plaintext gRPC for reverse proxy
|
||||
tls_cert = "/srv/eng-pad-server/certs/fullchain.pem"
|
||||
tls_key = "/srv/eng-pad-server/certs/privkey.pem"
|
||||
|
||||
[web]
|
||||
listen_addr = ":8080"
|
||||
|
||||
@@ -278,6 +278,134 @@ func (x *SyncNotebookResponse) GetSyncedAt() *timestamppb.Timestamp {
|
||||
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 {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
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() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -299,7 +427,7 @@ func (x *DeleteNotebookRequest) String() string {
|
||||
func (*DeleteNotebookRequest) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -312,7 +440,7 @@ func (x *DeleteNotebookRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use DeleteNotebookRequest.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -330,7 +458,7 @@ type DeleteNotebookResponse struct {
|
||||
|
||||
func (x *DeleteNotebookResponse) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -342,7 +470,7 @@ func (x *DeleteNotebookResponse) String() string {
|
||||
func (*DeleteNotebookResponse) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -355,7 +483,7 @@ func (x *DeleteNotebookResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use DeleteNotebookResponse.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -366,7 +494,7 @@ type ListNotebooksRequest struct {
|
||||
|
||||
func (x *ListNotebooksRequest) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -378,7 +506,7 @@ func (x *ListNotebooksRequest) String() string {
|
||||
func (*ListNotebooksRequest) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -391,7 +519,7 @@ func (x *ListNotebooksRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ListNotebooksRequest.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -403,7 +531,7 @@ type ListNotebooksResponse struct {
|
||||
|
||||
func (x *ListNotebooksResponse) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -415,7 +543,7 @@ func (x *ListNotebooksResponse) String() string {
|
||||
func (*ListNotebooksResponse) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -428,7 +556,7 @@ func (x *ListNotebooksResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ListNotebooksResponse.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -452,7 +580,7 @@ type NotebookSummary struct {
|
||||
|
||||
func (x *NotebookSummary) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -464,7 +592,7 @@ func (x *NotebookSummary) String() string {
|
||||
func (*NotebookSummary) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -477,7 +605,7 @@ func (x *NotebookSummary) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use NotebookSummary.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -532,7 +660,7 @@ type CreateShareLinkRequest struct {
|
||||
|
||||
func (x *CreateShareLinkRequest) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -544,7 +672,7 @@ func (x *CreateShareLinkRequest) String() string {
|
||||
func (*CreateShareLinkRequest) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -557,7 +685,7 @@ func (x *CreateShareLinkRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateShareLinkRequest.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -585,7 +713,7 @@ type CreateShareLinkResponse struct {
|
||||
|
||||
func (x *CreateShareLinkResponse) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -597,7 +725,7 @@ func (x *CreateShareLinkResponse) String() string {
|
||||
func (*CreateShareLinkResponse) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -610,7 +738,7 @@ func (x *CreateShareLinkResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateShareLinkResponse.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -643,7 +771,7 @@ type RevokeShareLinkRequest struct {
|
||||
|
||||
func (x *RevokeShareLinkRequest) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -655,7 +783,7 @@ func (x *RevokeShareLinkRequest) String() string {
|
||||
func (*RevokeShareLinkRequest) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -668,7 +796,7 @@ func (x *RevokeShareLinkRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RevokeShareLinkRequest.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -686,7 +814,7 @@ type RevokeShareLinkResponse struct {
|
||||
|
||||
func (x *RevokeShareLinkResponse) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -698,7 +826,7 @@ func (x *RevokeShareLinkResponse) String() string {
|
||||
func (*RevokeShareLinkResponse) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -711,7 +839,7 @@ func (x *RevokeShareLinkResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RevokeShareLinkResponse.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -723,7 +851,7 @@ type ListShareLinksRequest struct {
|
||||
|
||||
func (x *ListShareLinksRequest) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -735,7 +863,7 @@ func (x *ListShareLinksRequest) String() string {
|
||||
func (*ListShareLinksRequest) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -748,7 +876,7 @@ func (x *ListShareLinksRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ListShareLinksRequest.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -767,7 +895,7 @@ type ListShareLinksResponse struct {
|
||||
|
||||
func (x *ListShareLinksResponse) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -779,7 +907,7 @@ func (x *ListShareLinksResponse) String() string {
|
||||
func (*ListShareLinksResponse) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -792,7 +920,7 @@ func (x *ListShareLinksResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ListShareLinksResponse.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -814,7 +942,7 @@ type ShareLinkInfo struct {
|
||||
|
||||
func (x *ShareLinkInfo) Reset() {
|
||||
*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.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -826,7 +954,7 @@ func (x *ShareLinkInfo) String() string {
|
||||
func (*ShareLinkInfo) ProtoMessage() {}
|
||||
|
||||
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 {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -839,7 +967,7 @@ func (x *ShareLinkInfo) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ShareLinkInfo.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@@ -896,7 +1024,17 @@ const file_proto_engpad_v1_sync_proto_rawDesc = "" +
|
||||
"\fstroke_order\x18\x05 \x01(\x05R\vstrokeOrder\"}\n" +
|
||||
"\x14SyncNotebookResponse\x12,\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" +
|
||||
"\vnotebook_id\x18\x01 \x01(\x03R\n" +
|
||||
"notebookId\"\x18\n" +
|
||||
@@ -935,9 +1073,10 @@ const file_proto_engpad_v1_sync_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\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" +
|
||||
"\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" +
|
||||
"\rListNotebooks\x12\x1f.engpad.v1.ListNotebooksRequest\x1a .engpad.v1.ListNotebooksResponse\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
|
||||
}
|
||||
|
||||
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{
|
||||
(*SyncNotebookRequest)(nil), // 0: engpad.v1.SyncNotebookRequest
|
||||
(*PageData)(nil), // 1: engpad.v1.PageData
|
||||
(*StrokeData)(nil), // 2: engpad.v1.StrokeData
|
||||
(*SyncNotebookResponse)(nil), // 3: engpad.v1.SyncNotebookResponse
|
||||
(*DeleteNotebookRequest)(nil), // 4: engpad.v1.DeleteNotebookRequest
|
||||
(*DeleteNotebookResponse)(nil), // 5: engpad.v1.DeleteNotebookResponse
|
||||
(*ListNotebooksRequest)(nil), // 6: engpad.v1.ListNotebooksRequest
|
||||
(*ListNotebooksResponse)(nil), // 7: engpad.v1.ListNotebooksResponse
|
||||
(*NotebookSummary)(nil), // 8: engpad.v1.NotebookSummary
|
||||
(*CreateShareLinkRequest)(nil), // 9: engpad.v1.CreateShareLinkRequest
|
||||
(*CreateShareLinkResponse)(nil), // 10: engpad.v1.CreateShareLinkResponse
|
||||
(*RevokeShareLinkRequest)(nil), // 11: engpad.v1.RevokeShareLinkRequest
|
||||
(*RevokeShareLinkResponse)(nil), // 12: engpad.v1.RevokeShareLinkResponse
|
||||
(*ListShareLinksRequest)(nil), // 13: engpad.v1.ListShareLinksRequest
|
||||
(*ListShareLinksResponse)(nil), // 14: engpad.v1.ListShareLinksResponse
|
||||
(*ShareLinkInfo)(nil), // 15: engpad.v1.ShareLinkInfo
|
||||
(*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp
|
||||
(*GetNotebookRequest)(nil), // 4: engpad.v1.GetNotebookRequest
|
||||
(*GetNotebookResponse)(nil), // 5: engpad.v1.GetNotebookResponse
|
||||
(*DeleteNotebookRequest)(nil), // 6: engpad.v1.DeleteNotebookRequest
|
||||
(*DeleteNotebookResponse)(nil), // 7: engpad.v1.DeleteNotebookResponse
|
||||
(*ListNotebooksRequest)(nil), // 8: engpad.v1.ListNotebooksRequest
|
||||
(*ListNotebooksResponse)(nil), // 9: engpad.v1.ListNotebooksResponse
|
||||
(*NotebookSummary)(nil), // 10: engpad.v1.NotebookSummary
|
||||
(*CreateShareLinkRequest)(nil), // 11: engpad.v1.CreateShareLinkRequest
|
||||
(*CreateShareLinkResponse)(nil), // 12: engpad.v1.CreateShareLinkResponse
|
||||
(*RevokeShareLinkRequest)(nil), // 13: engpad.v1.RevokeShareLinkRequest
|
||||
(*RevokeShareLinkResponse)(nil), // 14: engpad.v1.RevokeShareLinkResponse
|
||||
(*ListShareLinksRequest)(nil), // 15: engpad.v1.ListShareLinksRequest
|
||||
(*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{
|
||||
1, // 0: engpad.v1.SyncNotebookRequest.pages:type_name -> engpad.v1.PageData
|
||||
2, // 1: engpad.v1.PageData.strokes:type_name -> engpad.v1.StrokeData
|
||||
16, // 2: engpad.v1.SyncNotebookResponse.synced_at:type_name -> google.protobuf.Timestamp
|
||||
8, // 3: engpad.v1.ListNotebooksResponse.notebooks:type_name -> engpad.v1.NotebookSummary
|
||||
16, // 4: engpad.v1.NotebookSummary.synced_at:type_name -> google.protobuf.Timestamp
|
||||
16, // 5: engpad.v1.CreateShareLinkResponse.expires_at:type_name -> google.protobuf.Timestamp
|
||||
15, // 6: engpad.v1.ListShareLinksResponse.links:type_name -> engpad.v1.ShareLinkInfo
|
||||
16, // 7: engpad.v1.ShareLinkInfo.created_at:type_name -> google.protobuf.Timestamp
|
||||
16, // 8: engpad.v1.ShareLinkInfo.expires_at:type_name -> google.protobuf.Timestamp
|
||||
0, // 9: engpad.v1.EngPadSyncService.SyncNotebook:input_type -> engpad.v1.SyncNotebookRequest
|
||||
4, // 10: engpad.v1.EngPadSyncService.DeleteNotebook:input_type -> engpad.v1.DeleteNotebookRequest
|
||||
6, // 11: engpad.v1.EngPadSyncService.ListNotebooks:input_type -> engpad.v1.ListNotebooksRequest
|
||||
9, // 12: engpad.v1.EngPadSyncService.CreateShareLink:input_type -> engpad.v1.CreateShareLinkRequest
|
||||
11, // 13: engpad.v1.EngPadSyncService.RevokeShareLink:input_type -> engpad.v1.RevokeShareLinkRequest
|
||||
13, // 14: engpad.v1.EngPadSyncService.ListShareLinks:input_type -> engpad.v1.ListShareLinksRequest
|
||||
3, // 15: engpad.v1.EngPadSyncService.SyncNotebook:output_type -> engpad.v1.SyncNotebookResponse
|
||||
5, // 16: engpad.v1.EngPadSyncService.DeleteNotebook:output_type -> engpad.v1.DeleteNotebookResponse
|
||||
7, // 17: engpad.v1.EngPadSyncService.ListNotebooks:output_type -> engpad.v1.ListNotebooksResponse
|
||||
10, // 18: engpad.v1.EngPadSyncService.CreateShareLink:output_type -> engpad.v1.CreateShareLinkResponse
|
||||
12, // 19: engpad.v1.EngPadSyncService.RevokeShareLink:output_type -> engpad.v1.RevokeShareLinkResponse
|
||||
14, // 20: engpad.v1.EngPadSyncService.ListShareLinks:output_type -> engpad.v1.ListShareLinksResponse
|
||||
15, // [15:21] is the sub-list for method output_type
|
||||
9, // [9:15] is the sub-list for method input_type
|
||||
9, // [9:9] is the sub-list for extension type_name
|
||||
9, // [9:9] is the sub-list for extension extendee
|
||||
0, // [0:9] is the sub-list for field type_name
|
||||
18, // 2: engpad.v1.SyncNotebookResponse.synced_at:type_name -> google.protobuf.Timestamp
|
||||
1, // 3: engpad.v1.GetNotebookResponse.pages:type_name -> engpad.v1.PageData
|
||||
18, // 4: engpad.v1.GetNotebookResponse.synced_at:type_name -> google.protobuf.Timestamp
|
||||
10, // 5: engpad.v1.ListNotebooksResponse.notebooks:type_name -> engpad.v1.NotebookSummary
|
||||
18, // 6: engpad.v1.NotebookSummary.synced_at:type_name -> google.protobuf.Timestamp
|
||||
18, // 7: engpad.v1.CreateShareLinkResponse.expires_at:type_name -> google.protobuf.Timestamp
|
||||
17, // 8: engpad.v1.ListShareLinksResponse.links:type_name -> engpad.v1.ShareLinkInfo
|
||||
18, // 9: engpad.v1.ShareLinkInfo.created_at:type_name -> google.protobuf.Timestamp
|
||||
18, // 10: engpad.v1.ShareLinkInfo.expires_at:type_name -> google.protobuf.Timestamp
|
||||
0, // 11: engpad.v1.EngPadSyncService.SyncNotebook:input_type -> engpad.v1.SyncNotebookRequest
|
||||
4, // 12: engpad.v1.EngPadSyncService.GetNotebook:input_type -> engpad.v1.GetNotebookRequest
|
||||
6, // 13: engpad.v1.EngPadSyncService.DeleteNotebook:input_type -> engpad.v1.DeleteNotebookRequest
|
||||
8, // 14: engpad.v1.EngPadSyncService.ListNotebooks:input_type -> engpad.v1.ListNotebooksRequest
|
||||
11, // 15: engpad.v1.EngPadSyncService.CreateShareLink:input_type -> engpad.v1.CreateShareLinkRequest
|
||||
13, // 16: engpad.v1.EngPadSyncService.RevokeShareLink:input_type -> engpad.v1.RevokeShareLinkRequest
|
||||
15, // 17: engpad.v1.EngPadSyncService.ListShareLinks:input_type -> engpad.v1.ListShareLinksRequest
|
||||
3, // 18: engpad.v1.EngPadSyncService.SyncNotebook:output_type -> engpad.v1.SyncNotebookResponse
|
||||
5, // 19: engpad.v1.EngPadSyncService.GetNotebook:output_type -> engpad.v1.GetNotebookResponse
|
||||
7, // 20: engpad.v1.EngPadSyncService.DeleteNotebook:output_type -> engpad.v1.DeleteNotebookResponse
|
||||
9, // 21: engpad.v1.EngPadSyncService.ListNotebooks:output_type -> engpad.v1.ListNotebooksResponse
|
||||
12, // 22: engpad.v1.EngPadSyncService.CreateShareLink:output_type -> engpad.v1.CreateShareLinkResponse
|
||||
14, // 23: engpad.v1.EngPadSyncService.RevokeShareLink:output_type -> engpad.v1.RevokeShareLinkResponse
|
||||
16, // 24: engpad.v1.EngPadSyncService.ListShareLinks:output_type -> engpad.v1.ListShareLinksResponse
|
||||
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() }
|
||||
@@ -1017,7 +1162,7 @@ func file_proto_engpad_v1_sync_proto_init() {
|
||||
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)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 16,
|
||||
NumMessages: 18,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
EngPadSyncService_SyncNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/SyncNotebook"
|
||||
EngPadSyncService_GetNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/GetNotebook"
|
||||
EngPadSyncService_DeleteNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/DeleteNotebook"
|
||||
EngPadSyncService_ListNotebooks_FullMethodName = "/engpad.v1.EngPadSyncService/ListNotebooks"
|
||||
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.
|
||||
type EngPadSyncServiceClient interface {
|
||||
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)
|
||||
ListNotebooks(ctx context.Context, in *ListNotebooksRequest, opts ...grpc.CallOption) (*ListNotebooksResponse, 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
|
||||
}
|
||||
|
||||
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) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DeleteNotebookResponse)
|
||||
@@ -112,6 +124,7 @@ func (c *engPadSyncServiceClient) ListShareLinks(ctx context.Context, in *ListSh
|
||||
// for forward compatibility.
|
||||
type EngPadSyncServiceServer interface {
|
||||
SyncNotebook(context.Context, *SyncNotebookRequest) (*SyncNotebookResponse, error)
|
||||
GetNotebook(context.Context, *GetNotebookRequest) (*GetNotebookResponse, error)
|
||||
DeleteNotebook(context.Context, *DeleteNotebookRequest) (*DeleteNotebookResponse, error)
|
||||
ListNotebooks(context.Context, *ListNotebooksRequest) (*ListNotebooksResponse, error)
|
||||
CreateShareLink(context.Context, *CreateShareLinkRequest) (*CreateShareLinkResponse, error)
|
||||
@@ -130,6 +143,9 @@ type UnimplementedEngPadSyncServiceServer struct{}
|
||||
func (UnimplementedEngPadSyncServiceServer) SyncNotebook(context.Context, *SyncNotebookRequest) (*SyncNotebookResponse, error) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
in := new(DeleteNotebookRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -285,6 +319,10 @@ var EngPadSyncService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "SyncNotebook",
|
||||
Handler: _EngPadSyncService_SyncNotebook_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetNotebook",
|
||||
Handler: _EngPadSyncService_GetNotebook_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "DeleteNotebook",
|
||||
Handler: _EngPadSyncService_DeleteNotebook_Handler,
|
||||
|
||||
@@ -18,10 +18,11 @@ type Config struct {
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
ListenAddr string `toml:"listen_addr"`
|
||||
GRPCAddr string `toml:"grpc_addr"`
|
||||
TLSCert string `toml:"tls_cert"`
|
||||
TLSKey string `toml:"tls_key"`
|
||||
ListenAddr string `toml:"listen_addr"`
|
||||
GRPCAddr string `toml:"grpc_addr"`
|
||||
GRPCPlainAddr string `toml:"grpc_plain_addr"`
|
||||
TLSCert string `toml:"tls_cert"`
|
||||
TLSKey string `toml:"tls_key"`
|
||||
}
|
||||
|
||||
type WebConfig struct {
|
||||
|
||||
@@ -29,8 +29,8 @@ func AuthInterceptor(database *sql.DB) grpc.UnaryServerInterceptor {
|
||||
return nil, status.Error(codes.Unauthenticated, "missing metadata")
|
||||
}
|
||||
|
||||
usernames := md.Get("username")
|
||||
passwords := md.Get("password")
|
||||
usernames := md.Get("x-engpad-username")
|
||||
passwords := md.Get("x-engpad-password")
|
||||
if len(usernames) == 0 || len(passwords) == 0 {
|
||||
return nil, status.Error(codes.Unauthenticated, "missing credentials")
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
TLSCert string
|
||||
TLSKey string
|
||||
DB *sql.DB
|
||||
BaseURL string
|
||||
Addr string
|
||||
PlainAddr string
|
||||
TLSCert string
|
||||
TLSKey string
|
||||
DB *sql.DB
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
// Start creates and starts the gRPC server. It returns the server so the
|
||||
@@ -31,7 +32,7 @@ func Start(cfg Config) (*grpc.Server, error) {
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS13,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -114,6 +114,71 @@ func (s *SyncService) SyncNotebook(ctx context.Context, req *pb.SyncNotebookRequ
|
||||
}, 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) {
|
||||
userID, ok := UserIDFromContext(ctx)
|
||||
if !ok {
|
||||
|
||||
@@ -25,6 +25,7 @@ func (ws *WebServer) handleLoginSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID, err := auth.AuthenticateUser(ws.db, username, password)
|
||||
if err != nil {
|
||||
slog.Error("login failed", "username", username, "error", err)
|
||||
ws.render(w, "login.html", map[string]string{"Error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
@@ -127,15 +128,21 @@ func (ws *WebServer) handleNotebook(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
pages = append(pages, pageInfo{
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
// Load share links for this notebook.
|
||||
shareLinks, _ := share.ListLinks(ws.db, id, ws.baseURL)
|
||||
|
||||
ws.render(w, "notebook.html", map[string]any{
|
||||
"Title": title,
|
||||
"Pages": pages,
|
||||
"PDFLink": fmt.Sprintf("/v1/notebooks/%d/pdf", id),
|
||||
"ID": id,
|
||||
"Title": title,
|
||||
"Pages": pages,
|
||||
"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,
|
||||
"PageNumber": num,
|
||||
"BackLink": fmt.Sprintf("/notebooks/%d", id),
|
||||
"SVGLink": fmt.Sprintf("/v1/notebooks/%d/pages/%d/svg", id, num),
|
||||
"JPGLink": fmt.Sprintf("/v1/notebooks/%d/pages/%d/jpg", id, num),
|
||||
"PDFLink": fmt.Sprintf("/v1/notebooks/%d/pdf", id),
|
||||
"SVGLink": fmt.Sprintf("/notebooks/%d/pages/%d/svg", id, num),
|
||||
"JPGLink": fmt.Sprintf("/notebooks/%d/pages/%d/jpg", id, num),
|
||||
"PDFLink": fmt.Sprintf("/notebooks/%d/pdf", id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -201,6 +208,7 @@ func (ws *WebServer) handleShareNotebook(w http.ResponseWriter, r *http.Request)
|
||||
"Title": title,
|
||||
"Pages": pages,
|
||||
"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 ---
|
||||
|
||||
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) {
|
||||
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")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
db *sql.DB
|
||||
baseURL string
|
||||
tmpl *template.Template
|
||||
tmpls map[string]*template.Template
|
||||
webauthn *webauthn.WebAuthn
|
||||
mu sync.Mutex
|
||||
sessions map[string]*webauthn.SessionData
|
||||
@@ -43,15 +43,32 @@ func Start(cfg Config) (*http.Server, error) {
|
||||
return nil, fmt.Errorf("template fs: %w", err)
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFS(templateFS, "*.html")
|
||||
layoutData, err := fs.ReadFile(templateFS, "layout.html")
|
||||
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{
|
||||
db: cfg.DB,
|
||||
baseURL: cfg.BaseURL,
|
||||
tmpl: tmpl,
|
||||
tmpls: tmpls,
|
||||
sessions: make(map[string]*webauthn.SessionData),
|
||||
}
|
||||
|
||||
@@ -81,6 +98,9 @@ func Start(cfg Config) (*http.Server, error) {
|
||||
// Share routes (no auth)
|
||||
r.Get("/s/{token}", ws.handleShareNotebook)
|
||||
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
|
||||
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/{id}", ws.handleNotebook)
|
||||
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)
|
||||
|
||||
// WebAuthn authenticated routes (registration + key management)
|
||||
|
||||
@@ -9,6 +9,7 @@ import "google/protobuf/timestamp.proto";
|
||||
|
||||
service EngPadSyncService {
|
||||
rpc SyncNotebook(SyncNotebookRequest) returns (SyncNotebookResponse);
|
||||
rpc GetNotebook(GetNotebookRequest) returns (GetNotebookResponse);
|
||||
rpc DeleteNotebook(DeleteNotebookRequest) returns (DeleteNotebookResponse);
|
||||
rpc ListNotebooks(ListNotebooksRequest) returns (ListNotebooksResponse);
|
||||
rpc CreateShareLink(CreateShareLinkRequest) returns (CreateShareLinkResponse);
|
||||
@@ -42,6 +43,19 @@ message SyncNotebookResponse {
|
||||
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 {
|
||||
int64 notebook_id = 1;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
.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 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; }
|
||||
.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; }
|
||||
label { display: block; margin-bottom: 0.25rem; font-weight: bold; }
|
||||
.form-group { margin-bottom: 1rem; }
|
||||
|
||||
@@ -4,7 +4,32 @@
|
||||
<h1>{{.Title}}</h1>
|
||||
<div class="actions">
|
||||
<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>
|
||||
|
||||
{{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">
|
||||
{{range .Pages}}
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user