Skip to content

Commit d06f225

Browse files
committed
feat: nitro api
1 parent a902bd1 commit d06f225

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1203
-8519
lines changed

apps/api/nitro.config.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
//https://nitro.unjs.io/config
22
export default defineNitroConfig({
3-
srcDir: "server"
3+
srcDir: "server",
4+
preset: 'heroku',
5+
routeRules: {
6+
'/api/**': { cors: true, headers: { 'access-control-allow-methods': '*' } },
7+
}
48
});

apps/api/package.json

+10-2
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@
44
"build": "nitro build",
55
"dev": "nitro dev",
66
"prepare": "nitro prepare",
7-
"preview": "node .output/server/index.mjs"
7+
"preview": "node .output/server/index.mjs",
8+
"start": "node .output/server/index.mjs",
9+
"db:seed": "npx prisma db seed"
10+
},
11+
"prisma": {
12+
"seed": "npx ts-node --transpile-only ./prisma/seed.ts"
813
},
914
"devDependencies": {
15+
"@ngneat/falso": "^5.0.0",
16+
"@types": "link:@types",
17+
"jsonwebtoken": "^9.0.2",
1018
"nitropack": "latest",
1119
"prisma": "^5.18.0"
1220
},
1321
"dependencies": {
1422
"@prisma/client": "^5.18.0",
1523
"bcryptjs": "^2.4.3",
16-
"jsonwebtoken": "^9.0.2"
24+
"slugify": "^1.6.0"
1725
}
1826
}

apps/api/prisma/seed.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ export const generateComment = async (id: number, slug: string) =>
3838
addComment(randParagraph(), slug, id);
3939

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

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

4848
// eslint-disable-next-line no-restricted-syntax
4949
for await (const article of articles) {

apps/api/server/auth-event-handler.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {default as jwt} from "jsonwebtoken";
2+
3+
export interface PrivateContext {
4+
auth: {
5+
id: number;
6+
}
7+
}
8+
9+
export function definePrivateEventHandler<T>(
10+
handler: (event: H3Event, cxt: PrivateContext) => T,
11+
options: { requireAuth: boolean } = {requireAuth: true}
12+
) {
13+
return defineEventHandler(async (event) => {
14+
// you can check request hmac, user, token, etc..
15+
const header = getHeader(event, 'authorization');
16+
let token;
17+
18+
if (
19+
(header && header.split(' ')[0] === 'Token') ||
20+
(header && header.split(' ')[0] === 'Bearer')
21+
) {
22+
token = header.split(' ')[1];
23+
}
24+
25+
if (options.requireAuth && !token) {
26+
throw createError({
27+
status: 401,
28+
statusMessage: 'Unauthorized',
29+
message: 'Missing authentication token'
30+
});
31+
}
32+
33+
if (token) {
34+
const verified = jwt.verify(token, process.env.JWT_SECRET);
35+
36+
if (!verified) {
37+
throw createError({
38+
status: 403,
39+
statusMessage: 'Unauthorized',
40+
message: 'Invalid authentication token'
41+
});
42+
}
43+
44+
return handler(event, {
45+
auth: {
46+
id: Number(verified.user.id)
47+
},
48+
})
49+
} else {
50+
return handler(event, {
51+
auth: null,
52+
})
53+
}
54+
55+
56+
})
57+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Comment } from './comment.model';
2+
3+
export interface Article {
4+
id: number;
5+
title: string;
6+
slug: string;
7+
description: string;
8+
comments: Comment[];
9+
favorited: boolean;
10+
}
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Article } from './article.model';
2+
3+
export interface Comment {
4+
id: number;
5+
createdAt: Date;
6+
updatedAt: Date;
7+
body: string;
8+
article?: Article;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class HttpException extends Error {
2+
errorCode: number;
3+
constructor(
4+
errorCode: number,
5+
public readonly message: string | any,
6+
) {
7+
super(message);
8+
this.errorCode = errorCode;
9+
}
10+
}
11+
12+
export default HttpException;
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface Profile {
2+
username: string;
3+
bio: string;
4+
image: string;
5+
following: boolean;
6+
}

apps/api/server/models/tag.model.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Tag {
2+
name: string;
3+
}

apps/api/server/models/user.model.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Article } from './article.model';
2+
import { Comment } from './comment.model';
3+
4+
export interface User {
5+
id: number;
6+
username: string;
7+
email: string;
8+
password: string;
9+
bio: string | null;
10+
image: any | null;
11+
articles: Article[];
12+
favorites: Article[];
13+
followedBy: User[];
14+
following: User[];
15+
comments: Comment[];
16+
demo: boolean;
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default defineEventHandler(async (event) => {
2+
setResponseStatus(event, 200);
3+
return "";
4+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import HttpException from "~/models/http-exception.model";
2+
import {definePrivateEventHandler} from "~/auth-event-handler";
3+
4+
export default definePrivateEventHandler(async (event, {auth}) => {
5+
const id = Number(getRouterParam(event, 'id'));
6+
7+
const comment = await usePrisma().comment.findFirst({
8+
where: {
9+
id,
10+
author: {
11+
id: auth.id,
12+
},
13+
},
14+
select: {
15+
author: {
16+
select: {
17+
id: true,
18+
username: true,
19+
},
20+
},
21+
},
22+
});
23+
24+
if (!comment) {
25+
throw new HttpException(404, {});
26+
}
27+
28+
if (comment.author.id !== auth.id) {
29+
throw new HttpException(403, {
30+
message: 'You are not authorized to delete this comment',
31+
});
32+
}
33+
34+
await usePrisma().comment.delete({
35+
where: {
36+
id,
37+
},
38+
});
39+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {definePrivateEventHandler} from "~/auth-event-handler";
2+
3+
export default definePrivateEventHandler(async (event, {auth}) => {
4+
const slug = getRouterParam(event, 'slug');
5+
6+
const queries = [];
7+
8+
queries.push({
9+
author: {
10+
demo: true,
11+
},
12+
});
13+
14+
if (auth?.id) {
15+
queries.push({
16+
author: {
17+
id: auth.id,
18+
},
19+
});
20+
}
21+
22+
const comments = await usePrisma().article.findUnique({
23+
where: {
24+
slug,
25+
},
26+
include: {
27+
comments: {
28+
where: {
29+
OR: queries,
30+
},
31+
select: {
32+
id: true,
33+
createdAt: true,
34+
updatedAt: true,
35+
body: true,
36+
author: {
37+
select: {
38+
username: true,
39+
bio: true,
40+
image: true,
41+
followedBy: true,
42+
},
43+
},
44+
},
45+
},
46+
},
47+
});
48+
49+
const result = comments?.comments.map((comment: any) => ({
50+
...comment,
51+
author: {
52+
username: comment.author.username,
53+
bio: comment.author.bio,
54+
image: comment.author.image,
55+
following: comment.author.followedBy.some((follow: any) => follow.id === auth.id),
56+
},
57+
}));
58+
59+
return {comments: result};
60+
}, {requireAuth: false});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import HttpException from "~/models/http-exception.model";
2+
import {definePrivateEventHandler} from "~/auth-event-handler";
3+
4+
export default definePrivateEventHandler(async (event, {auth}) => {
5+
const {comment} = await readBody(event);
6+
const slug = getRouterParam(event, 'slug');
7+
8+
if (!comment.body) {
9+
throw new HttpException(422, {errors: {body: ["can't be blank"]}});
10+
}
11+
12+
const article = await usePrisma().article.findUnique({
13+
where: {
14+
slug,
15+
},
16+
select: {
17+
id: true,
18+
},
19+
});
20+
21+
const createdComment = await usePrisma().comment.create({
22+
data: {
23+
body: comment.body,
24+
article: {
25+
connect: {
26+
id: article?.id,
27+
},
28+
},
29+
author: {
30+
connect: {
31+
id: auth.id,
32+
},
33+
},
34+
},
35+
include: {
36+
author: {
37+
select: {
38+
username: true,
39+
bio: true,
40+
image: true,
41+
followedBy: true,
42+
},
43+
},
44+
},
45+
});
46+
47+
return {
48+
comment: {
49+
id: createdComment.id,
50+
createdAt: createdComment.createdAt,
51+
updatedAt: createdComment.updatedAt,
52+
body: createdComment.body,
53+
author: {
54+
username: createdComment.author.username,
55+
bio: createdComment.author.bio,
56+
image: createdComment.author.image,
57+
following: createdComment.author.followedBy.some((follow: any) => follow.id === auth.id),
58+
},
59+
}
60+
};
61+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import profileMapper from "~/utils/profile.utils";
2+
import {Tag} from "~/models/tag.model";
3+
import {definePrivateEventHandler} from "~/auth-event-handler";
4+
5+
export default definePrivateEventHandler(async (event, {auth}) => {
6+
const slug = getRouterParam(event, "slug");
7+
8+
const { _count, ...article } = await usePrisma().article.update({
9+
where: {
10+
slug,
11+
},
12+
data: {
13+
favoritedBy: {
14+
disconnect: {
15+
id: auth.id,
16+
},
17+
},
18+
},
19+
include: {
20+
tagList: {
21+
select: {
22+
name: true,
23+
},
24+
},
25+
author: {
26+
select: {
27+
username: true,
28+
bio: true,
29+
image: true,
30+
followedBy: true,
31+
},
32+
},
33+
favoritedBy: true,
34+
_count: {
35+
select: {
36+
favoritedBy: true,
37+
},
38+
},
39+
},
40+
});
41+
42+
const result = {
43+
...article,
44+
author: profileMapper(article.author, auth.id),
45+
tagList: article?.tagList.map((tag: Tag) => tag.name),
46+
favorited: article.favoritedBy.some((favorited: any) => favorited.id === auth.id),
47+
favoritesCount: _count?.favoritedBy,
48+
};
49+
50+
return {article: result};
51+
});

0 commit comments

Comments
 (0)