Compare commits

6 Commits

Author SHA1 Message Date
f1b67b9909 Add GetNotebook RPC for pulling complete notebook data
New RPC returns notebook metadata, all pages, and all strokes for
a given server-side notebook ID. Enables desktop and other clients
to download notebooks from the server (pull sync).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:06:20 -07:00
651eabe995 Add notebook delete, fix button styling, rename Share button
- Add delete notebook handler with ownership check and CASCADE delete
- Rename "Create Share Link" to "Share"
- Fix action button heights: use inline-flex + align-items for
  consistent sizing across <a> and <button> elements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:43:57 -07:00
aeb12d9f50 Add rendering routes and share UI to web server
The web UI was linking to /v1/ REST API paths that aren't served
through nginx. Added SVG/JPG/PDF rendering and share link endpoints
directly to the web server so everything works through port 443.

- Add render.go with SVG, JPG, PDF handlers for auth and share paths
- Register render routes and share management routes in web server
- Update template links from /v1/... to /notebooks/... paths
- Add share link creation, display, and revocation to notebook view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:22:07 -07:00
ab2884a8e9 Fix gRPC auth metadata keys, allow TLS 1.2 for Android clients
- Read x-engpad-username/x-engpad-password from gRPC metadata
  (matching what the Android client sends)
- Allow TLS 1.2 on gRPC port — Android's BoringSSL/OkHttp transport
  does not negotiate TLS 1.3 without Conscrypt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:08:24 -07:00
691301dade Update docs for Docker-on-deimos deployment, add grpc_plain_addr option
- ARCHITECTURE.md: document nginx + direct gRPC topology, add
  grpc_plain_addr config, update cert filenames to Let's Encrypt
  convention, add passwd to CLI table
- RUNBOOK.md: replace systemctl/journalctl with docker commands,
  fix cert path references, improve sync troubleshooting steps
- Example config: update cert paths, document grpc_plain_addr option
- grpcserver: add optional plaintext gRPC listener for reverse proxy
- config: add GRPCPlainAddr field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:58:01 -07:00
2185bbe563 Add passwd command, fix template rendering, update deployment docs
- Add `passwd` CLI command to reset user passwords
- Fix web UI templates: parse each page template with layout so blocks
  render correctly (was outputting empty pages)
- Add login error logging for debugging auth failures
- Update README with deploy workflow and container management commands
- Update RUNBOOK for Docker-on-deimos deployment (replaces systemd refs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:27:31 -07:00
19 changed files with 956 additions and 210 deletions

View File

@@ -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 |

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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
}

View File

@@ -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,

View File

@@ -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"

View File

@@ -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,
}, },

View File

@@ -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,

View File

@@ -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"`
} }

View File

@@ -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")
} }

View File

@@ -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
} }

View File

@@ -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 {

View File

@@ -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)
} }
} }

View 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
}

View File

@@ -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)

View File

@@ -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;
} }

View File

@@ -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; }

View File

@@ -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>