feat(05-01): add push_subscriptions table + calendar_events.title column; VAPID env wiring
- schema.ts: new pushSubscriptions mysqlTable (user_id FK cascade, endpoint unique, p256dh, auth) - schema.ts: add nullable title varchar(500) to calendarEvents after rawVevent (D-02/NOTIF-01) - 0003_same_xavin.sql: CREATE TABLE push_subscriptions + ALTER calendar_events ADD title - migration applied to dev DB via db:generate + db:migrate (NOT db:push per anti-pattern) - docker-compose.yml: inject VAPID_PUBLIC_KEY/PRIVATE_KEY/SUBJECT into api environment block - .env.example: document all three VAPID vars with placeholders + generation instructions
This commit is contained in:
+33
-18
@@ -1,22 +1,37 @@
|
|||||||
# Database
|
# FamilySync — environment variable reference
|
||||||
DB_HOST=mariadb
|
# Copy to .env and fill in real values. .env is gitignored and must never be committed.
|
||||||
DB_PORT=3306
|
#
|
||||||
DB_USER=familysync
|
# Deployment: these vars are injected into the Docker Compose `api` service via
|
||||||
DB_PASSWORD=
|
# the `environment:` block in docker-compose.yml. All values are resolved at
|
||||||
DB_NAME=familysync
|
# container start time from the host .env file.
|
||||||
DB_ROOT_PASSWORD=
|
|
||||||
|
|
||||||
# OIDC (Authelia) — fill in after registering the client
|
# ── MariaDB ───────────────────────────────────────────────────────────────────
|
||||||
OIDC_AUTH_SECRET=
|
DB_PASSWORD=change_me_strong_password
|
||||||
OIDC_ISSUER=
|
DB_ROOT_PASSWORD=change_me_root_password
|
||||||
|
|
||||||
|
# ── OIDC / Authelia ───────────────────────────────────────────────────────────
|
||||||
|
# Authorization code + PKCE flow (client_secret_basic). See CLAUDE.md §Authelia.
|
||||||
|
OIDC_AUTH_SECRET=change_me_32_char_secret_minimum
|
||||||
|
OIDC_ISSUER=https://auth.example.com
|
||||||
OIDC_CLIENT_ID=familysync
|
OIDC_CLIENT_ID=familysync
|
||||||
OIDC_CLIENT_SECRET=
|
OIDC_CLIENT_SECRET=change_me_client_secret
|
||||||
OIDC_REDIRECT_URI=https://familysync.yourdomain.com/callback
|
OIDC_REDIRECT_URI=https://familysync.example.com/callback
|
||||||
OIDC_AUTH_EXTERNAL_URL=https://familysync.yourdomain.com
|
OIDC_AUTH_EXTERNAL_URL=https://auth.example.com
|
||||||
|
# Scopes granted by the Authelia client definition (must include offline_access for
|
||||||
|
# refresh-token session persistence).
|
||||||
|
OIDC_SCOPES=openid profile email offline_access
|
||||||
|
|
||||||
# CalDAV broker encryption key — generate with:
|
# ── App-password encryption ───────────────────────────────────────────────────
|
||||||
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
# 32-byte hex key used to AES-256-GCM encrypt Fastmail app passwords at rest.
|
||||||
APP_PASSWORD_ENCRYPTION_KEY=
|
# Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
APP_PASSWORD_ENCRYPTION_KEY=change_me_64_hex_chars
|
||||||
|
|
||||||
# DEV ONLY — injects a fixed dev user, skips Authelia. Hard-disabled when NODE_ENV=production. NEVER set in prod.
|
# ── VAPID — Web Push notifications (Phase 5) ─────────────────────────────────
|
||||||
# DEV_AUTH_BYPASS=true
|
# Generate a keypair (one-time, per deployment):
|
||||||
|
# npx web-push generate-vapid-keys --json
|
||||||
|
# VAPID_PUBLIC_KEY is served to the PWA at GET /api/push/vapid-public-key (no secret).
|
||||||
|
# VAPID_PRIVATE_KEY signs push messages — treat as a secret; never commit it.
|
||||||
|
# VAPID_SUBJECT is a contact URL (mailto: or https:) sent to push services.
|
||||||
|
VAPID_PUBLIC_KEY=replace_with_url_safe_base64_public_key
|
||||||
|
VAPID_PRIVATE_KEY=replace_with_url_safe_base64_private_key
|
||||||
|
VAPID_SUBJECT=mailto:admin@familysync.example.com
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE `push_subscriptions` (
|
||||||
|
`id` int AUTO_INCREMENT NOT NULL,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`endpoint` text NOT NULL,
|
||||||
|
`p256dh` text NOT NULL,
|
||||||
|
`auth` varchar(256) NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||||
|
`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT `push_subscriptions_id` PRIMARY KEY(`id`),
|
||||||
|
CONSTRAINT `uniq_push_endpoint` UNIQUE(`endpoint`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `calendar_events` ADD `title` varchar(500);--> statement-breakpoint
|
||||||
|
ALTER TABLE `push_subscriptions` ADD CONSTRAINT `push_subscriptions_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_push_subscriptions_user_id` ON `push_subscriptions` (`user_id`);
|
||||||
@@ -0,0 +1,969 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "091236ec-ff3a-4982-8e9d-022a398f24f9",
|
||||||
|
"prevId": "3a73e735-2d98-4ddb-b68a-fc1a3e6a81e8",
|
||||||
|
"tables": {
|
||||||
|
"calendar_events": {
|
||||||
|
"name": "calendar_events",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"calendar_id": {
|
||||||
|
"name": "calendar_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"uid": {
|
||||||
|
"name": "uid",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"etag": {
|
||||||
|
"name": "etag",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"object_url": {
|
||||||
|
"name": "object_url",
|
||||||
|
"type": "varchar(1024)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"raw_vevent": {
|
||||||
|
"name": "raw_vevent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"dtstart_utc": {
|
||||||
|
"name": "dtstart_utc",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"dtstart_date": {
|
||||||
|
"name": "dtstart_date",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"all_day": {
|
||||||
|
"name": "all_day",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"has_rrule": {
|
||||||
|
"name": "has_rrule",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_calendar_events_dtstart_utc": {
|
||||||
|
"name": "idx_calendar_events_dtstart_utc",
|
||||||
|
"columns": [
|
||||||
|
"dtstart_utc"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_calendar_events_dtstart_date": {
|
||||||
|
"name": "idx_calendar_events_dtstart_date",
|
||||||
|
"columns": [
|
||||||
|
"dtstart_date"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_calendar_events_has_rrule": {
|
||||||
|
"name": "idx_calendar_events_has_rrule",
|
||||||
|
"columns": [
|
||||||
|
"has_rrule"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"calendar_events_calendar_id_calendars_id_fk": {
|
||||||
|
"name": "calendar_events_calendar_id_calendars_id_fk",
|
||||||
|
"tableFrom": "calendar_events",
|
||||||
|
"tableTo": "calendars",
|
||||||
|
"columnsFrom": [
|
||||||
|
"calendar_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"calendar_events_id": {
|
||||||
|
"name": "calendar_events_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"uniq_calendar_uid": {
|
||||||
|
"name": "uniq_calendar_uid",
|
||||||
|
"columns": [
|
||||||
|
"calendar_id",
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"calendar_outbox": {
|
||||||
|
"name": "calendar_outbox",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"name": "operation",
|
||||||
|
"type": "enum('create','update','delete')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "enum('pending','done','failed','dead')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'pending'"
|
||||||
|
},
|
||||||
|
"uid": {
|
||||||
|
"name": "uid",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"calendar_url": {
|
||||||
|
"name": "calendar_url",
|
||||||
|
"type": "varchar(1024)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"calendar_object_url": {
|
||||||
|
"name": "calendar_object_url",
|
||||||
|
"type": "varchar(1024)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"etag": {
|
||||||
|
"name": "etag",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"name": "payload",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"attempt_count": {
|
||||||
|
"name": "attempt_count",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"next_attempt_at": {
|
||||||
|
"name": "next_attempt_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"last_error": {
|
||||||
|
"name": "last_error",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"group_id": {
|
||||||
|
"name": "group_id",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_outbox_user_status": {
|
||||||
|
"name": "idx_outbox_user_status",
|
||||||
|
"columns": [
|
||||||
|
"user_id",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_outbox_next_attempt": {
|
||||||
|
"name": "idx_outbox_next_attempt",
|
||||||
|
"columns": [
|
||||||
|
"next_attempt_at",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_outbox_uid": {
|
||||||
|
"name": "idx_outbox_uid",
|
||||||
|
"columns": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"calendar_outbox_user_id_users_id_fk": {
|
||||||
|
"name": "calendar_outbox_user_id_users_id_fk",
|
||||||
|
"tableFrom": "calendar_outbox",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"calendar_outbox_id": {
|
||||||
|
"name": "calendar_outbox_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"calendars": {
|
||||||
|
"name": "calendars",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "varchar(1024)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"name": "display_name",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "varchar(7)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ctag": {
|
||||||
|
"name": "ctag",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sync_token": {
|
||||||
|
"name": "sync_token",
|
||||||
|
"type": "varchar(1024)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_synced_at": {
|
||||||
|
"name": "last_synced_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_shared": {
|
||||||
|
"name": "is_shared",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_calendars_user_id": {
|
||||||
|
"name": "idx_calendars_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"calendars_user_id_users_id_fk": {
|
||||||
|
"name": "calendars_user_id_users_id_fk",
|
||||||
|
"tableFrom": "calendars",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"calendars_id": {
|
||||||
|
"name": "calendars_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"uniq_calendar_user_url": {
|
||||||
|
"name": "uniq_calendar_user_url",
|
||||||
|
"columns": [
|
||||||
|
"user_id",
|
||||||
|
"url"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"list_items": {
|
||||||
|
"name": "list_items",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"list_id": {
|
||||||
|
"name": "list_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"name": "text",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"checked": {
|
||||||
|
"name": "checked",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"rank": {
|
||||||
|
"name": "rank",
|
||||||
|
"type": "varchar(255) COLLATE utf8mb4_bin",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_list_items_list_id_rank": {
|
||||||
|
"name": "idx_list_items_list_id_rank",
|
||||||
|
"columns": [
|
||||||
|
"list_id",
|
||||||
|
"rank"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_list_items_list_id_checked": {
|
||||||
|
"name": "idx_list_items_list_id_checked",
|
||||||
|
"columns": [
|
||||||
|
"list_id",
|
||||||
|
"checked"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"list_items_list_id_lists_id_fk": {
|
||||||
|
"name": "list_items_list_id_lists_id_fk",
|
||||||
|
"tableFrom": "list_items",
|
||||||
|
"tableTo": "lists",
|
||||||
|
"columnsFrom": [
|
||||||
|
"list_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"list_items_id": {
|
||||||
|
"name": "list_items_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"list_shares": {
|
||||||
|
"name": "list_shares",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"list_id": {
|
||||||
|
"name": "list_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_list_shares_user_id": {
|
||||||
|
"name": "idx_list_shares_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"list_shares_list_id_lists_id_fk": {
|
||||||
|
"name": "list_shares_list_id_lists_id_fk",
|
||||||
|
"tableFrom": "list_shares",
|
||||||
|
"tableTo": "lists",
|
||||||
|
"columnsFrom": [
|
||||||
|
"list_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"list_shares_user_id_users_id_fk": {
|
||||||
|
"name": "list_shares_user_id_users_id_fk",
|
||||||
|
"tableFrom": "list_shares",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"list_shares_id": {
|
||||||
|
"name": "list_shares_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"uniq_list_share": {
|
||||||
|
"name": "uniq_list_share",
|
||||||
|
"columns": [
|
||||||
|
"list_id",
|
||||||
|
"user_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"lists": {
|
||||||
|
"name": "lists",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"owner_id": {
|
||||||
|
"name": "owner_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_shared": {
|
||||||
|
"name": "is_shared",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_lists_owner_id": {
|
||||||
|
"name": "idx_lists_owner_id",
|
||||||
|
"columns": [
|
||||||
|
"owner_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"lists_owner_id_users_id_fk": {
|
||||||
|
"name": "lists_owner_id_users_id_fk",
|
||||||
|
"tableFrom": "lists",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"lists_id": {
|
||||||
|
"name": "lists_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"member_credentials": {
|
||||||
|
"name": "member_credentials",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"encrypted_password": {
|
||||||
|
"name": "encrypted_password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"fastmail_email": {
|
||||||
|
"name": "fastmail_email",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_member_credentials_user_id": {
|
||||||
|
"name": "idx_member_credentials_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"member_credentials_user_id_users_id_fk": {
|
||||||
|
"name": "member_credentials_user_id_users_id_fk",
|
||||||
|
"tableFrom": "member_credentials",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"member_credentials_id": {
|
||||||
|
"name": "member_credentials_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"push_subscriptions": {
|
||||||
|
"name": "push_subscriptions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"name": "endpoint",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"p256dh": {
|
||||||
|
"name": "p256dh",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"name": "auth",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_push_subscriptions_user_id": {
|
||||||
|
"name": "idx_push_subscriptions_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"push_subscriptions_user_id_users_id_fk": {
|
||||||
|
"name": "push_subscriptions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "push_subscriptions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"push_subscriptions_id": {
|
||||||
|
"name": "push_subscriptions_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"uniq_push_endpoint": {
|
||||||
|
"name": "uniq_push_endpoint",
|
||||||
|
"columns": [
|
||||||
|
"endpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"oidc_iss": {
|
||||||
|
"name": "oidc_iss",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"oidc_sub": {
|
||||||
|
"name": "oidc_sub",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"name": "display_name",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "varchar(7)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"users_id": {
|
||||||
|
"name": "users_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"uniq_oidc_identity": {
|
||||||
|
"name": "uniq_oidc_identity",
|
||||||
|
"columns": [
|
||||||
|
"oidc_iss",
|
||||||
|
"oidc_sub"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"tables": {},
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,13 @@
|
|||||||
"when": 1781029240528,
|
"when": 1781029240528,
|
||||||
"tag": "0002_yielding_mattie_franklin",
|
"tag": "0002_yielding_mattie_franklin",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1781052360194,
|
||||||
|
"tag": "0003_same_xavin",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -119,6 +119,9 @@ export const calendarEvents = mysqlTable(
|
|||||||
etag: varchar('etag', { length: 256 }),
|
etag: varchar('etag', { length: 256 }),
|
||||||
objectUrl: varchar('object_url', { length: 1024 }), // CalDAV object URL; populated from obj.url by sync.ts (D-08)
|
objectUrl: varchar('object_url', { length: 1024 }), // CalDAV object URL; populated from obj.url by sync.ts (D-08)
|
||||||
rawVevent: text('raw_vevent').notNull(), // full VCALENDAR/VEVENT string for ical.js
|
rawVevent: text('raw_vevent').notNull(), // full VCALENDAR/VEVENT string for ical.js
|
||||||
|
// Readable event title extracted from VEVENT SUMMARY by sync.ts (D-02/NOTIF-01).
|
||||||
|
// Nullable: populated by Phase 5 sync update; pre-existing rows remain NULL until resynced.
|
||||||
|
title: varchar('title', { length: 500 }),
|
||||||
dtstartUtc: timestamp('dtstart_utc'), // NULL for all-day events
|
dtstartUtc: timestamp('dtstart_utc'), // NULL for all-day events
|
||||||
dtstartDate: date('dtstart_date'), // set for all-day events; NULL for timed
|
dtstartDate: date('dtstart_date'), // set for all-day events; NULL for timed
|
||||||
allDay: boolean('all_day').default(false).notNull(),
|
allDay: boolean('all_day').default(false).notNull(),
|
||||||
@@ -223,6 +226,33 @@ export const listShares = mysqlTable(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push notification subscriptions (Phase 5 — Web Push).
|
||||||
|
*
|
||||||
|
* Member-count-agnostic (D-18): one row per browser push subscription endpoint.
|
||||||
|
* endpoint is globally unique — a single device endpoint belongs to exactly one user.
|
||||||
|
* Cascade delete on user removal keeps subscriptions clean without orphan cleanup jobs.
|
||||||
|
*/
|
||||||
|
export const pushSubscriptions = mysqlTable(
|
||||||
|
'push_subscriptions',
|
||||||
|
{
|
||||||
|
id: int().primaryKey().autoincrement(),
|
||||||
|
userId: int('user_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
endpoint: text('endpoint').notNull(),
|
||||||
|
p256dh: text('p256dh').notNull(),
|
||||||
|
auth: varchar('auth', { length: 256 }).notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
// One endpoint is globally unique — a single device maps to exactly one subscription row
|
||||||
|
unique('uniq_push_endpoint').on(t.endpoint),
|
||||||
|
index('idx_push_subscriptions_user_id').on(t.userId),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Items within a list.
|
* Items within a list.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ services:
|
|||||||
# persistence (D-12/AUTH-02) and must also be allowed on the Authelia client.
|
# persistence (D-12/AUTH-02) and must also be allowed on the Authelia client.
|
||||||
OIDC_SCOPES: ${OIDC_SCOPES:-openid profile email offline_access}
|
OIDC_SCOPES: ${OIDC_SCOPES:-openid profile email offline_access}
|
||||||
APP_PASSWORD_ENCRYPTION_KEY: ${APP_PASSWORD_ENCRYPTION_KEY:-}
|
APP_PASSWORD_ENCRYPTION_KEY: ${APP_PASSWORD_ENCRYPTION_KEY:-}
|
||||||
|
# VAPID keys for Web Push notifications (Phase 5).
|
||||||
|
# Generate with: npx web-push generate-vapid-keys --json
|
||||||
|
# Private key must only live in the gitignored .env — never commit it.
|
||||||
|
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY}
|
||||||
|
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY}
|
||||||
|
VAPID_SUBJECT: ${VAPID_SUBJECT}
|
||||||
depends_on:
|
depends_on:
|
||||||
mariadb:
|
mariadb:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
Reference in New Issue
Block a user