From ee9963c3b67afc629c362b50ff39f8be0eb9dd8e Mon Sep 17 00:00:00 2001 From: Thanu Poptiphueng Date: Sat, 20 Apr 2024 14:52:43 +0700 Subject: [PATCH] added update image route --- README.md | 3 + addMetadata.ts | 8 +- drizzle/0001_chilly_bullseye.sql | 10 + drizzle/meta/0001_snapshot.json | 484 +++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 1 + pnpm-lock.yaml | 205 +++++++++++++ src/config.ts | 8 + src/minio.ts | 40 +++ src/schema.ts | 12 +- src/userRoute.ts | 171 +++++++++-- 11 files changed, 914 insertions(+), 35 deletions(-) create mode 100644 README.md create mode 100644 drizzle/0001_chilly_bullseye.sql create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 src/minio.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..2fc42eb --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +1. `pnpm install` +2. `pnpm initialize_data` +3. `docker compose up` diff --git a/addMetadata.ts b/addMetadata.ts index 85720d0..97d84e6 100644 --- a/addMetadata.ts +++ b/addMetadata.ts @@ -1,8 +1,12 @@ import { db } from "./src/db"; -import { group, opinion, zone, province } from "./src/schema.ts"; -import { Groups, Opinions, Provinces, Districts } from "./initialData.ts"; +import { group, opinion, zone, province } from "./src/schema"; +import { Groups, Opinions, Provinces, Districts } from "./initialData"; +import { createBucket, createClient } from "./src/minio"; async function main() { + let mc = createClient(); + await createBucket(mc); + const isInitialized = await db.query.group .findMany() .then((groups) => groups.length > 0); diff --git a/drizzle/0001_chilly_bullseye.sql b/drizzle/0001_chilly_bullseye.sql new file mode 100644 index 0000000..9dbb8cc --- /dev/null +++ b/drizzle/0001_chilly_bullseye.sql @@ -0,0 +1,10 @@ +CREATE TABLE `image_to_user` ( + `user_id` integer PRIMARY KEY NOT NULL, + `image_name` text NOT NULL, + `created_on` integer DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +DROP TABLE `phone_tokens`;--> statement-breakpoint +ALTER TABLE users ADD `image` text;--> statement-breakpoint +CREATE INDEX `image_idx` ON `users` (`image`); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..f7eab48 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,484 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "2cf2acb7-cc98-4f28-8ead-5916b87b7683", + "prevId": "58f80520-7300-4bc4-943d-87568666e42d", + "tables": { + "groups": { + "name": "groups", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "groups_name_unique": { + "name": "groups_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "image_to_user": { + "name": "image_to_user", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_on": { + "name": "created_on", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "image_to_user_user_id_users_id_fk": { + "name": "image_to_user_user_id_users_id_fk", + "tableFrom": "image_to_user", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "opinions": { + "name": "opinions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'3Choice'" + } + }, + "indexes": { + "opinions_name_unique": { + "name": "opinions_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "provinces": { + "name": "provinces", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "provinces_name_unique": { + "name": "provinces_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cid": { + "name": "cid", + "type": "text(13)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_phone": { + "name": "public_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "facebook": { + "name": "facebook", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "twitter": { + "name": "twitter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tiktok": { + "name": "tiktok", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "other_social": { + "name": "other_social", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job": { + "name": "job", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "education": { + "name": "education", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vision": { + "name": "vision", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "zone_id": { + "name": "zone_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_cid_unique": { + "name": "users_cid_unique", + "columns": [ + "cid" + ], + "isUnique": true + }, + "users_phone_unique": { + "name": "users_phone_unique", + "columns": [ + "phone" + ], + "isUnique": true + }, + "phone_idx": { + "name": "phone_idx", + "columns": [ + "phone" + ], + "isUnique": false + }, + "image_idx": { + "name": "image_idx", + "columns": [ + "image" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_group_id_groups_id_fk": { + "name": "users_group_id_groups_id_fk", + "tableFrom": "users", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_zone_id_zones_id_fk": { + "name": "users_zone_id_zones_id_fk", + "tableFrom": "users", + "tableTo": "zones", + "columnsFrom": [ + "zone_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_opinions": { + "name": "user_opinions", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "opinion_id": { + "name": "opinion_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "choice": { + "name": "choice", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'deciding'" + } + }, + "indexes": {}, + "foreignKeys": { + "user_opinions_user_id_users_id_fk": { + "name": "user_opinions_user_id_users_id_fk", + "tableFrom": "user_opinions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_opinions_opinion_id_opinions_id_fk": { + "name": "user_opinions_opinion_id_opinions_id_fk", + "tableFrom": "user_opinions", + "tableTo": "opinions", + "columnsFrom": [ + "opinion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_opinions_user_id_opinion_id_pk": { + "columns": [ + "opinion_id", + "user_id" + ], + "name": "user_opinions_user_id_opinion_id_pk" + } + }, + "uniqueConstraints": {} + }, + "zones": { + "name": "zones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "province_id": { + "name": "province_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "zones_name_province_id_unique": { + "name": "zones_name_province_id_unique", + "columns": [ + "name", + "province_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "zones_province_id_provinces_id_fk": { + "name": "zones_province_id_provinces_id_fk", + "tableFrom": "zones", + "tableTo": "provinces", + "columnsFrom": [ + "province_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 7bc40ec..9603222 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1713548458041, "tag": "0000_right_nebula", "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1713599233997, + "tag": "0001_chilly_bullseye", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 6ce7e3c..1d6704c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "drizzle-zod": "^0.5.1", "express": "^4.19.2", "jsonwebtoken": "^9.0.2", + "minio": "^7.1.3", "trpc-playground": "^1.0.4", "zod": "^3.22.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7000f20..e8c4aaf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 + minio: + specifier: ^7.1.3 + version: 7.1.3 trpc-playground: specifier: ^1.0.4 version: 1.0.4(@trpc/server@10.45.2)(@types/node@20.12.7)(express@4.19.2)(typescript@5.4.5)(zod@3.22.4) @@ -898,6 +901,12 @@ packages: '@types/send': 0.17.4 dev: true + /@zxing/text-encoding@0.9.0: + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + requiresBuild: true + dev: false + optional: true + /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: true @@ -941,6 +950,17 @@ packages: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} dev: false + /async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + dev: false + + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: false + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -1001,6 +1021,12 @@ packages: readable-stream: 3.6.2 dev: false + /block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + dependencies: + readable-stream: 3.6.2 + dev: false + /body-parser@1.20.2: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1041,6 +1067,14 @@ packages: fill-range: 7.0.1 dev: true + /browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + dev: false + + /buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: false + /buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} dev: false @@ -1250,6 +1284,11 @@ packages: supports-color: 5.5.0 dev: true + /decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + dev: false + /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -1710,6 +1749,13 @@ packages: micromatch: 4.0.5 dev: true + /fast-xml-parser@4.3.6: + resolution: {integrity: sha512-M2SovcRxD4+vC493Uc2GZVcZaj66CCJhWurC4viynVSTvrpErCShNcDz1lAho6n9REQKvL/ll4A4/fw6Y9z8nw==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + /fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} dependencies: @@ -1750,6 +1796,11 @@ packages: to-regex-range: 5.0.1 dev: true + /filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + dev: false + /finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} engines: {node: '>= 0.8'} @@ -1772,6 +1823,12 @@ packages: semver-regex: 4.0.5 dev: true + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1908,6 +1965,13 @@ packages: engines: {node: '>= 0.4'} dev: false + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + /hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -1980,6 +2044,19 @@ packages: engines: {node: '>= 0.10'} dev: false + /ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + dev: false + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: false + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1987,11 +2064,23 @@ packages: binary-extensions: 2.3.0 dev: true + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} dev: true + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: false + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2023,6 +2112,13 @@ packages: engines: {node: '>=8'} dev: true + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.15 + dev: false + /is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} @@ -2045,6 +2141,10 @@ packages: dreamopt: 0.8.0 dev: true + /json-stream@1.0.0: + resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==} + dev: false + /jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} @@ -2252,6 +2352,26 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: false + /minio@7.1.3: + resolution: {integrity: sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==} + engines: {node: ^16 || ^18 || >=20} + dependencies: + async: 3.2.5 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 0.2.13 + fast-xml-parser: 4.3.6 + ipaddr.js: 2.2.0 + json-stream: 1.0.0 + lodash: 4.17.21 + mime-types: 2.1.35 + query-string: 7.1.3 + through2: 4.0.2 + web-encoding: 1.1.5 + xml: 1.0.1 + xml2js: 0.5.0 + dev: false + /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: false @@ -2459,6 +2579,11 @@ packages: nice-napi: 1.0.2 dev: true + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: false + /postcss@8.4.38: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} @@ -2516,6 +2641,16 @@ packages: side-channel: 1.0.6 dev: false + /query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -2612,6 +2747,10 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: false + /sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + dev: false + /semver-regex@4.0.5: resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} engines: {node: '>=12'} @@ -2782,11 +2921,21 @@ packages: engines: {node: '>= 8'} dev: true + /split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + dev: false + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} dev: false + /strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + dev: false + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -2812,6 +2961,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + /strtok3@7.0.0: resolution: {integrity: sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==} engines: {node: '>=14.16'} @@ -2854,6 +3007,12 @@ packages: readable-stream: 3.6.2 dev: false + /through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + dependencies: + readable-stream: 3.6.2 + dev: false + /timers-ext@0.1.7: resolution: {integrity: sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==} dependencies: @@ -3008,6 +3167,16 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.15 + dev: false + /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -3078,6 +3247,25 @@ packages: fsevents: 2.3.3 dev: false + /web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: false + + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: false + /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -3100,6 +3288,23 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.3.0 + xmlbuilder: 11.0.1 + dev: false + + /xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + dev: false + + /xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: false + /xss@1.0.15: resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} engines: {node: '>= 0.10.0'} diff --git a/src/config.ts b/src/config.ts index dc4bc36..b57aaa3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,4 +4,12 @@ export const Config = { "T4kE6/tIqCVEZYg9lwsqeJjYfOoXTXSXDEMyParsJjj57CjSdkrfPOLWP74/9lJpcBA=", token_duration: process.env.TOKEN_DURATION || "365d", api_url: process.env.API_URL || "http://localhost:3000", + bucketName: process.env.BUCKET_NAME || "sorvor", + minioEndpoint: process.env.MINIO_ENDPOINT || "localhost", + minioSSL: (process.env.MINIO_SSL && process.env.MINIO_SSL == "true") || false, + minioPort: parseInt(process.env.MINIO_PORT || "9000"), + minioAccessKey: process.env.MINIO_ACCESS_KEY || "minioadmin", + minioSecretKey: + process.env.MINIO_SECRET_KEY || + "K7RSS3iy/191QBeYwLJALtxGfZIHJVdBigSMdBXjqNE=", }; diff --git a/src/minio.ts b/src/minio.ts new file mode 100644 index 0000000..27a1932 --- /dev/null +++ b/src/minio.ts @@ -0,0 +1,40 @@ +import * as minio from "minio"; +import { Config } from "./config"; + +export function createClient() { + const mc = new minio.Client({ + endPoint: Config.minioEndpoint, + useSSL: Config.minioSSL, + port: Config.minioPort, + accessKey: Config.minioAccessKey, + secretKey: Config.minioSecretKey, + }); + + return mc; +} + +export async function createUploadImageUrl( + mc: minio.Client, + objectName: string, + contentType: string +) { + let policy = mc.newPostPolicy(); + policy.setKey(objectName); + policy.setBucket(Config.bucketName); + let expires = new Date(); + expires.setSeconds(30 * 60); + policy.setExpires(expires); + policy.setContentType(contentType); + policy.setContentDisposition(`attachment; filename="${objectName}"`); + policy.setContentLengthRange(1, 3 * 1024 * 1024); + let rs = await mc.presignedPostPolicy(policy); + return rs; +} + +export async function createBucket(mc: minio.Client) { + if (!(await mc.bucketExists(Config.bucketName))) { + await mc.makeBucket(Config.bucketName); + } else { + console.log("Bucket already exists"); + } +} diff --git a/src/schema.ts b/src/schema.ts index 638cd91..86d3119 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -24,6 +24,7 @@ export const user = sqliteTable( twitter: text("twitter"), tiktok: text("tiktok"), otherSocial: text("other_social"), + image: text("image"), email: text("email"), job: text("job").notNull(), education: text("education").notNull(), @@ -38,6 +39,7 @@ export const user = sqliteTable( }, (t) => ({ phone_idx: index("phone_idx").on(t.phone), + image_idx: index("image_idx").on(t.image), }) ); @@ -127,10 +129,12 @@ export const provinceRelation = relations(province, ({ many }) => ({ zones: many(zone), })); -//----------------PhoneToken -export const phoneToken = sqliteTable("phone_tokens", { - phone: text("phone").primaryKey(), - token: text("token").notNull(), +//----------------ImageToUser +export const imageToUser = sqliteTable("image_to_user", { + userId: integer("user_id") + .primaryKey() + .references(() => user.id), + imageName: text("image_name").notNull(), createdOn: integer("created_on", { mode: "timestamp" }).default( sql`CURRENT_TIMESTAMP` ), diff --git a/src/userRoute.ts b/src/userRoute.ts index 6c526bb..459a729 100644 --- a/src/userRoute.ts +++ b/src/userRoute.ts @@ -1,12 +1,13 @@ import { router, publicProcedure, protectedProcedure } from "./trpc"; import { db } from "./db"; -import { opinion, user, userOpinion } from "./schema"; +import { imageToUser, opinion, user, userOpinion } from "./schema"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; -import { SQL, eq } from "drizzle-orm"; +import { SQL, count, eq } from "drizzle-orm"; import { Config } from "./config"; import { TRPCError } from "@trpc/server"; import * as jwt from "jsonwebtoken"; +import { createClient, createUploadImageUrl } from "./minio"; const userInsertSchema = createInsertSchema(user, { cid: (schema) => @@ -33,6 +34,41 @@ type UserInsertSchema = z.infer; type UserUpdateSchema = z.infer; export const userRoute = router({ + createUser: publicProcedure + .input( + userInsertSchema.omit({ id: true }).extend({ + opinions: opinionInsertSchema, + }) + ) + .mutation( + async ({ input }) => await createUser({ ...input }, input.opinions) + ), + // changeImage: protectedProcedure + updateUser: protectedProcedure + .input(userUpdateSchema) + .mutation(async ({ input, ctx }) => await updateUser(ctx.user.id, input)), + login: publicProcedure + .input(z.object({ cid: z.string(), phone: z.string() })) + .mutation(async ({ input }) => await login(input.cid, input.phone)), + changeOpinion: protectedProcedure + .input(opinionUpdateSchema) + .mutation( + async ({ input, ctx }) => + await changeOpinion(input.opinionId, ctx.user.id, input.choice) + ), + requestChangeImage: protectedProcedure + .input(z.object({ imageName: z.string(), contentType: z.string() })) + .mutation( + async ({ input, ctx }) => + await requestChangeImage( + ctx.user.id, + input.imageName, + input.contentType + ) + ), + confirmChangeImage: protectedProcedure.mutation( + async ({ ctx }) => await confirmChangeImage(ctx.user.id, ctx.user.image) + ), getAllUser: publicProcedure .input( z.object({ @@ -53,27 +89,6 @@ export const userRoute = router({ input.zone ) ), - createUser: publicProcedure - .input( - userInsertSchema.omit({ id: true }).extend({ - opinions: opinionInsertSchema, - }) - ) - .mutation( - async ({ input }) => await createUser({ ...input }, input.opinions) - ), - updateUser: protectedProcedure - .input(userUpdateSchema) - .mutation(async ({ input, ctx }) => await updateUser(ctx.user.id, input)), - login: publicProcedure - .input(z.object({ cid: z.string(), phone: z.string() })) - .mutation(async ({ input }) => await login(input.cid, input.phone)), - changeOpinion: protectedProcedure - .input(opinionUpdateSchema) - .mutation( - async ({ input, ctx }) => - await changeOpinion(input.opinionId, ctx.user.id, input.choice) - ), }); async function getAllUser( @@ -112,6 +127,7 @@ async function getAllUser( phone: hidePhone(u.phone), })); } + async function createUser( newUser: UserInsertSchema, opinions: OpinionInsertSchema @@ -154,12 +170,6 @@ async function login(cid: string, phone: string) { } } -function createJWT(phone: string) { - return jwt.sign({ phone: phone }, Config.jwt_secret, { - expiresIn: "365d", - }); -} - async function changeOpinion( opinionId: number, userId: number, @@ -203,6 +213,99 @@ async function changeOpinion( }); } } + +async function requestChangeImage( + userId: number, + imageName: string, + contentType: string +) { + const mc = createClient(); + // Check if the image is valid + const allowedImageTypes = z.enum(["image/png", "image/jpeg", "image/webp"]); + if (!allowedImageTypes.safeParse(contentType).success) { + throw new TRPCError({ + message: "Only PNG, JPEG, and WEBP images are allowed", + code: "BAD_REQUEST", + }); + } + const allowedExtension = z.enum(["png", "jpeg", "jpg", "webp"]); + const extension = imageName.split(".").pop(); + if (!allowedExtension.safeParse(extension).success) { + throw new TRPCError({ + message: "only .png, .jpeg, .jpg, and .webp extensions are allowed", + code: "BAD_REQUEST", + }); + } + // Create a unique image name + let tryCount = 0; + let objectName: string | null = null; + while (tryCount < 3) { + let imageName = `${generateRandomString()}.${extension}`; + let ok = await db + .select({ value: count(user.image) }) + .from(user) + .where(eq(user.image, imageName)) + .then((v) => v[0].value === 0); + if (ok) { + objectName = imageName; + break; + } + } + if (objectName === null) { + throw new TRPCError({ + message: "Unable to create image request (conflicting name)", + code: "INTERNAL_SERVER_ERROR", + }); + } + // Store a record in the database + await db + .insert(imageToUser) + .values({ userId, imageName: objectName }) + .onConflictDoUpdate({ + target: [imageToUser.userId], + set: { imageName: objectName }, + }); + return await createUploadImageUrl(mc, objectName, contentType); +} + +async function confirmChangeImage(userId: number, oldImage: string | null) { + const mc = createClient(); + let rs = await db + .select({ imageName: imageToUser.imageName }) + .from(imageToUser) + .where(eq(imageToUser.userId, userId)); + if (rs.length === 0) { + throw new TRPCError({ + message: "No image request found", + code: "BAD_REQUEST", + }); + } + let imageName = rs[0].imageName; + const isImageExist = await mc + .statObject(Config.bucketName, imageName) + .then(() => true) + .catch(() => false); + if (!isImageExist) { + throw new TRPCError({ + message: "Image not found", + code: "BAD_REQUEST", + }); + } + const promises: Promise[] = []; + if (oldImage) { + promises.push(mc.removeObject(Config.bucketName, oldImage).catch(() => {})); + } + const updateUser = db + .update(user) + .set({ image: imageName }) + .where(eq(user.id, userId)); + const deleteRecord = db + .delete(imageToUser) + .where(eq(imageToUser.userId, userId)); + await Promise.all([...promises, updateUser, deleteRecord]); + return { status: "success" }; +} + function isValidThaiID(id: string) { if (!/^\d{13}$/.test(id)) { return false; @@ -219,3 +322,13 @@ function hidePhone(phone: string | null) { if (phone === null) return phone; return phone.slice(0, 2).concat("******").concat(phone.slice(-3)); } + +function createJWT(phone: string) { + return jwt.sign({ phone: phone }, Config.jwt_secret, { + expiresIn: "365d", + }); +} + +function generateRandomString() { + return Math.random().toString(36).substring(2, 15); +}