Skip to content

Commit

Permalink
Merge pull request #7 from gothinkster/main
Browse files Browse the repository at this point in the history
Merge from upstream
  • Loading branch information
fgibelin authored Oct 23, 2024
2 parents 3f6d734 + 7734aa6 commit d774e33
Show file tree
Hide file tree
Showing 45 changed files with 2,339 additions and 1,081 deletions.
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: cd apps/api && npx prisma generate && && pnpm build && pnpm start
6 changes: 5 additions & 1 deletion apps/api/nitro.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
//https://nitro.unjs.io/config
export default defineNitroConfig({
srcDir: "server"
srcDir: "server",
preset: 'heroku',
routeRules: {
'/api/**': { cors: true, headers: { 'access-control-allow-methods': '*' } },
}
});
12 changes: 10 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,23 @@
"build": "nitro build",
"dev": "nitro dev",
"prepare": "nitro prepare",
"preview": "node .output/server/index.mjs"
"preview": "node .output/server/index.mjs",
"start": "node .output/server/index.mjs",
"db:seed": "npx prisma db seed"
},
"prisma": {
"seed": "npx ts-node --transpile-only ./prisma/seed.ts"
},
"devDependencies": {
"@ngneat/falso": "^5.0.0",
"@types": "link:@types",
"jsonwebtoken": "^9.0.2",
"nitropack": "latest",
"prisma": "^5.18.0"
},
"dependencies": {
"@prisma/client": "^5.18.0",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2"
"slugify": "^1.6.0"
}
}
4 changes: 2 additions & 2 deletions apps/api/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ export const generateComment = async (id: number, slug: string) =>
addComment(randParagraph(), slug, id);

export const main = async () => {
const users = await Promise.all(Array.from({ length: 30 }, () => generateUser()));
const users = await Promise.all(Array.from({ length: 3 }, () => generateUser()));
users?.map(user => user);

// eslint-disable-next-line no-restricted-syntax
for await (const user of users) {
const articles = await Promise.all(Array.from({ length: 20 }, () => generateArticle(user.id)));
const articles = await Promise.all(Array.from({ length: 2 }, () => generateArticle(user.id)));

// eslint-disable-next-line no-restricted-syntax
for await (const article of articles) {
Expand Down
57 changes: 57 additions & 0 deletions apps/api/server/auth-event-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {default as jwt} from "jsonwebtoken";

export interface PrivateContext {
auth: {
id: number;
}
}

export function definePrivateEventHandler<T>(
handler: (event: H3Event, cxt: PrivateContext) => T,
options: { requireAuth: boolean } = {requireAuth: true}
) {
return defineEventHandler(async (event) => {
// you can check request hmac, user, token, etc..
const header = getHeader(event, 'authorization');
let token;

if (
(header && header.split(' ')[0] === 'Token') ||
(header && header.split(' ')[0] === 'Bearer')
) {
token = header.split(' ')[1];
}

if (options.requireAuth && !token) {
throw createError({
status: 401,
statusMessage: 'Unauthorized',
message: 'Missing authentication token'
});
}

if (token) {
const verified = jwt.verify(token, process.env.JWT_SECRET);

if (!verified) {
throw createError({
status: 403,
statusMessage: 'Unauthorized',
message: 'Invalid authentication token'
});
}

return handler(event, {
auth: {
id: Number(verified.user.id)
},
})
} else {
return handler(event, {
auth: null,
})
}


})
}
10 changes: 10 additions & 0 deletions apps/api/server/models/article.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Comment } from './comment.model';

export interface Article {
id: number;
title: string;
slug: string;
description: string;
comments: Comment[];
favorited: boolean;
}
9 changes: 9 additions & 0 deletions apps/api/server/models/comment.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Article } from './article.model';

export interface Comment {
id: number;
createdAt: Date;
updatedAt: Date;
body: string;
article?: Article;
}
12 changes: 12 additions & 0 deletions apps/api/server/models/http-exception.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class HttpException extends Error {
errorCode: number;
constructor(
errorCode: number,
public readonly message: string | any,
) {
super(message);
this.errorCode = errorCode;
}
}

export default HttpException;
6 changes: 6 additions & 0 deletions apps/api/server/models/profile.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Profile {
username: string;
bio: string;
image: string;
following: boolean;
}
3 changes: 3 additions & 0 deletions apps/api/server/models/tag.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface Tag {
name: string;
}
17 changes: 17 additions & 0 deletions apps/api/server/models/user.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Article } from './article.model';
import { Comment } from './comment.model';

export interface User {
id: number;
username: string;
email: string;
password: string;
bio: string | null;
image: any | null;
articles: Article[];
favorites: Article[];
followedBy: User[];
following: User[];
comments: Comment[];
demo: boolean;
}
Binary file added apps/api/server/public/images/demo-avatar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/api/server/public/images/smiley-cyrus.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/api/server/routes/api/[...].options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default defineEventHandler(async (event) => {
setResponseStatus(event, 200);
return "";
});
39 changes: 39 additions & 0 deletions apps/api/server/routes/api/articles/[slug]/comments/[id].delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import HttpException from "~/models/http-exception.model";
import {definePrivateEventHandler} from "~/auth-event-handler";

export default definePrivateEventHandler(async (event, {auth}) => {
const id = Number(getRouterParam(event, 'id'));

const comment = await usePrisma().comment.findFirst({
where: {
id,
author: {
id: auth.id,
},
},
select: {
author: {
select: {
id: true,
username: true,
},
},
},
});

if (!comment) {
throw new HttpException(404, {});
}

if (comment.author.id !== auth.id) {
throw new HttpException(403, {
message: 'You are not authorized to delete this comment',
});
}

await usePrisma().comment.delete({
where: {
id,
},
});
});
60 changes: 60 additions & 0 deletions apps/api/server/routes/api/articles/[slug]/comments/index.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {definePrivateEventHandler} from "~/auth-event-handler";

export default definePrivateEventHandler(async (event, {auth}) => {
const slug = getRouterParam(event, 'slug');

const queries = [];

queries.push({
author: {
demo: true,
},
});

if (auth?.id) {
queries.push({
author: {
id: auth.id,
},
});
}

const comments = await usePrisma().article.findUnique({
where: {
slug,
},
include: {
comments: {
where: {
OR: queries,
},
select: {
id: true,
createdAt: true,
updatedAt: true,
body: true,
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
},
},
},
});

const result = comments?.comments.map((comment: any) => ({
...comment,
author: {
username: comment.author.username,
bio: comment.author.bio,
image: comment.author.image,
following: comment.author.followedBy.some((follow: any) => follow.id === auth.id),
},
}));

return {comments: result};
}, {requireAuth: false});
61 changes: 61 additions & 0 deletions apps/api/server/routes/api/articles/[slug]/comments/index.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import HttpException from "~/models/http-exception.model";
import {definePrivateEventHandler} from "~/auth-event-handler";

export default definePrivateEventHandler(async (event, {auth}) => {
const {comment} = await readBody(event);
const slug = getRouterParam(event, 'slug');

if (!comment.body) {
throw new HttpException(422, {errors: {body: ["can't be blank"]}});
}

const article = await usePrisma().article.findUnique({
where: {
slug,
},
select: {
id: true,
},
});

const createdComment = await usePrisma().comment.create({
data: {
body: comment.body,
article: {
connect: {
id: article?.id,
},
},
author: {
connect: {
id: auth.id,
},
},
},
include: {
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
},
});

return {
comment: {
id: createdComment.id,
createdAt: createdComment.createdAt,
updatedAt: createdComment.updatedAt,
body: createdComment.body,
author: {
username: createdComment.author.username,
bio: createdComment.author.bio,
image: createdComment.author.image,
following: createdComment.author.followedBy.some((follow: any) => follow.id === auth.id),
},
}
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import profileMapper from "~/utils/profile.utils";
import {Tag} from "~/models/tag.model";
import {definePrivateEventHandler} from "~/auth-event-handler";

export default definePrivateEventHandler(async (event, {auth}) => {
const slug = getRouterParam(event, "slug");

const { _count, ...article } = await usePrisma().article.update({
where: {
slug,
},
data: {
favoritedBy: {
disconnect: {
id: auth.id,
},
},
},
include: {
tagList: {
select: {
name: true,
},
},
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
favoritedBy: true,
_count: {
select: {
favoritedBy: true,
},
},
},
});

const result = {
...article,
author: profileMapper(article.author, auth.id),
tagList: article?.tagList.map((tag: Tag) => tag.name),
favorited: article.favoritedBy.some((favorited: any) => favorited.id === auth.id),
favoritesCount: _count?.favoritedBy,
};

return {article: result};
});
Loading

0 comments on commit d774e33

Please sign in to comment.