Skip to content

Commit 99b5341

Browse files
Merge PR: Refactor images, users, and logging
This PR introduces multiple improvements across the app. - Refactors image handling with better avatar and post image support. - Updates user logic, including guest sign-in, and improved PATCH handling. - Improves logging configuration and request logging. - Cleans up code structure by extracting image schemas, utilities, and storage logic.
2 parents f9c89c1 + 144165e commit 99b5341

39 files changed

Lines changed: 1651 additions & 706 deletions

.env.test

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ SECRET=secret
77
PORT=8080
88
ALLOWED_ORIGINS=*
99
MAX_FILE_SIZE_MB=2
10+
STORAGE_KEY=storage-key
1011
STORAGE_ROOT_DIR=public
11-
SUPABASE_BUCKET=bucket-name
12-
SUPABASE_ANON_KEY=public-anon-key
13-
SUPABASE_URL=https://xyzcompany.supabase.co
14-
SUPABASE_BUCKET_URL=https://xyzcompany.supabase.co/storage/v1/object/bucket
12+
STORAGE_BUCKET=bucket-name
13+
STORAGE_URL=https://xyzcompany.supabase.co
14+
STORAGE_BUCKET_URL=https://xyzcompany.supabase.co/storage/v1/object/bucket

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default tseslint.config({
1919
rules: {
2020
'@stylistic/semi': 'error',
2121
'@typescript-eslint/no-explicit-any': 'error',
22+
'@typescript-eslint/no-non-null-assertion': 'off',
2223
'@typescript-eslint/no-unsafe-assignment': 'error',
2324
'@typescript-eslint/no-confusing-void-expression': 'off',
2425
'@typescript-eslint/restrict-template-expressions': 'off',
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the column `author_id` on the `images` table. All the data in the column will be lost.
5+
- A unique constraint covering the columns `[user_id]` on the table `images` will be added. If there are existing duplicate values, this will fail.
6+
- Added the required column `owner_id` to the `images` table without a default value. This is not possible if the table is not empty.
7+
- Made the column `bio` on table `users` required. This step will fail if there are existing NULL values in that column.
8+
9+
*/
10+
-- DropForeignKey
11+
ALTER TABLE "public"."images" DROP CONSTRAINT "images_author_id_fkey";
12+
13+
-- AlterTable
14+
ALTER TABLE "public"."images" DROP COLUMN "author_id",
15+
ADD COLUMN "owner_id" UUID NOT NULL,
16+
ADD COLUMN "user_id" UUID;
17+
18+
-- AlterTable
19+
ALTER TABLE "public"."users" ALTER COLUMN "bio" SET NOT NULL,
20+
ALTER COLUMN "bio" SET DEFAULT '';
21+
22+
-- CreateIndex
23+
CREATE UNIQUE INDEX "images_user_id_key" ON "public"."images"("user_id");
24+
25+
-- AddForeignKey
26+
ALTER TABLE "public"."images" ADD CONSTRAINT "images_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
27+
28+
-- AddForeignKey
29+
ALTER TABLE "public"."images" ADD CONSTRAINT "images_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the column `user_id` on the `images` table. All the data in the column will be lost.
5+
6+
*/
7+
-- DropForeignKey
8+
ALTER TABLE "public"."images" DROP CONSTRAINT "images_user_id_fkey";
9+
10+
-- DropIndex
11+
DROP INDEX "public"."images_user_id_key";
12+
13+
-- AlterTable
14+
ALTER TABLE "public"."images" DROP COLUMN "user_id";
15+
16+
-- CreateTable
17+
CREATE TABLE "public"."Avatar" (
18+
"user_id" UUID NOT NULL,
19+
"image_id" UUID NOT NULL
20+
);
21+
22+
-- CreateIndex
23+
CREATE UNIQUE INDEX "Avatar_user_id_key" ON "public"."Avatar"("user_id");
24+
25+
-- AddForeignKey
26+
ALTER TABLE "public"."Avatar" ADD CONSTRAINT "Avatar_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
27+
28+
-- AddForeignKey
29+
ALTER TABLE "public"."Avatar" ADD CONSTRAINT "Avatar_image_id_fkey" FOREIGN KEY ("image_id") REFERENCES "public"."images"("id") ON DELETE CASCADE ON UPDATE CASCADE;

prisma/schema.prisma

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ datasource db {
1111
model User {
1212
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
1313
order Int @unique @default(autoincrement())
14+
bio String @default("")
1415
password String @db.Char(60)
1516
fullname String @db.VarChar(100)
1617
username String @unique @db.VarChar(50)
1718
isAdmin Boolean @default(false) @map("is_admin")
1819
createdAt DateTime @default(now()) @map("created_at")
1920
updatedAt DateTime @updatedAt @map("updated_at")
20-
votesOnPosts VotesOnPosts[]
21-
comments Comment[]
21+
avatar Avatar?
2222
images Image[]
2323
posts Post[]
24-
bio String?
24+
comments Comment[]
25+
votesOnPosts VotesOnPosts[]
2526
2627
@@map("users")
2728
}
@@ -96,7 +97,7 @@ model VotesOnPosts {
9697
model Image {
9798
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
9899
order Int @unique @default(autoincrement())
99-
ownerId String @map("author_id") @db.Uuid
100+
ownerId String @map("owner_id") @db.Uuid
100101
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
101102
createdAt DateTime @default(now()) @map("created_at")
102103
updatedAt DateTime @updatedAt @map("updated_at")
@@ -113,6 +114,14 @@ model Image {
113114
yPos Int @default(0) @map("y_pos")
114115
scale Float @default(1.0)
115116
posts Post[]
117+
avatars Avatar[]
116118
117119
@@map("images")
118120
}
121+
122+
model Avatar {
123+
userId String @unique @map("user_id") @db.Uuid
124+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
125+
imageId String @map("image_id") @db.Uuid
126+
image Image @relation(fields: [imageId], references: [id], onDelete: Cascade)
127+
}

prisma/seed/run.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ const postCount = titles.length;
2626
const tags = [
2727
'open_source',
2828
'full_stack',
29-
'javaScript',
30-
'typeScript',
29+
'javascript',
30+
'typescript',
3131
'security',
3232
'frontend',
3333
'software',
@@ -65,14 +65,14 @@ async function main() {
6565
fullname: 'Clark Kent / Kal-El',
6666
bio: 'From Krypton with love.',
6767
username: 'superman',
68-
isAdmin: false,
68+
isAdmin: true,
6969
},
7070
{
7171
password: bcrypt.hashSync(...passHashArgs),
7272
bio: 'From Gotham with love.',
7373
fullname: 'Bruce Wayne',
7474
username: 'batman',
75-
isAdmin: false,
75+
isAdmin: true,
7676
},
7777
],
7878
});

src/api/v1/auth/auth.router.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
import * as Exp from 'express';
21
import * as Types from '@/types';
32
import * as Utils from '@/lib/utils';
43
import * as AppError from '@/lib/app-error';
54
import * as Validators from '@/middlewares/validators';
6-
import { User } from '@/../prisma/client';
5+
import { Router, RequestHandler } from 'express';
76
import passport from '@/lib/passport';
87
import logger from '@/lib/logger';
98

10-
export const authRouter = Exp.Router();
9+
export const authRouter = Router();
1110

1211
authRouter.post('/signin', async (req, res, next) => {
1312
await (
1413
passport.authenticate(
1514
'local',
1615
{ session: false },
17-
(error: unknown, user: User | false | null | undefined) => {
16+
(error: unknown, user: Types.PublicUser | false | null | undefined) => {
1817
if (error || !user) {
1918
if (error) logger.error(error);
2019
next(new AppError.AppSignInError());
@@ -29,7 +28,7 @@ authRouter.post('/signin', async (req, res, next) => {
2928
});
3029
}
3130
}
32-
) as Exp.RequestHandler
31+
) as RequestHandler
3332
)(req, res, next);
3433
});
3534

src/api/v1/images/image.schema.ts

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
1+
import * as Image from '@/lib/image';
12
import { z } from 'zod';
23

3-
export const posSchema = z.coerce
4-
.number()
5-
.optional()
6-
.transform((n) => n && Math.trunc(n));
7-
8-
export const scaleSchema = z.coerce.number().optional();
9-
10-
export const infoSchema = z.string().trim().optional();
11-
12-
export const altSchema = z.string().trim().optional();
13-
14-
export const imageSchema = z.object({
15-
scale: scaleSchema,
16-
info: infoSchema,
17-
xPos: posSchema,
18-
yPos: posSchema,
19-
alt: altSchema,
20-
});
4+
export const imageSchema = Image.imageSchema.merge(
5+
z.object({
6+
isAvatar: z.preprocess(
7+
(v) => [true, 'true', 'on'].includes(v as string | boolean),
8+
z.boolean().optional().default(false)
9+
),
10+
})
11+
);

src/api/v1/images/images.router.ts

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
import * as Exp from 'express';
21
import * as Types from '@/types';
32
import * as Utils from '@/lib/utils';
3+
import * as Image from '@/lib/image';
4+
import * as Storage from '@/lib/storage';
45
import * as Schema from './image.schema';
56
import * as Service from './images.service';
67
import * as Middlewares from '@/middlewares';
7-
import { Image } from '@/../prisma/client';
8+
import { Router, Request, Response } from 'express';
9+
import { Image as ImageT } from '@/../prisma/client';
810

9-
export const imagesRouter = Exp.Router();
11+
export const imagesRouter = Router();
1012

11-
imagesRouter.get('/', async (req, res) => {
12-
res.json(
13-
await Service.getAllImages(Utils.getPaginationFiltersFromReqQuery(req))
14-
);
15-
});
13+
imagesRouter.get(
14+
'/',
15+
Middlewares.authValidator,
16+
Middlewares.adminValidator,
17+
async (req, res) => {
18+
res.json(
19+
await Service.getAllImages(Utils.getPaginationFiltersFromReqQuery(req))
20+
);
21+
}
22+
);
1623

1724
imagesRouter.get('/:id', async (req, res) => {
1825
res.json(await Service.findImageById(req.params.id));
@@ -22,14 +29,14 @@ imagesRouter.post(
2229
'/',
2330
Middlewares.authValidator,
2431
Middlewares.createFileProcessor('image'),
25-
async (req: Exp.Request, res: Exp.Response) => {
32+
async (req: Request, res: Response) => {
2633
const user = req.user as Types.PublicUser;
27-
const imageFile = await Service.getValidImageFileFormReq(req);
34+
const imageFile = await Image.getValidImageFileFormReq(req);
2835
const data = {
2936
...Schema.imageSchema.parse(req.body),
30-
...Service.getImageMetadata(imageFile),
37+
...Image.getImageMetadata(imageFile),
3138
};
32-
const uploadRes = await Service.uploadImage(imageFile, user);
39+
const uploadRes = await Storage.uploadImage(imageFile, user);
3340
const savedImage = await Service.saveImage(uploadRes, data, user);
3441
res.status(201).json(savedImage);
3542
}
@@ -39,24 +46,24 @@ imagesRouter.put(
3946
'/:id',
4047
Middlewares.authValidator,
4148
Middlewares.createOwnerValidator(
42-
Service.getImageOwnerAndInjectImageInResLocals
49+
Image.getImageOwnerAndInjectImageInResLocals
4350
),
4451
Middlewares.createFileProcessor('image'),
45-
async (req: Exp.Request, res: Exp.Response<unknown, { image: Image }>) => {
52+
async (req: Request, res: Response<unknown, { image: ImageT }>) => {
4653
const { image } = res.locals;
4754
const user = req.user as Types.PublicUser;
4855
if (req.file) {
49-
const imageFile = await Service.getValidImageFileFormReq(req);
56+
const imageFile = await Image.getValidImageFileFormReq(req);
5057
const data = {
5158
...Schema.imageSchema.parse(req.body),
52-
...Service.getImageMetadata(imageFile),
59+
...Image.getImageMetadata(imageFile),
5360
};
54-
const uploadRes = await Service.uploadImage(imageFile, user, image);
61+
const uploadRes = await Storage.uploadImage(imageFile, user, image);
5562
const savedImage = await Service.saveImage(uploadRes, data, user);
5663
res.json(savedImage);
5764
} else {
5865
const data = Schema.imageSchema.parse(req.body);
59-
const updatedImage = await Service.updateImageData(data, req.params.id);
66+
const updatedImage = await Service.updateImageData(data, image, user);
6067
res.json(updatedImage);
6168
}
6269
}
@@ -66,11 +73,11 @@ imagesRouter.delete(
6673
'/:id',
6774
Middlewares.authValidator,
6875
Middlewares.createAdminOrOwnerValidator(
69-
Service.getImageOwnerAndInjectImageInResLocals
76+
Image.getImageOwnerAndInjectImageInResLocals
7077
),
71-
async (req, res: Exp.Response<unknown, { image: Image }>) => {
78+
async (req, res: Response<unknown, { image: ImageT }>) => {
7279
const { image } = res.locals;
73-
await Service.removeUploadedImage(image);
80+
await Storage.removeImage(image);
7481
await Service.deleteImageById(image.id);
7582
res.status(204).send();
7683
}

0 commit comments

Comments
 (0)