diff --git a/.env.example b/.env.example
index 37af88a..31b2d4d 100644
--- a/.env.example
+++ b/.env.example
@@ -1,14 +1,5 @@
# REST API configuration
REST_API_URL=
-# Auth configuration - required in production
-AUTH_ORIGIN=
-AUTH_SECRET=
-
-# Google configuration
-GOOGLE_CLIENT_ID=
-GOOGLE_CLIENT_SECRET=
-GOOGLE_REFRESH_TOKEN_URL=
-
# .NET backend runs locally on HTTPS (must be set to 0), for production must be set to 1
NODE_TLS_REJECT_UNAUTHORIZED=
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 18b9ac5..d3c1052 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -13,12 +13,14 @@
"Lato",
"nuxt",
"nuxtjs",
+ "ofetch",
"Raleway",
"Roboto",
"sidebase",
"tailwindcss",
"vueuc",
- "wght"
+ "wght",
+ "dtos"
],
"files.eol": "\n",
"[typescript]": {
diff --git a/components/posts/NftPost.vue b/components/posts/NftPost.vue
index 59f9648..b4c32c9 100644
--- a/components/posts/NftPost.vue
+++ b/components/posts/NftPost.vue
@@ -15,7 +15,7 @@
{{ post.description }}
-
+
{{ attribute.traitType }}
Show more
@@ -25,9 +25,9 @@
diff --git a/nuxt.config.ts b/nuxt.config.ts
index abbdd53..d5cf508 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -1,6 +1,9 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
+ runtimeConfig: {
+ restApiUrl: process.env.REST_API_URL,
+ },
modules: [
'@nuxtjs/tailwindcss',
'nuxt-icon',
diff --git a/server/api/auth.ts b/server/api/auth.ts
deleted file mode 100644
index 75ce4c4..0000000
--- a/server/api/auth.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default defineEventHandler((_event) => {
- return 'Hello Nitro';
-});
diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts
new file mode 100644
index 0000000..e2452f6
--- /dev/null
+++ b/server/api/auth/login.post.ts
@@ -0,0 +1,8 @@
+import { useAuthService } from '~/server/services/auth.service';
+import { VerifyNonce } from '~/types/auth';
+
+export default defineEventHandler(async (event) => {
+ const { signature, address } = await readBody(event);
+ const service = useAuthService();
+ return await service.verifyNonce(signature, address);
+});
diff --git a/server/api/auth/nonce.post.ts b/server/api/auth/nonce.post.ts
new file mode 100644
index 0000000..fe81552
--- /dev/null
+++ b/server/api/auth/nonce.post.ts
@@ -0,0 +1,8 @@
+import { useAuthService } from '~/server/services/auth.service';
+import { GetNonceDTO } from '~/types/auth';
+
+export default defineEventHandler(async (event) => {
+ const { address } = await readBody(event);
+ const service = useAuthService();
+ return await service.getNonce(address);
+});
diff --git a/server/api/auth/refresh.post.ts b/server/api/auth/refresh.post.ts
new file mode 100644
index 0000000..11c20a9
--- /dev/null
+++ b/server/api/auth/refresh.post.ts
@@ -0,0 +1,10 @@
+import { useAuthService } from '~/server/services/auth.service';
+import { AuthenticatedUser, RefreshTokenRequestDTO } from '~/types/auth';
+
+type Body = RefreshTokenRequestDTO & AuthenticatedUser;
+
+export default defineEventHandler(async (event) => {
+ const { refreshToken: refToken, jwt } = await readBody(event);
+ const service = useAuthService(jwt);
+ return await service.refreshToken(refToken);
+});
diff --git a/server/api/comment/[id]/index.delete.ts b/server/api/comment/[id]/index.delete.ts
new file mode 100644
index 0000000..b5a196a
--- /dev/null
+++ b/server/api/comment/[id]/index.delete.ts
@@ -0,0 +1,16 @@
+import { useCommentService } from '~/server/services/comment.service';
+import { AuthenticatedUser } from '~/types/auth';
+
+type Body = AuthenticatedUser;
+
+export default defineEventHandler(async (event) => {
+ const { jwt } = await readBody(event);
+ const commentId = getRouterParam(event, 'id');
+ if (Number.isNaN(commentId)) {
+ return createError({
+ message: 'Invalid comment id',
+ });
+ }
+ const service = useCommentService(jwt);
+ return await service.remove(Number(commentId));
+});
diff --git a/server/api/comment/[id]/index.put.ts b/server/api/comment/[id]/index.put.ts
new file mode 100644
index 0000000..4503e1a
--- /dev/null
+++ b/server/api/comment/[id]/index.put.ts
@@ -0,0 +1,17 @@
+import { useCommentService } from '~/server/services/comment.service';
+import { AuthenticatedUser } from '~/types/auth';
+import { UpdateCommentDTO } from '~/types/dtos';
+
+type Body = AuthenticatedUser & UpdateCommentDTO;
+
+export default defineEventHandler(async (event) => {
+ const { jwt, content } = await readBody(event);
+ const commentId = getRouterParam(event, 'id');
+ if (Number.isNaN(commentId)) {
+ return createError({
+ message: 'Invalid comment id',
+ });
+ }
+ const service = useCommentService(jwt);
+ return await service.update(Number(commentId), content);
+});
diff --git a/server/api/comment/[id]/like/index.delete.ts b/server/api/comment/[id]/like/index.delete.ts
new file mode 100644
index 0000000..13369a7
--- /dev/null
+++ b/server/api/comment/[id]/like/index.delete.ts
@@ -0,0 +1,15 @@
+import { useCommentService } from '~/server/services/comment.service';
+import { AuthenticatedUser } from '~/types/auth';
+
+export default defineEventHandler(async (event) => {
+ const { jwt } = await readBody(event);
+ const commentId = getRouterParam(event, 'id');
+ if (Number.isNaN(commentId)) {
+ return createError({
+ message: 'Invalid comment id',
+ });
+ }
+
+ const service = useCommentService(jwt);
+ return await service.unlike(Number(commentId));
+});
diff --git a/server/api/comment/[id]/like/index.get.ts b/server/api/comment/[id]/like/index.get.ts
new file mode 100644
index 0000000..2bb28a6
--- /dev/null
+++ b/server/api/comment/[id]/like/index.get.ts
@@ -0,0 +1,15 @@
+import { useCommentService } from '~/server/services/comment.service';
+import { AuthenticatedUser } from '~/types/auth';
+
+export default defineEventHandler(async (event) => {
+ const { jwt } = await readBody(event);
+ const commentId = getRouterParam(event, 'id');
+ if (Number.isNaN(commentId)) {
+ return createError({
+ message: 'Invalid comment id',
+ });
+ }
+
+ const service = useCommentService(jwt);
+ return await service.getLikes(Number(commentId));
+});
diff --git a/server/api/comment/[id]/like/index.post.ts b/server/api/comment/[id]/like/index.post.ts
new file mode 100644
index 0000000..690cc86
--- /dev/null
+++ b/server/api/comment/[id]/like/index.post.ts
@@ -0,0 +1,15 @@
+import { useCommentService } from '~/server/services/comment.service';
+import { AuthenticatedUser } from '~/types/auth';
+
+export default defineEventHandler(async (event) => {
+ const { jwt } = await readBody(event);
+ const commentId = getRouterParam(event, 'id');
+ if (Number.isNaN(commentId)) {
+ return createError({
+ message: 'Invalid comment id',
+ });
+ }
+
+ const service = useCommentService(jwt);
+ return await service.like(Number(commentId));
+});
diff --git a/server/api/comment/index.post.ts b/server/api/comment/index.post.ts
new file mode 100644
index 0000000..645bebb
--- /dev/null
+++ b/server/api/comment/index.post.ts
@@ -0,0 +1,11 @@
+import { useCommentService } from '~/server/services/comment.service';
+import { AuthenticatedUser } from '~/types/auth';
+import { AddCommentDTO, NFTAddress } from '~/types/dtos';
+
+type Body = AuthenticatedUser & AddCommentDTO & NFTAddress;
+
+export default defineEventHandler(async (event) => {
+ const { jwt, content, parentCommentId, address } = await readBody(event);
+ const service = useCommentService(jwt);
+ return await service.add(address, content, parentCommentId);
+});
diff --git a/server/api/post/[id]/like/index.delete.ts b/server/api/post/[id]/like/index.delete.ts
new file mode 100644
index 0000000..373f770
--- /dev/null
+++ b/server/api/post/[id]/like/index.delete.ts
@@ -0,0 +1,15 @@
+import { usePostService } from '~/server/services/post.service';
+import { AuthenticatedUser } from '~/types/auth';
+
+export default defineEventHandler(async (event) => {
+ const { jwt } = await readBody(event);
+ const nftAddress = getRouterParam(event, 'id');
+ if (!nftAddress) {
+ return createError({
+ message: 'Invalid NFT address',
+ });
+ }
+
+ const service = usePostService(jwt);
+ return await service.unlike(nftAddress);
+});
diff --git a/server/api/post/[id]/like/index.get.ts b/server/api/post/[id]/like/index.get.ts
new file mode 100644
index 0000000..66b9c13
--- /dev/null
+++ b/server/api/post/[id]/like/index.get.ts
@@ -0,0 +1,15 @@
+import { usePostService } from '~/server/services/post.service';
+import { AuthenticatedUser } from '~/types/auth';
+
+export default defineEventHandler(async (event) => {
+ const { jwt } = await readBody(event);
+ const nftAddress = getRouterParam(event, 'id');
+ if (!nftAddress) {
+ return createError({
+ message: 'Invalid NFT address',
+ });
+ }
+
+ const service = usePostService(jwt);
+ return await service.getLikes(nftAddress);
+});
diff --git a/server/api/post/[id]/like/index.post.ts b/server/api/post/[id]/like/index.post.ts
new file mode 100644
index 0000000..f393f73
--- /dev/null
+++ b/server/api/post/[id]/like/index.post.ts
@@ -0,0 +1,15 @@
+import { usePostService } from '~/server/services/post.service';
+import { AuthenticatedUser } from '~/types/auth';
+
+export default defineEventHandler(async (event) => {
+ const { jwt } = await readBody(event);
+ const nftAddress = getRouterParam(event, 'id');
+ if (!nftAddress) {
+ return createError({
+ message: 'Invalid NFT address',
+ });
+ }
+
+ const service = usePostService(jwt);
+ return await service.like(nftAddress);
+});
diff --git a/server/api/post/my.get.ts b/server/api/post/my.get.ts
new file mode 100644
index 0000000..d9248d7
--- /dev/null
+++ b/server/api/post/my.get.ts
@@ -0,0 +1,11 @@
+import { usePostService } from '~/server/services/post.service';
+import { AuthenticatedUser } from '~/types/auth';
+import { PaginationDTO } from '~/types/dtos';
+
+export default defineEventHandler(async (event) => {
+ const { jwt } = await readBody(event);
+ const { pageNumber, pageSize } = getQuery(event);
+
+ const service = usePostService(jwt);
+ return await service.getMyPosts(pageNumber, pageSize);
+});
diff --git a/server/errors/index.ts b/server/errors/index.ts
new file mode 100644
index 0000000..8056e12
--- /dev/null
+++ b/server/errors/index.ts
@@ -0,0 +1,55 @@
+import { ServerErrorType } from '~/types/error';
+
+export class ServerError {
+ static notFound(_reason: string) {
+ return createError({
+ status: ServerErrorType.NOT_FOUND,
+ statusText: 'Not found',
+ statusMessage: _reason,
+ });
+ }
+
+ static unauthorized(_reason: string) {
+ return createError({
+ status: ServerErrorType.UNAUTHORIZED,
+ statusText: 'Unauthorized',
+ statusMessage: _reason,
+ });
+ }
+
+ static forbidden(_reason: string) {
+ return createError({
+ status: ServerErrorType.FORBIDDEN,
+ statusText: 'Forbidden',
+ statusMessage: _reason,
+ });
+ }
+
+ static internalServerError(_reason: string) {
+ return createError({
+ status: ServerErrorType.INTERNAL_SERVER_ERROR,
+ statusText: 'Internal server error',
+ statusMessage: _reason,
+ });
+ }
+
+ static unavailable() {
+ return createError({
+ status: ServerErrorType.UNAVAILABLE,
+ statusText: 'Service unavailable',
+ });
+ }
+
+ static fromCode(code: number, _reason: string) {
+ switch (code) {
+ case 401:
+ return ServerError.unauthorized(_reason);
+ case 403:
+ return ServerError.forbidden(_reason);
+ case 404:
+ return ServerError.notFound(_reason);
+ default:
+ return ServerError.internalServerError(_reason);
+ }
+ }
+}
diff --git a/server/services/auth.service.ts b/server/services/auth.service.ts
new file mode 100644
index 0000000..facdcde
--- /dev/null
+++ b/server/services/auth.service.ts
@@ -0,0 +1,42 @@
+import { useApi } from '../utils/api';
+import { GetNonceDTO, RefreshTokenDTO, RefreshTokenRequestDTO, VerifyNonce } from '~/types/auth';
+
+export function useAuthService(token?: string) {
+ const getNonce = async (address: string): Promise =>
+ await useApi('auth/nonce-message', undefined, {
+ method: 'POST',
+ body: {
+ address,
+ },
+ });
+
+ const verifyNonce = async (signature: string, address: string): Promise => {
+ const response = await useApi('auth/login', undefined, {
+ method: 'POST',
+ body: {
+ signature,
+ address,
+ },
+ });
+ return response;
+ };
+
+ const refreshToken = async (refreshToken: string): Promise => {
+ if (!token) {
+ throw new Error('Missing user token');
+ }
+ const response = await useApi('auth/refresh', token, {
+ method: 'POST',
+ body: {
+ refreshToken,
+ },
+ });
+ return response;
+ };
+
+ return {
+ getNonce,
+ verifyNonce,
+ refreshToken,
+ };
+}
diff --git a/server/services/comment.service.ts b/server/services/comment.service.ts
new file mode 100644
index 0000000..34ac4a0
--- /dev/null
+++ b/server/services/comment.service.ts
@@ -0,0 +1,67 @@
+import { useApi } from '../utils/api';
+import { AddCommentDTO, CommentDTO, CommentLikeDTO, UpdateCommentDTO } from '~/types/dtos';
+
+export function useCommentService(token: string) {
+ const getAll = async (nftAddress: string, pageNumber: number, pageSize: number): Promise => {
+ const params: URLSearchParams = new URLSearchParams({
+ pageNumber: String(pageNumber),
+ pageSize: String(pageSize),
+ });
+
+ return await useApi(`post/${nftAddress}/comments?${params.toString()}`, token);
+ };
+
+ const add = async (nftAddress: string, content: string, parentCommentId: number): Promise => {
+ const resp = await useApi(`post/${nftAddress}/comments`, token, {
+ method: 'POST',
+ body: {
+ parentCommentId,
+ content,
+ },
+ });
+ return resp;
+ };
+
+ const update = async (commentId: number, content: string): Promise => {
+ const resp = await useApi(`post/comments/${commentId}`, token, {
+ method: 'PUT',
+ body: {
+ content,
+ },
+ });
+ return resp;
+ };
+
+ const remove = async (commentId: number) => {
+ const resp = await useApi(`post/comments/${commentId}`, token, {
+ method: 'DELETE',
+ });
+ return resp;
+ };
+
+ const getLikes = async (commentId: number): Promise => {
+ return await useApi(`post/comments/${commentId}/likes`, token);
+ };
+
+ const like = async (commentId: number): Promise => {
+ return await useApi(`post/comments/${commentId}/ likes`, token, {
+ method: 'POST',
+ });
+ };
+
+ const unlike = async (commentId: number): Promise => {
+ await useApi(`post/comments/${commentId}/ likes`, token, {
+ method: 'DELETE',
+ });
+ };
+
+ return {
+ getAll,
+ getLikes,
+ like,
+ unlike,
+ add,
+ update,
+ remove,
+ };
+}
diff --git a/server/services/post.service.ts b/server/services/post.service.ts
new file mode 100644
index 0000000..e461cf4
--- /dev/null
+++ b/server/services/post.service.ts
@@ -0,0 +1,33 @@
+import { useApi } from '../utils/api';
+import { LikePostResponseDTO, NFTPost, PostLikeDTO } from '~/types/dtos';
+
+export function usePostService(token: string) {
+ const getMyPosts = async (pageNumber: number, pageSize: number): Promise => {
+ const params: URLSearchParams = new URLSearchParams({
+ pageNumber: String(pageNumber),
+ pageSize: String(pageSize),
+ });
+
+ return await useApi(`post/my?${params.toString()}`, token);
+ };
+
+ const getLikes = async (nftAddress: string): Promise => {
+ return await useApi(`post/${nftAddress}/likes}`, token);
+ };
+
+ const like = async (nftAddress: string): Promise => {
+ const resp = await useApi(`post/${nftAddress}/likes`, token, {
+ method: 'POST',
+ });
+ return resp;
+ };
+
+ const unlike = async (nftAddress: string): Promise => {
+ const resp = await useApi(`post/${nftAddress}/likes`, token, {
+ method: 'DELETE',
+ });
+ return resp;
+ };
+
+ return { getMyPosts, like, unlike, getLikes };
+}
diff --git a/server/utils/api.ts b/server/utils/api.ts
new file mode 100644
index 0000000..3108443
--- /dev/null
+++ b/server/utils/api.ts
@@ -0,0 +1,52 @@
+import { ofetch, type FetchOptions, FetchError } from 'ofetch';
+import { ServerError } from '../errors';
+
+const RUNTIME_CONFIG = useRuntimeConfig();
+
+const apiFetch = ofetch.create({
+ baseURL: RUNTIME_CONFIG.restApiUrl,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// We need this because HeadersInit is somehow broken and doesn't include Authorization
+type RequestHeaders = HeadersInit & { Authorization?: string };
+
+type RequestOptions['body'] = undefined> = Omit, 'body'> & {
+ method?: 'PUT' | 'POST' | 'GET' | 'DELETE';
+ headers?: RequestHeaders;
+} & (B extends undefined ? {} : { body: B });
+
+export const useApi = async ['body'] = undefined>(
+ path: string,
+ token?: string,
+ options?: RequestOptions,
+) => {
+ try {
+ const headers: RequestHeaders = {
+ ...options?.headers,
+ };
+ if (token) {
+ headers.Authorization = `Bearer ${token}`;
+ }
+ return await apiFetch(path, {
+ ...options,
+ headers,
+ });
+ } catch (error) {
+ if (error instanceof FetchError) {
+ if (error.statusCode) {
+ throw ServerError.fromCode(error.statusCode, error.message);
+ } else {
+ throw ServerError.unavailable();
+ }
+ } else {
+ // This is a generic error, we don't know what happened.
+
+ // eslint-disable-next-line no-console
+ console.error(error);
+ throw ServerError.internalServerError('Something went wrong');
+ }
+ }
+};
diff --git a/store/account.ts b/store/account.ts
index 2da5336..daaa066 100644
--- a/store/account.ts
+++ b/store/account.ts
@@ -17,4 +17,8 @@ export const useAccountStore = defineStore('account', {
this.alias = alias;
},
},
+
+ getters: {
+ isLogged: (state) => !!state.account,
+ },
});
diff --git a/tsconfig.json b/tsconfig.json
index a9dbc11..a746f2a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
- "extends": "./.nuxt/tsconfig.json",
- "compilerOptions": {
- "types": ["naive-ui/volar"]
- }
+ "extends": "./.nuxt/tsconfig.json"
}
diff --git a/types/auth/index.ts b/types/auth/index.ts
new file mode 100644
index 0000000..e38c28b
--- /dev/null
+++ b/types/auth/index.ts
@@ -0,0 +1,21 @@
+export interface AuthenticatedUser {
+ jwt: string;
+}
+
+export interface GetNonceDTO {
+ address: string;
+}
+
+export interface VerifyNonce {
+ signature: string;
+ address: string;
+}
+
+export interface RefreshTokenDTO {
+ accessToken: string;
+ refreshToken: string;
+}
+
+export interface RefreshTokenRequestDTO {
+ refreshToken: string;
+}
diff --git a/types/dtos/index.ts b/types/dtos/index.ts
new file mode 100644
index 0000000..7559d5a
--- /dev/null
+++ b/types/dtos/index.ts
@@ -0,0 +1,6 @@
+export * from './nft';
+
+export interface PaginationDTO {
+ pageNumber: number;
+ pageSize: number;
+}
diff --git a/types/dtos/nft.ts b/types/dtos/nft.ts
new file mode 100644
index 0000000..3d3ab56
--- /dev/null
+++ b/types/dtos/nft.ts
@@ -0,0 +1,87 @@
+export interface Attribute {
+ traitType: string;
+ value: string;
+}
+
+export interface NFTNode {
+ address: string;
+ name?: string;
+ uri?: string;
+ description?: string;
+ attributes?: Attribute[];
+ image?: string;
+ externalUrl?: string;
+ animationUrl?: string;
+ tokenId: string;
+ createdAtBlock: number;
+ raw?: string;
+ tags?: string[];
+}
+
+export interface NFTPost {
+ id: string;
+ nft: NFTNode;
+ description: string;
+ owner: {
+ username?: string;
+ address: string;
+ profileImg?: string;
+ };
+ likes: {
+ username: string;
+ address: string;
+ }[];
+ comments: {
+ username: string;
+ address: string;
+ text: string;
+ }[];
+}
+
+export interface NFTAddress {
+ address: string;
+}
+
+export interface CommentDTO {
+ id: number;
+ content: string;
+ commenterAddress: string;
+ postNFTAddress: string;
+ parentCommentId: number;
+ commentReplyCount: number;
+ likeCount: number;
+ createdAt: string;
+}
+
+export interface AddCommentDTO {
+ content: string;
+ parentCommentId: number;
+}
+
+export interface UpdateCommentDTO {
+ content: string;
+}
+
+export interface LikePostResponseDTO {
+ id: number;
+ likerAddress: string;
+ postNFTAddress: string;
+}
+
+export interface CommentLikeDTO {
+ id: number;
+ liker: {
+ address: string;
+ username: string;
+ };
+ commentId: number;
+}
+
+export interface PostLikeDTO {
+ id: number;
+ liker: {
+ address: string;
+ username: string;
+ };
+ postNFTAddress: string;
+}
diff --git a/types/error.ts b/types/error.ts
new file mode 100644
index 0000000..67b6ad0
--- /dev/null
+++ b/types/error.ts
@@ -0,0 +1,7 @@
+export enum ServerErrorType {
+ NOT_FOUND = 404,
+ UNAUTHORIZED = 401,
+ FORBIDDEN = 403,
+ INTERNAL_SERVER_ERROR = 500,
+ UNAVAILABLE = 503,
+}
diff --git a/types/index.ts b/types/index.ts
index 6b8183c..924fd26 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -5,43 +5,3 @@ declare global {
ethereum?: import('web3').providers.Eip1193Provider;
}
}
-
-export interface Attribute {
- traitType: string;
- value: string;
-}
-
-export interface NFTNode {
- address: string;
- name?: string;
- uri?: string;
- description?: string;
- attributes?: Attribute[];
- image?: string;
- externalUrl?: string;
- animationUrl?: string;
- tokenId: string;
- createdAtBlock: number;
- raw?: string;
- tags?: string[];
-}
-
-export interface NFTPost {
- id: string;
- nft: NFTNode;
- description: string;
- owner: {
- username?: string;
- address: string;
- profileImg?: string;
- };
- likes: {
- username: string;
- address: string;
- }[];
- comments: {
- username: string;
- address: string;
- text: string;
- }[];
-}