Add emails
This commit is contained in:
parent
8c0fe19cf9
commit
5792decc8f
8 changed files with 192 additions and 244 deletions
258
package-lock.json
generated
258
package-lock.json
generated
|
@ -13,15 +13,15 @@
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"astro": "^4.3.2",
|
"astro": "^4.3.2",
|
||||||
"better-sqlite3": "^9.4.0",
|
"better-sqlite3": "^9.4.0",
|
||||||
|
"change-case": "^5.4.2",
|
||||||
|
"html-template-tag": "^4.0.1",
|
||||||
"kysely": "^0.27.2",
|
"kysely": "^0.27.2",
|
||||||
"pg": "^8.11.3",
|
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.9",
|
"@types/better-sqlite3": "^7.6.9",
|
||||||
"@types/pg": "^8.11.0",
|
|
||||||
"prisma": "^5.9.1",
|
"prisma": "^5.9.1",
|
||||||
"prisma-kysely": "^1.7.1"
|
"prisma-kysely": "^1.7.1"
|
||||||
}
|
}
|
||||||
|
@ -1807,74 +1807,6 @@
|
||||||
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
|
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/pg": {
|
|
||||||
"version": "8.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.0.tgz",
|
|
||||||
"integrity": "sha512-sDAlRiBNthGjNFfvt0k6mtotoVYVQ63pA8R4EMWka7crawSR60waVYR0HAgmPRs/e2YaeJTD/43OoZ3PFw80pw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*",
|
|
||||||
"pg-protocol": "*",
|
|
||||||
"pg-types": "^4.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/pg/node_modules/pg-types": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"pg-int8": "1.0.1",
|
|
||||||
"pg-numeric": "1.0.2",
|
|
||||||
"postgres-array": "~3.0.1",
|
|
||||||
"postgres-bytea": "~3.0.0",
|
|
||||||
"postgres-date": "~2.1.0",
|
|
||||||
"postgres-interval": "^3.0.0",
|
|
||||||
"postgres-range": "^1.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/pg/node_modules/postgres-array": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/pg/node_modules/postgres-bytea": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"obuf": "~1.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/pg/node_modules/postgres-date": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/pg/node_modules/postgres-interval": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/retry": {
|
"node_modules/@types/retry": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||||
|
@ -2681,14 +2613,6 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer-writer": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/camelcase": {
|
"node_modules/camelcase": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
|
||||||
|
@ -2749,6 +2673,11 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/change-case": {
|
||||||
|
"version": "5.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.2.tgz",
|
||||||
|
"integrity": "sha512-WB3UiTDpT+vrTilAWaJS4gaIH/jc1He4H9f6erQvraUYas90uWT0JOYFkG1imdNv710XJ6gJvqynrgOHc4ihDA=="
|
||||||
|
},
|
||||||
"node_modules/character-entities": {
|
"node_modules/character-entities": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
|
||||||
|
@ -4278,11 +4207,24 @@
|
||||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/html-es6cape": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-es6cape/-/html-es6cape-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-utzhH8rq2VABdW1LsPdv5tmxeMNOtP83If0jKCa79xPBgLWfcMvdf9K+EZoxJ5P7KioCxTs6WBnSDWLQHJ2lWA=="
|
||||||
|
},
|
||||||
"node_modules/html-escaper": {
|
"node_modules/html-escaper": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||||||
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
|
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/html-template-tag": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-template-tag/-/html-template-tag-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-c8a1i2G4aKkas2CXqagh89hB6uzUxlU65gUPQkGhkiFImTEboUmBmMqXZhmoBRqtJtYLtCc2wofT/I7qJF+1Jw==",
|
||||||
|
"dependencies": {
|
||||||
|
"html-es6cape": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-void-elements": {
|
"node_modules/html-void-elements": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||||
|
@ -6134,12 +6076,6 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/obuf": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
@ -6401,11 +6337,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/packet-reader": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
|
|
||||||
},
|
|
||||||
"node_modules/parse-json": {
|
"node_modules/parse-json": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||||
|
@ -6521,98 +6452,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pg": {
|
|
||||||
"version": "8.11.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
|
|
||||||
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
|
|
||||||
"dependencies": {
|
|
||||||
"buffer-writer": "2.0.0",
|
|
||||||
"packet-reader": "1.0.0",
|
|
||||||
"pg-connection-string": "^2.6.2",
|
|
||||||
"pg-pool": "^3.6.1",
|
|
||||||
"pg-protocol": "^1.6.0",
|
|
||||||
"pg-types": "^2.1.0",
|
|
||||||
"pgpass": "1.x"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"pg-cloudflare": "^1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"pg-native": ">=3.0.1"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"pg-native": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-cloudflare": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/pg-connection-string": {
|
|
||||||
"version": "2.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
|
|
||||||
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
|
|
||||||
},
|
|
||||||
"node_modules/pg-int8": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-numeric": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-pool": {
|
|
||||||
"version": "3.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
|
|
||||||
"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"pg": ">=8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-protocol": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
|
|
||||||
},
|
|
||||||
"node_modules/pg-types": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
|
||||||
"dependencies": {
|
|
||||||
"pg-int8": "1.0.1",
|
|
||||||
"postgres-array": "~2.0.0",
|
|
||||||
"postgres-bytea": "~1.0.0",
|
|
||||||
"postgres-date": "~1.0.4",
|
|
||||||
"postgres-interval": "^1.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pgpass": {
|
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
|
||||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
|
||||||
"dependencies": {
|
|
||||||
"split2": "^4.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||||
|
@ -6834,47 +6673,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||||
},
|
},
|
||||||
"node_modules/postgres-array": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postgres-bytea": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postgres-date": {
|
|
||||||
"version": "1.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
|
||||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postgres-interval": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"xtend": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postgres-range": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/prebuild-install": {
|
"node_modules/prebuild-install": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
||||||
|
@ -8358,14 +8156,6 @@
|
||||||
"integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
|
"integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/split2": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sprintf-js": {
|
"node_modules/sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
|
@ -9687,14 +9477,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
"node_modules/xtend": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"astro": "^4.3.2",
|
"astro": "^4.3.2",
|
||||||
"better-sqlite3": "^9.4.0",
|
"better-sqlite3": "^9.4.0",
|
||||||
|
"change-case": "^5.4.2",
|
||||||
|
"html-template-tag": "^4.0.1",
|
||||||
"kysely": "^0.27.2",
|
"kysely": "^0.27.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
|
54
src/lib/postal-client.ts
Normal file
54
src/lib/postal-client.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { snakeCase } from "change-case";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
interface ClientConfig {
|
||||||
|
key: string;
|
||||||
|
host: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For more options, see: http://apiv1.postalserver.io/controllers/send/message.html
|
||||||
|
*/
|
||||||
|
interface SendConfig {
|
||||||
|
to: Array<string>;
|
||||||
|
cc?: Array<string>;
|
||||||
|
bcc?: Array<string>;
|
||||||
|
from?: string;
|
||||||
|
sender?: string;
|
||||||
|
subject?: string;
|
||||||
|
replyTo?: string;
|
||||||
|
plainBody?: string;
|
||||||
|
htmlBody?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseSchema = z.object({
|
||||||
|
status: z.union([z.literal("success"), z.literal("error")]),
|
||||||
|
data: z.record(z.any()),
|
||||||
|
});
|
||||||
|
|
||||||
|
function convertObjectKeys<T>(
|
||||||
|
obj: Record<string, any>,
|
||||||
|
fn: (key: string) => string
|
||||||
|
): Record<string, any> {
|
||||||
|
const entries = Object.entries(obj).map(([key, val]) => [fn(key), val]);
|
||||||
|
return Object.fromEntries(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createClient({ host, key }: ClientConfig) {
|
||||||
|
return {
|
||||||
|
async send(config: SendConfig) {
|
||||||
|
const url = join(host, "/api/v1/send/message");
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Server-API-Key": key,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(convertObjectKeys(config, snakeCase)),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const parsed = responseSchema.parse(data);
|
||||||
|
return parsed;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
6
src/mail.ts
Normal file
6
src/mail.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { createClient } from "./lib/postal-client";
|
||||||
|
|
||||||
|
export const mailClient = createClient({
|
||||||
|
key: import.meta.env.POSTAL_KEY,
|
||||||
|
host: import.meta.env.POSTAL_HOST,
|
||||||
|
});
|
46
src/mails/confirm-email.ts
Normal file
46
src/mails/confirm-email.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import html from "html-template-tag";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
lang: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function renderMail({ lang, href }: Params) {
|
||||||
|
const htmlBody = html`<!DOCTYPE html>
|
||||||
|
<html lang="${lang}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Potvrď prosím svůj e-mail</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
|
||||||
|
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1.25em 2em;
|
||||||
|
background-color: #a71122;
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>${t("confirm_email.title")}</h1>
|
||||||
|
<p>${t("confirm_email.body")}</p>
|
||||||
|
<p><a class="button" href="${href}">${t("confirm_email.link")}</a></p>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
const plainBody = `# ${t("confirm_email.title")}
|
||||||
|
|
||||||
|
${t("confirm_email.body")}
|
||||||
|
${href}`;
|
||||||
|
return { htmlBody, plainBody };
|
||||||
|
}
|
18
src/pages/[lang]/preview/confirm-email.html.ts
Normal file
18
src/pages/[lang]/preview/confirm-email.html.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import renderMail from "../../../mails/confirm-email";
|
||||||
|
import type { APIContext } from "astro";
|
||||||
|
import type { Lang } from "../../../lang";
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
interface Params extends Record<string, string> {
|
||||||
|
lang: Lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET({ params: { lang } }: APIContext<never, Params>) {
|
||||||
|
const { htmlBody } = renderMail({ lang, href: "" });
|
||||||
|
return new Response(htmlBody, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
18
src/pages/[lang]/preview/confirm-email.txt.ts
Normal file
18
src/pages/[lang]/preview/confirm-email.txt.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import renderMail from "../../../mails/confirm-email";
|
||||||
|
import type { APIContext } from "astro";
|
||||||
|
import type { Lang } from "../../../lang";
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
interface Params extends Record<string, string> {
|
||||||
|
lang: Lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET({ params: { lang } }: APIContext<never, Params>) {
|
||||||
|
const { plainBody } = renderMail({ lang, href: "" });
|
||||||
|
return new Response(plainBody, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import { letterFormSchema } from "../../letter/schema";
|
||||||
import { db } from "../../db";
|
import { db } from "../../db";
|
||||||
import { generateToken } from "../../utils";
|
import { generateToken } from "../../utils";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { mailClient } from "../../mail";
|
||||||
|
import renderMail from "../../mails/confirm-email";
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
|
@ -40,23 +42,43 @@ export async function POST({ request, params: { lang }, redirect }: APIContext<n
|
||||||
|
|
||||||
const confirmationToken = generateToken(32);
|
const confirmationToken = generateToken(32);
|
||||||
|
|
||||||
const { insertId } = await db
|
const letterRecord = await db
|
||||||
.insertInto("Letter")
|
.insertInto("Letter")
|
||||||
.values({
|
.values({
|
||||||
...data,
|
...data,
|
||||||
confirmationToken,
|
confirmationToken,
|
||||||
subscribed: Number(data.subscribed),
|
subscribed: Number(data.subscribed),
|
||||||
})
|
})
|
||||||
|
.returning(["id", "email", "firstName", "confirmationToken"])
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (insertId) {
|
if (letterRecord) {
|
||||||
sendConfirmationEmail(lang, insertId.toString(), confirmationToken);
|
sendConfirmationEmail(lang, letterRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect(`/${lang}/letter-sent`, 303);
|
return redirect(`/${lang}/letter-sent`, 303);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendConfirmationEmail(lang: Lang, id: string, token: string) {
|
async function sendConfirmationEmail(
|
||||||
const confirmationUrl = join(import.meta.env.BASE_URL, lang, "confirm", id, token);
|
lang: Lang,
|
||||||
console.log("Confirmation link: ", confirmationUrl);
|
letter: { id: number; email: string; confirmationToken: string }
|
||||||
|
) {
|
||||||
|
const subject = "Potvrď prosím svůj e-mail";
|
||||||
|
const confirmationUrl = join(
|
||||||
|
import.meta.env.BASE_URL,
|
||||||
|
lang,
|
||||||
|
"confirm",
|
||||||
|
String(letter.id),
|
||||||
|
letter.confirmationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
const { htmlBody, plainBody } = renderMail({ lang, href: confirmationUrl });
|
||||||
|
|
||||||
|
mailClient.send({
|
||||||
|
subject,
|
||||||
|
from: import.meta.env.MAIL_DEFAULT_FROM,
|
||||||
|
to: [letter.email],
|
||||||
|
htmlBody,
|
||||||
|
plainBody,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue