added update image route
This commit is contained in:
@@ -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=",
|
||||
};
|
||||
|
||||
40
src/minio.ts
Normal file
40
src/minio.ts
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
),
|
||||
|
||||
171
src/userRoute.ts
171
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<typeof userInsertSchema>;
|
||||
type UserUpdateSchema = z.infer<typeof userUpdateSchema>;
|
||||
|
||||
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<any>[] = [];
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user