all requirement added
This commit is contained in:
parent
f3cc1147e2
commit
d304e6e69d
42 changed files with 2638 additions and 0 deletions
29
.env
Normal file
29
.env
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
SERVER_URL=http://localhost
|
||||
SERVER_PORT=3000
|
||||
|
||||
DATABASE_URL=postgres://postgres:saimon%40567@localhost:5432/planpost_canvas_dev
|
||||
|
||||
MINIO_ACCESS_KEY=rEiuiqB8JCSmWt7AswOM
|
||||
MINIO_SECRET_KEY=en3ut7Zp71uAfGrhvMkH6Pk7ZM1qZb9mFxj7KzD5
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
|
||||
JWT_ACCESS_TOKEN_SECRET=planpostai%^$_%43%65576canvas_dev_2025_planpostai!@43223_canvas$%%$$
|
||||
|
||||
JWT_REFRESH_TOKEN_SECRET=planpostai!@!@@**&$$43223_canvas_dev_2025_planpostai@45342356$%^$349332$$
|
||||
|
||||
JWT_EMAIL_TOKEN_SECRET=planpostAi!!@@!!!3242d2345ffdddf4^$367sss744!!@canvas
|
||||
|
||||
JWT_EMAIL_RESET_PASSWORD_SECRET=planpostAi!!!3ddssdeersssdffd^$367744!!!!@@!!!3242234ff54^$3677ff44!!@canvas
|
||||
|
||||
|
||||
USER_CANVAS_JWT_ACCESS_TOKEN_SECRET=planpostai%^$_%43%65576canvas%%$$
|
||||
|
||||
MAIL_HOST=mail.adspillar.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USER=canvas@adspillar.com
|
||||
MAIL_PASS=Adspillar2025!!canvas!!
|
||||
|
||||
PEXELS_URL=https://api.pexels.com/v1
|
||||
PEXELS_ACCESS_KEY=6PK7hLvOuG6nFsmC8c9EV0P8hGkyHeIhYpiRxhkEfh2ePK0GhiQypBhI
|
||||
|
||||
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
**/*.trace
|
||||
**/*.zip
|
||||
**/*.tar.gz
|
||||
**/*.tgz
|
||||
**/*.log
|
||||
package-lock.json
|
||||
**/*.bun
|
||||
BIN
bun.lockb
Normal file
BIN
bun.lockb
Normal file
Binary file not shown.
11
drizzle.config.ts
Normal file
11
drizzle.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from 'drizzle-kit';
|
||||
import { ENV } from './src/config/env';
|
||||
|
||||
export default defineConfig({
|
||||
out: './drizzle',
|
||||
schema: './src/db/schema.ts',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: ENV.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
52
drizzle/0000_workable_iron_man.sql
Normal file
52
drizzle/0000_workable_iron_man.sql
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
CREATE TABLE "project_category" (
|
||||
"category_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid,
|
||||
"category" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "projects" (
|
||||
"project_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"userId" uuid,
|
||||
"category" uuid,
|
||||
"object" json,
|
||||
"name" text,
|
||||
"description" text,
|
||||
"is_public" boolean DEFAULT true NOT NULL,
|
||||
"preview_url" text,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "shapes" (
|
||||
"shape_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"shapes" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "uploads" (
|
||||
"upload_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"filename" text NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"projectId" uuid,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "users" (
|
||||
"user_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"name" text,
|
||||
"password" text NOT NULL,
|
||||
"is_active" boolean DEFAULT true NOT NULL,
|
||||
"is_verified" boolean DEFAULT false NOT NULL,
|
||||
"is_admin" boolean DEFAULT false NOT NULL,
|
||||
"refresh_token" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "project_category" ADD CONSTRAINT "project_category_user_id_users_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("user_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_userId_users_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("user_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_category_project_category_category_id_fk" FOREIGN KEY ("category") REFERENCES "public"."project_category"("category_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "uploads" ADD CONSTRAINT "uploads_projectId_projects_project_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."projects"("project_id") ON DELETE no action ON UPDATE no action;
|
||||
354
drizzle/meta/0000_snapshot.json
Normal file
354
drizzle/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
{
|
||||
"id": "90741dda-2291-48fc-a730-1d0c20ef1991",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.project_category": {
|
||||
"name": "project_category",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"category_id": {
|
||||
"name": "category_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"project_category_user_id_users_user_id_fk": {
|
||||
"name": "project_category_user_id_users_user_id_fk",
|
||||
"tableFrom": "project_category",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"user_id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.projects": {
|
||||
"name": "projects",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"object": {
|
||||
"name": "object",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_public": {
|
||||
"name": "is_public",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"preview_url": {
|
||||
"name": "preview_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"projects_userId_users_user_id_fk": {
|
||||
"name": "projects_userId_users_user_id_fk",
|
||||
"tableFrom": "projects",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"user_id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"projects_category_project_category_category_id_fk": {
|
||||
"name": "projects_category_project_category_category_id_fk",
|
||||
"tableFrom": "projects",
|
||||
"tableTo": "project_category",
|
||||
"columnsFrom": [
|
||||
"category"
|
||||
],
|
||||
"columnsTo": [
|
||||
"category_id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.shapes": {
|
||||
"name": "shapes",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"shape_id": {
|
||||
"name": "shape_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"shapes": {
|
||||
"name": "shapes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.uploads": {
|
||||
"name": "uploads",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"upload_id": {
|
||||
"name": "upload_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"filename": {
|
||||
"name": "filename",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"projectId": {
|
||||
"name": "projectId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"uploads_projectId_projects_project_id_fk": {
|
||||
"name": "uploads_projectId_projects_project_id_fk",
|
||||
"tableFrom": "uploads",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"projectId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"project_id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"is_verified": {
|
||||
"name": "is_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"is_admin": {
|
||||
"name": "is_admin",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1739942538665,
|
||||
"tag": "0000_workable_iron_man",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
18
env.example.js
Normal file
18
env.example.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// all of these are required to run the project
|
||||
|
||||
|
||||
// SERVER_URL
|
||||
// SERVER_PORT
|
||||
|
||||
// DATABASE_URL
|
||||
|
||||
// MINIO_ACCESS_KEY
|
||||
// MINIO_SECRET_KEY
|
||||
// MINIO_ENDPOINT
|
||||
// MINIO_PORT
|
||||
|
||||
// CLERK_SECRET_KEY
|
||||
|
||||
// JWT_ACCESS_TOKEN_SECRET
|
||||
|
||||
// JWT_REFRESH_TOKEN_SECRET
|
||||
28
env.examples
Normal file
28
env.examples
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
SERVER_URL=
|
||||
SERVER_PORT=
|
||||
|
||||
DATABASE_URL=
|
||||
|
||||
MINIO_ACCESS_KEY=
|
||||
MINIO_SECRET_KEY=
|
||||
MINIO_ENDPOINT=
|
||||
MINIO_PORT=
|
||||
|
||||
JWT_ACCESS_TOKEN_SECRET=
|
||||
|
||||
JWT_REFRESH_TOKEN_SECRET=
|
||||
|
||||
JWT_EMAIL_TOKEN_SECRET=
|
||||
|
||||
JWT_EMAIL_RESET_PASSWORD_SECRET=
|
||||
|
||||
|
||||
USER_CANVAS_JWT_ACCESS_TOKEN_SECRET=
|
||||
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=
|
||||
MAIL_USER=
|
||||
MAIL_PASS=
|
||||
|
||||
PEXELS_URL=
|
||||
PEXELS_ACCESS_KEY=
|
||||
32
package.json
Normal file
32
package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "canvas_backend",
|
||||
"version": "1.0.50",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push:pg",
|
||||
"dev": "bun run --watch src/app.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/cookie": "^0.8.0",
|
||||
"@elysiajs/cors": "^1.2.0",
|
||||
"@elysiajs/swagger": "^1.2.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-orm": "^0.38.4",
|
||||
"elysia": "latest",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"minio": "^8.0.3",
|
||||
"nodemailer": "^6.10.0",
|
||||
"pg": "^8.13.1",
|
||||
"postgres": "^3.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.11.10",
|
||||
"bun-types": "latest",
|
||||
"drizzle-kit": "^0.30.2",
|
||||
"tsx": "^4.19.2"
|
||||
},
|
||||
"module": "src/app.js"
|
||||
}
|
||||
150
src/api/auth/auth.controller.ts
Normal file
150
src/api/auth/auth.controller.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { ENV } from "../../config/env"
|
||||
// @ts-ignore
|
||||
import jwt from "jsonwebtoken";
|
||||
import { sendResetPasswordEmail, sendVerificationEmail, storeRefreshToken } from "../../helper/auth/auth.helper";
|
||||
import { db } from "../../db";
|
||||
import { users } from "../../db/schema";
|
||||
// @ts-ignore
|
||||
import { eq } from "drizzle-orm";
|
||||
import path from "path";
|
||||
|
||||
export const registerUser = async (email: string, password: string, name: string, set: any) => {
|
||||
const bcryptHash = await Bun.password.hash(password, {
|
||||
algorithm: "bcrypt",
|
||||
cost: 10, // number between 4-31
|
||||
});
|
||||
|
||||
const saveUser = await db.insert(users).values({ email, password: bcryptHash, name }).returning({ id: users.id });
|
||||
|
||||
if (saveUser.length > 0 && saveUser[0].id) {
|
||||
const token = jwt.sign({ id: saveUser[0].id }, ENV.JWT_EMAIL_TOKEN_SECRET, { expiresIn: '10m' });
|
||||
const response = await sendVerificationEmail(email, token, set);
|
||||
return response;
|
||||
} else {
|
||||
set.status = 500;
|
||||
return { status: 500, message: "Failed to register user" };
|
||||
}
|
||||
};
|
||||
|
||||
export const loginUser = async (email: string, cookie: any, set: any) => {
|
||||
try {
|
||||
// generate access token
|
||||
const accessToken = jwt.sign({ email }, ENV.JWT_ACCESS_TOKEN_SECRET, { expiresIn: '3h' });
|
||||
|
||||
// generate refresh token
|
||||
const refreshToken = jwt.sign({ email }, ENV.JWT_REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
|
||||
|
||||
// store refresh token in db
|
||||
const storeRToken = await storeRefreshToken(email, refreshToken);
|
||||
|
||||
if (storeRToken.success === true) {
|
||||
cookie.access_token.set({
|
||||
value: accessToken,
|
||||
httpOnly: true,
|
||||
secure: true, // Set to true in production
|
||||
sameSite: 'none', // Adjust based on your needs
|
||||
path: "/",
|
||||
maxAge: 3 * 60 * 60, // 3 hours in seconds
|
||||
});
|
||||
cookie.refresh_token.set({
|
||||
value: refreshToken,
|
||||
httpOnly: true,
|
||||
secure: true, // Set to true in production
|
||||
sameSite: 'none', // Adjust based on your needs
|
||||
path: "/",
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days in seconds
|
||||
});
|
||||
set.status = 200;
|
||||
return { status: 200, message: "User log-in successful", token: accessToken };
|
||||
}
|
||||
else {
|
||||
set.status = 500;
|
||||
return { status: 500, message: storeRToken.message };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
set.status = 500;
|
||||
return { status: 500, message: "Internal server error" };
|
||||
}
|
||||
}
|
||||
|
||||
// for resetting password, this will send the verification e-mail
|
||||
export const resetPassword = async (email: string, set: any) => {
|
||||
try {
|
||||
const user = await db.select({ id: users.id }).from(users).where(eq(users.email, email));
|
||||
if (user.length === 0) {
|
||||
set.status = 404;
|
||||
return { status: 404, message: "User not found" };
|
||||
}
|
||||
else {
|
||||
const token = jwt.sign({ id: user[0].id }, ENV.JWT_EMAIL_RESET_PASSWORD_SECRET, { expiresIn: '10m' });
|
||||
const sendEmail = await sendResetPasswordEmail(email, token, set);
|
||||
return sendEmail;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
set.status = 500;
|
||||
return { status: 500, message: "Internal server error" };
|
||||
}
|
||||
}
|
||||
|
||||
// for resending verification e-mail this is only for users who have not verified their e-mail yet
|
||||
export const resendVerificationEmail = async (email: string, set: any) => {
|
||||
try {
|
||||
const user = await db.select({ id: users.id, isVerified: users.is_verified, isActive: users.is_active }).from(users).where(eq(users.email, email));
|
||||
if (user.length === 0) {
|
||||
set.status = 404;
|
||||
return { status: 404, message: "User not found" };
|
||||
}
|
||||
else {
|
||||
if (user[0].isVerified) {
|
||||
set.status = 400;
|
||||
return { status: 400, message: "User already verified" };
|
||||
}
|
||||
else if (!user[0].isActive) {
|
||||
set.status = 400;
|
||||
return { status: 400, message: "User is not active" };
|
||||
}
|
||||
else {
|
||||
const token = jwt.sign({ id: user[0].id }, ENV.JWT_EMAIL_TOKEN_SECRET, { expiresIn: '10m' });
|
||||
const sendEmail = await sendVerificationEmail(email, token, set);
|
||||
return sendEmail;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
set.status = 500;
|
||||
return { status: 500, message: "Internal server error" };
|
||||
}
|
||||
}
|
||||
|
||||
// this controller is for e-mail verification
|
||||
export const verifyUser = async (id: string, set: any) => {
|
||||
try {
|
||||
const user = await db.select({ isVerified: users.is_verified }).from(users).where(eq(users.id, id));
|
||||
|
||||
if (user.length === 0) {
|
||||
set.status = 404;
|
||||
return Bun.file(path.resolve('./src/html/error.html'));
|
||||
}
|
||||
|
||||
if (user[0].isVerified) {
|
||||
set.status = 200;
|
||||
return Bun.file(path.resolve('./src/html/alreadyVerify.html'));
|
||||
}
|
||||
|
||||
await db.update(users).set({ is_verified: true }).where(eq(users.id, id));
|
||||
set.status = 200;
|
||||
return Bun.file(path.resolve('./src/html/success.html'));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
set.status = 500;
|
||||
return { status: 500, message: "Internal server error" };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
178
src/api/auth/auth.route.ts
Normal file
178
src/api/auth/auth.route.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import Elysia, { t } from "elysia";
|
||||
import { verifyAuth } from "../../middlewares/auth.middlewares";
|
||||
import { loginUser, registerUser, resendVerificationEmail, resetPassword, verifyUser } from "./auth.controller";
|
||||
import { checkUserInDb } from "../../helper/auth/auth.helper";
|
||||
// @ts-ignore
|
||||
import jwt from "jsonwebtoken";
|
||||
import { ENV } from "../../config/env";
|
||||
import path from "path";
|
||||
import { db } from "../../db";
|
||||
import { users } from "../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const authRoute = new Elysia({
|
||||
prefix: "/auth",
|
||||
tags: ["Auth"],
|
||||
detail: {
|
||||
description: "Routes for managing users",
|
||||
}
|
||||
});
|
||||
|
||||
authRoute.post("/login", async ({ body, cookie, set }) => {
|
||||
const { email, password } = body;
|
||||
const findUser = await checkUserInDb(email, password);
|
||||
if (findUser.success === true && findUser.can_login === true) {
|
||||
const response = await loginUser(findUser.email as string, cookie, set);
|
||||
return response;
|
||||
}
|
||||
else if (findUser.message === "User not verified") {
|
||||
set.status = 401;
|
||||
return { status: 401, message: "User not verified, please verify your email" };
|
||||
}
|
||||
else {
|
||||
set.status = 404;
|
||||
return { status: 404, message: findUser.message };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
email: t.String(),
|
||||
password: t.String()
|
||||
})
|
||||
});
|
||||
|
||||
authRoute.post("/register", async ({ body, set }) => {
|
||||
const { email, password, name } = body;
|
||||
const findUser = await checkUserInDb(email, password);
|
||||
if (findUser.success === true && findUser.can_register === true) {
|
||||
const response = await registerUser(email, password, name, set);
|
||||
return response;
|
||||
}
|
||||
else {
|
||||
set.status = 409;
|
||||
return { status: 409, message: "User already exists, please login" };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
email: t.String(),
|
||||
password: t.String(),
|
||||
name: t.String(),
|
||||
})
|
||||
})
|
||||
|
||||
authRoute.get("/isAuthenticated", async ({ cookie }) => {
|
||||
const response = await verifyAuth(cookie);
|
||||
return response;
|
||||
})
|
||||
|
||||
// for resetting password
|
||||
authRoute.post('/reset-password/verify', async ({ body, set }) => {
|
||||
const { email } = body;
|
||||
const response = await resetPassword(email, set);
|
||||
return response;
|
||||
}, {
|
||||
body: t.Object({
|
||||
email: t.String(),
|
||||
})
|
||||
})
|
||||
|
||||
// for e-mail verification
|
||||
authRoute.get('/verify', async ({ query, set }) => {
|
||||
const { token } = query;
|
||||
try {
|
||||
const decoded = jwt.verify(token, ENV.JWT_EMAIL_TOKEN_SECRET);
|
||||
const response = await verifyUser(decoded.id as string, set);
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
set.status = 400;
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return Bun.file(path.resolve('./src/html/error.html'));
|
||||
}
|
||||
return Bun.file(path.resolve('./src/html/error.html'));
|
||||
}
|
||||
});
|
||||
|
||||
// for resend verification email, if user did not receive the verification email or the verification email expired
|
||||
authRoute.post('/resend-verification', async ({ body, set }) => {
|
||||
const { email } = body;
|
||||
const response = await resendVerificationEmail(email, set);
|
||||
return response;
|
||||
}, {
|
||||
body: t.Object({
|
||||
email: t.String(),
|
||||
})
|
||||
});
|
||||
|
||||
authRoute.get("/reset-password", async ({ query, set }) => {
|
||||
const { token } = query;
|
||||
try {
|
||||
const decoded = jwt.verify(token, ENV.JWT_EMAIL_RESET_PASSWORD_SECRET) as { id: string };
|
||||
|
||||
if (!decoded?.id) {
|
||||
set.status = 400;
|
||||
return { status: 400, message: "Invalid token" };
|
||||
}
|
||||
|
||||
// Serve the reset password page where the user can enter a new password
|
||||
return Bun.file(path.resolve('./src/html/resetPassword.html'));
|
||||
|
||||
} catch (error: any) {
|
||||
set.status = 400;
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return Bun.file(path.resolve('./src/html/error.html'));
|
||||
}
|
||||
return Bun.file(path.resolve('./src/html/error.html'));
|
||||
}
|
||||
}, {
|
||||
query: t.Object({
|
||||
token: t.String(),
|
||||
})
|
||||
});
|
||||
|
||||
authRoute.post("/reset-password", async ({ query, set, body }) => {
|
||||
const { token } = query;
|
||||
const { password } = body;
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, ENV.JWT_EMAIL_RESET_PASSWORD_SECRET) as { id: string };
|
||||
|
||||
if (!decoded?.id) {
|
||||
set.status = 400;
|
||||
return { status: 400, message: "Invalid token" };
|
||||
}
|
||||
|
||||
// Hash the new password
|
||||
const bcryptHash = await Bun.password.hash(password, {
|
||||
algorithm: "bcrypt",
|
||||
cost: 10,
|
||||
});
|
||||
|
||||
// Update password in the database
|
||||
const updatePassword = await db
|
||||
.update(users)
|
||||
.set({ password: bcryptHash })
|
||||
.where(eq(users.id, decoded.id));
|
||||
|
||||
if (updatePassword?.rowCount !== 0) {
|
||||
return { status: 200, message: "Password updated successfully" };
|
||||
} else {
|
||||
set.status = 400;
|
||||
return { status: 400, message: "Password update failed" };
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
set.status = 400;
|
||||
if (error.name === "TokenExpiredError") {
|
||||
return { status: 400, message: "Token has expired" };
|
||||
}
|
||||
return { status: 400, message: "Invalid request" };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
password: t.String(),
|
||||
}),
|
||||
query: t.Object({
|
||||
token: t.String(),
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
45
src/api/category/category.controller.ts
Normal file
45
src/api/category/category.controller.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db";
|
||||
import { category } from "../../db/schema";
|
||||
|
||||
export const getAllCategory = async (token: string) => {
|
||||
try {
|
||||
const allCategory = await db.select({ id: category?.id, name: category?.category }).from(category);
|
||||
|
||||
if (allCategory.length === 0) {
|
||||
return { status: 404, message: "No categories found", token }
|
||||
}
|
||||
|
||||
return { status: 200, message: "Categories fetched successfully", data: allCategory, token };
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching categories:', error);
|
||||
return { status: 500, message: "An error occurred while fetching the categories", token };
|
||||
}
|
||||
}
|
||||
|
||||
export const createCategory = async (token: string, name: string, user_id: string) => {
|
||||
try {
|
||||
const newCategory = await db.insert(category).values({ category: name, user_id: user_id });
|
||||
|
||||
return { status: 200, message: "Category created successfully", data: newCategory, token };
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error creating category:', error);
|
||||
return { status: 500, message: "An error occurred while creating the category", token };
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteCategory = async (token: string, id: string) => {
|
||||
try {
|
||||
const deleted = await db.delete(category).where(eq(category?.id, id)).returning({ id: category?.id, name: category?.category });
|
||||
if (deleted.length === 0) {
|
||||
return { status: 404, message: "Category not found", token };
|
||||
}
|
||||
return { status: 200, message: "Category deleted successfully", data: deleted, token };
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('Error deleting category:', error);
|
||||
return { status: 500, message: "An error occurred while deleting the category", token };
|
||||
}
|
||||
}
|
||||
53
src/api/category/category.route.ts
Normal file
53
src/api/category/category.route.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import Elysia, { t } from "elysia";
|
||||
import { verifyAuth } from "../../middlewares/auth.middlewares";
|
||||
import { createCategory, deleteCategory, getAllCategory } from "./category.controller";
|
||||
|
||||
export const categoryRoutes = new Elysia({
|
||||
prefix: "/category",
|
||||
tags: ["Category routes"],
|
||||
detail: {
|
||||
description: "Routes for managing project category",
|
||||
}
|
||||
}).derive(async ({ cookie }) => {
|
||||
const authData = await verifyAuth(cookie);
|
||||
return { authData }; // Inject into context
|
||||
});
|
||||
|
||||
categoryRoutes.get("/get-all", async ({ authData }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const data = await getAllCategory(token);
|
||||
return data;
|
||||
}
|
||||
})
|
||||
|
||||
categoryRoutes.post("/create", async ({ authData, body: { name } }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const user_id = authData?.userId;
|
||||
const data = await createCategory(token, name, user_id);
|
||||
return data;
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
})
|
||||
})
|
||||
|
||||
categoryRoutes.delete("/delete/:id", async ({ authData, params: { id } }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const data = await deleteCategory(token, id);
|
||||
return data;
|
||||
}
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
})
|
||||
})
|
||||
29
src/api/design/design.controller.ts
Normal file
29
src/api/design/design.controller.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db";
|
||||
import { projects } from "../../db/schema";
|
||||
|
||||
export const getAllDesign = async (token: string) => {
|
||||
try {
|
||||
const getDesign = await db.select({
|
||||
object: projects.object,
|
||||
name: projects.name,
|
||||
description: projects.description,
|
||||
preview_url: projects.preview_url,
|
||||
}).from(projects).where(eq(projects.is_public, true));
|
||||
|
||||
// Filter out projects where the object field is an empty object
|
||||
const filteredDesigns = getDesign?.filter(project =>
|
||||
project.object && Object.keys(project.object).length > 0
|
||||
);
|
||||
|
||||
if (filteredDesigns.length === 0) {
|
||||
return { status: 404, message: "No designs found", token };
|
||||
} else {
|
||||
return { status: 200, message: "Designs fetched successfully", data: filteredDesigns, token };
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching designs:', error);
|
||||
return { status: 500, message: "Internal Server Error" };
|
||||
}
|
||||
};
|
||||
28
src/api/design/design.route.ts
Normal file
28
src/api/design/design.route.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Elysia, t } from "elysia";
|
||||
import { ENV } from "../../config/env";
|
||||
// @ts-ignore
|
||||
import jwt from "jsonwebtoken";
|
||||
import { getAllDesign } from "./design.controller";
|
||||
|
||||
export const designRoutes = new Elysia({
|
||||
prefix: "/design",
|
||||
tags: ["Design"],
|
||||
detail: {
|
||||
description: "Routes for managing design",
|
||||
}
|
||||
}).derive(({ headers }) => {
|
||||
const token = headers?.authorization?.split(" ")[1];
|
||||
return { token };
|
||||
});
|
||||
|
||||
designRoutes.get("/", async ({ token }) => {
|
||||
const verifyToken = await jwt.verify(token, ENV.USER_CANVAS_JWT_ACCESS_TOKEN_SECRET);
|
||||
if (!verifyToken) {
|
||||
return { status: 401, message: "Unauthorized" }
|
||||
}
|
||||
else {
|
||||
const data = await getAllDesign(token as string);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
24
src/api/index.ts
Normal file
24
src/api/index.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import Elysia from "elysia";
|
||||
import { projectRoutes } from "./project/project.route";
|
||||
import { uploadRoutes } from "./upload/upload.route";
|
||||
import { authRoute } from "./auth/auth.route";
|
||||
import { uploadShapesRoutes } from "./uploadShapes/upload.shapes.route";
|
||||
import { photoLibraryRoutes } from "./photoLibrary/photo.library.route";
|
||||
import { categoryRoutes } from "./category/category.route";
|
||||
import { designRoutes } from "./design/design.route";
|
||||
|
||||
export const api = new Elysia({
|
||||
prefix: "/api",
|
||||
});
|
||||
|
||||
api.get("/", () => {
|
||||
return "Hello from PlanPostAI Canvas API"
|
||||
})
|
||||
|
||||
api.use(authRoute);
|
||||
api.use(projectRoutes);
|
||||
api.use(uploadRoutes);
|
||||
api.use(photoLibraryRoutes);
|
||||
api.use(uploadShapesRoutes);
|
||||
api.use(categoryRoutes);
|
||||
api.use(designRoutes);
|
||||
21
src/api/photoLibrary/photo.library.controller.ts
Normal file
21
src/api/photoLibrary/photo.library.controller.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { ENV } from "../../config/env";
|
||||
|
||||
export const getPhotos = async (keyword: string, pre_page: number, token: string) => {
|
||||
try {
|
||||
const url = `${ENV.PEXELS_URL}/search?query=${keyword}&per_page=${pre_page}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: process.env.PEXELS_ACCESS_KEY as string,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return { status: 500, message: "An error occurred while getting the photos", token }
|
||||
}
|
||||
const data = await response.json();
|
||||
return { data, token }
|
||||
} catch (error: any) {
|
||||
console.log("Error in getting photos:", error.message || error.toString());
|
||||
return { status: 500, message: "An error occurred while getting the photos", token };
|
||||
}
|
||||
}
|
||||
31
src/api/photoLibrary/photo.library.route.ts
Normal file
31
src/api/photoLibrary/photo.library.route.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import Elysia, { t } from "elysia";
|
||||
import { verifyAuth } from "../../middlewares/auth.middlewares";
|
||||
import { getPhotos } from "./photo.library.controller";
|
||||
|
||||
export const photoLibraryRoutes = new Elysia({
|
||||
prefix: "/photos",
|
||||
tags: ["Photo library"],
|
||||
detail: {
|
||||
description: "Routes for managing photo library",
|
||||
}
|
||||
}).derive(async ({ cookie }) => {
|
||||
const authData = await verifyAuth(cookie);
|
||||
return { authData }; // Inject into context
|
||||
});
|
||||
|
||||
photoLibraryRoutes.get("/", async ({ query, authData
|
||||
}) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const { keyword, per_page } = query;
|
||||
const token = authData?.token;
|
||||
const data = await getPhotos(keyword, per_page, token);
|
||||
return { data };
|
||||
}
|
||||
}, {
|
||||
query: t.Object({
|
||||
keyword: t.String(),
|
||||
per_page: t.Number(),
|
||||
})
|
||||
})
|
||||
191
src/api/project/project.controller.ts
Normal file
191
src/api/project/project.controller.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db";
|
||||
import { category, projects, uploads } from "../../db/schema";
|
||||
import { createEmptyProject } from "../../helper/projects/createProject";
|
||||
import { createBucket } from "../../helper/upload/createBucket";
|
||||
import { removeBucket } from "../../helper/upload/removeBucket";
|
||||
|
||||
export const getAllProjects = async (userId: string, token: string) => {
|
||||
try {
|
||||
// Fetch all projects for the given user
|
||||
const allProjects = await db.select({
|
||||
id: projects.id,
|
||||
name: projects.name,
|
||||
description: projects.description,
|
||||
preview_url: projects.preview_url,
|
||||
object: projects.object,
|
||||
category_name: category.category,
|
||||
category: category.id,
|
||||
}).from(projects).leftJoin(category, eq(projects.category, category.id)).where(eq(projects.userId, userId));
|
||||
|
||||
// Identify projects where 'object' is empty or 'object.objects' is empty
|
||||
const projectsToDelete = allProjects?.filter(proj =>
|
||||
(proj.object && typeof proj.object === "object" && Object.keys(proj.object).length === 0) ||
|
||||
(proj.object?.objects && Array.isArray(proj.object.objects) && proj.object.objects.length === 0)
|
||||
);
|
||||
|
||||
// Delete projects with empty 'object' or empty 'object.objects'
|
||||
await Promise.all(
|
||||
projectsToDelete?.map(async (proj) => {
|
||||
// Step 1: Delete associated uploads first
|
||||
await db.delete(uploads).where(eq(uploads.projectId, proj.id));
|
||||
|
||||
// Step 2: Delete the project itself
|
||||
await db.delete(projects).where(eq(projects.id, proj.id));
|
||||
|
||||
// Step 3: Delete the associated bucket
|
||||
await removeBucket(proj.id);
|
||||
})
|
||||
);
|
||||
|
||||
// Get remaining projects
|
||||
const remainingProjects = allProjects?.filter(proj =>
|
||||
!(
|
||||
(proj.object && typeof proj.object === "object" && Object.keys(proj.object).length === 0) ||
|
||||
(proj.object?.objects && Array.isArray(proj.object.objects) && proj.object.objects.length === 0)
|
||||
)
|
||||
);
|
||||
|
||||
if (remainingProjects?.length === 0) {
|
||||
return { status: 404, message: "No projects found", token };
|
||||
}
|
||||
|
||||
return { status: 200, message: "Projects fetched successfully", data: remainingProjects, token };
|
||||
} catch (error: any) {
|
||||
console.log(error.message);
|
||||
return { status: 500, message: "An error occurred while fetching projects", token };
|
||||
}
|
||||
};
|
||||
|
||||
export const getEachProjects = async (id: string, token: string) => {
|
||||
try {
|
||||
const project = await db.select({
|
||||
id: projects.id,
|
||||
name: projects.name,
|
||||
description: projects.description,
|
||||
preview_url: projects.preview_url,
|
||||
object: projects.object,
|
||||
category_name: category.category,
|
||||
category_id: projects.category,
|
||||
}).from(projects).leftJoin(category, eq(projects.category, category.id)).where(eq(projects.id, id));
|
||||
|
||||
if (project.length === 0) {
|
||||
return { status: 404, message: "Project not found", token };
|
||||
}
|
||||
return { status: 200, message: "Project fetched successfully", data: project[0], token };
|
||||
} catch (error: any) {
|
||||
console.log(error.message);
|
||||
return { status: 500, message: "An error occurred while fetching projects", token };
|
||||
}
|
||||
};
|
||||
|
||||
export const createProject = async (userId: string, token: string) => {
|
||||
try {
|
||||
const { id } = await createEmptyProject(userId);
|
||||
const bucket = await createBucket(id);
|
||||
return { status: 200, message: "New project created successfully", data: { id, bucketName: bucket }, token };
|
||||
|
||||
} catch (error: any) {
|
||||
console.log(error.message);
|
||||
return { status: 500, message: "An error occurred while creating projects", token }
|
||||
}
|
||||
};
|
||||
|
||||
export const updateProject = async (id: string, body: any, token: string) => {
|
||||
try {
|
||||
// 1. Validate if project exists
|
||||
const existingProject = await db.select().from(projects).where(eq(projects.id, id));
|
||||
if (existingProject.length === 0) {
|
||||
return { status: 404, message: "Project not found", token };
|
||||
}
|
||||
|
||||
const { object, name, description, preview_url, category } = body;
|
||||
|
||||
// The preview_url will come from client-side as well, where before updating the project a project capture will be taken and uploaded to the bucket. than the url will be sent to the server.And rest of them are normal process
|
||||
|
||||
// 2. Validate if the category exists
|
||||
if (category === "" || category === undefined || category === null) {
|
||||
const updatedProject = await db.update(projects).set({
|
||||
object,
|
||||
name,
|
||||
description,
|
||||
preview_url,
|
||||
}).where(eq(projects.id, id)).returning({
|
||||
id: projects.id,
|
||||
object: projects.object,
|
||||
name: projects.name,
|
||||
description: projects.description,
|
||||
preview_url: projects.preview_url,
|
||||
category: projects.category
|
||||
});
|
||||
|
||||
if (updatedProject.length === 0) {
|
||||
return { status: 500, message: "Failed to update the project", token };
|
||||
}
|
||||
return { status: 200, message: "Project updated successfully", data: updatedProject[0], token };
|
||||
}
|
||||
else {
|
||||
const updatedProject = await db.update(projects).set({
|
||||
object,
|
||||
name,
|
||||
description,
|
||||
preview_url,
|
||||
category
|
||||
}).where(eq(projects.id, id)).returning({
|
||||
id: projects.id,
|
||||
object: projects.object,
|
||||
name: projects.name,
|
||||
description: projects.description,
|
||||
preview_url: projects.preview_url,
|
||||
category: projects.category
|
||||
});
|
||||
|
||||
if (updatedProject.length === 0) {
|
||||
return { status: 500, message: "Failed to update the project", token };
|
||||
}
|
||||
return { status: 200, message: "Project updated successfully", data: updatedProject[0], token };
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log("Error updating project:", error.message || error.toString());
|
||||
return { status: 500, message: "An error occurred while updating the project", token };
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteProject = async (id: string, token: string) => {
|
||||
try {
|
||||
const deletedUploads = await db
|
||||
.delete(uploads)
|
||||
.where(eq(uploads.projectId, id))
|
||||
.returning({ id: uploads.id });
|
||||
|
||||
if (deletedUploads.length >= 0) {
|
||||
// Step 4: Delete the project
|
||||
const deletedProject = await db
|
||||
.delete(projects)
|
||||
.where(eq(projects.id, id))
|
||||
.returning({ id: projects.id });
|
||||
|
||||
if (deletedProject.length === 0) {
|
||||
return { status: 404, message: "Project not found", token };
|
||||
}
|
||||
|
||||
// Step 5: Delete the associated bucket
|
||||
const bucketDeletionResult = await removeBucket(id);
|
||||
|
||||
if (bucketDeletionResult.status !== 200) {
|
||||
return {
|
||||
status: bucketDeletionResult.status,
|
||||
message: `Error deleting bucket: ${bucketDeletionResult.message}`,
|
||||
token
|
||||
};
|
||||
}
|
||||
return { status: 200, message: "Project and associated bucket deleted successfully", token };
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log("Error in deleteProject:", error.message || error.toString());
|
||||
return { status: 500, message: "An error occurred while deleting the project", token };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
86
src/api/project/project.route.ts
Normal file
86
src/api/project/project.route.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { Elysia, t } from "elysia";
|
||||
import { createProject, deleteProject, getAllProjects, getEachProjects, updateProject } from "./project.controller";
|
||||
import { verifyAuth } from "../../middlewares/auth.middlewares";
|
||||
|
||||
export const projectRoutes = new Elysia({
|
||||
prefix: "/projects",
|
||||
tags: ["Projects"],
|
||||
detail: {
|
||||
description: "Routes for managing projects",
|
||||
}
|
||||
}).derive(async ({ cookie }) => {
|
||||
const authData = await verifyAuth(cookie);
|
||||
return { authData }; // Inject into context
|
||||
});
|
||||
|
||||
projectRoutes.get("/each/:project_id", async ({ params: { project_id }, authData }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const response = await getEachProjects(project_id, token);
|
||||
return response;
|
||||
}
|
||||
}, {
|
||||
params: t.Object({
|
||||
project_id: t.String()
|
||||
})
|
||||
});
|
||||
|
||||
projectRoutes.get("/", async ({ authData }: any) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const userId = authData.userId;
|
||||
const token = authData.token;
|
||||
const response = await getAllProjects(userId, token);
|
||||
return response;
|
||||
}
|
||||
});
|
||||
|
||||
projectRoutes.post("/create", async ({ authData }: any) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const userId = authData.userId;
|
||||
const token = authData.token;
|
||||
const response = await createProject(userId, token);
|
||||
return response;
|
||||
}
|
||||
});
|
||||
|
||||
projectRoutes.put("/update/:project_id", async ({ body, params: { project_id }, authData }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const response = await updateProject(project_id, body, token);
|
||||
return response;
|
||||
}
|
||||
}, {
|
||||
params: t.Object({
|
||||
project_id: t.String()
|
||||
}),
|
||||
body: t.Object({
|
||||
object: t.Record(t.String(), t.Any()), // Allows any JSON object
|
||||
name: t.String(),
|
||||
description: t.String(),
|
||||
preview_url: t.String(),
|
||||
category: t.String(),
|
||||
})
|
||||
});
|
||||
|
||||
projectRoutes.delete("/delete/:project_id", async ({ params: { project_id }, authData }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const response = await deleteProject(project_id, token);
|
||||
return response;
|
||||
}
|
||||
}, {
|
||||
params: t.Object({
|
||||
project_id: t.String()
|
||||
})
|
||||
});
|
||||
|
||||
106
src/api/upload/upload.controller.ts
Normal file
106
src/api/upload/upload.controller.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db";
|
||||
import { projects, uploads } from "../../db/schema";
|
||||
import { uploadToMinio } from "../../helper/upload/uploadToMinio";
|
||||
import { removeFromMinio } from "../../helper/upload/removeFromMinio";
|
||||
|
||||
export const uploadPhoto = async (file: File, project_id: string, userId: string, token: string) => {
|
||||
try {
|
||||
// Validate userId
|
||||
if (!userId || typeof userId !== "string") {
|
||||
return { status: 400, message: "Invalid user ID", token };
|
||||
}
|
||||
|
||||
// Validate projectId
|
||||
if (!project_id || typeof project_id !== "string") {
|
||||
return { status: 400, message: "Invalid project ID", token };
|
||||
}
|
||||
|
||||
// Validate file input
|
||||
if (!file || !(file instanceof File) || !file.name) {
|
||||
return { status: 400, message: "Invalid or missing file", token };
|
||||
}
|
||||
const findProject = await db.select().from(projects).where(eq(projects.id, project_id));
|
||||
|
||||
if (findProject.length > 0) {
|
||||
// Extract file extension (e.g., ".jpg", ".png")
|
||||
const fileExtension = file.name.substring(file.name.lastIndexOf("."));
|
||||
|
||||
// Generate a unique filename using the timestamp
|
||||
const timestamp = Date.now(); // Current timestamp in milliseconds
|
||||
const uniqueFileName = `${file.name.split(".")[0]}-${timestamp}${fileExtension}`;
|
||||
|
||||
// Upload file to MinIO with the unique filename
|
||||
const urlLink = await uploadToMinio(file, project_id, uniqueFileName);
|
||||
if (!urlLink || !urlLink.url) {
|
||||
return { status: 500, message: "File upload failed", token };
|
||||
}
|
||||
|
||||
// Save file info in DB with modified filename
|
||||
const saveFile = await db.insert(uploads).values({
|
||||
filename: uniqueFileName,
|
||||
url: urlLink.url,
|
||||
projectId: project_id,
|
||||
}).returning();
|
||||
|
||||
return { status: 200, message: "File uploaded successfully", data: saveFile, token };
|
||||
}
|
||||
else {
|
||||
return { status: 404, message: "No projects found with this project id", token }
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error processing file:", error);
|
||||
return { status: 500, message: "An error occurred while uploading the photo", token };
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePhoto = async (url: string, token: string) => {
|
||||
try {
|
||||
if (!url) {
|
||||
return { status: 404, message: "File url is missing", token }
|
||||
}
|
||||
|
||||
const deleteFile = await db
|
||||
.delete(uploads)
|
||||
.where(eq(uploads.url, url))
|
||||
.returning();
|
||||
|
||||
// Ensure there's a file to delete
|
||||
if (!deleteFile || deleteFile.length === 0) {
|
||||
return { status: 404, message: "File not found", token };
|
||||
}
|
||||
|
||||
const { projectId, filename } = deleteFile[0];
|
||||
|
||||
// Ensure projectId and filename are valid
|
||||
if (!projectId || !filename) {
|
||||
return { status: 400, message: "Invalid project ID or filename", token };
|
||||
}
|
||||
|
||||
const minioRemove = await removeFromMinio(projectId, filename);
|
||||
|
||||
return { status: 200, message: minioRemove.msg, token };
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error processing file:", error);
|
||||
return { status: 500, message: `An error occurred while deleting the photo: ${error.message}`, token };
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllPhoto = async (id: string, token: string) => {
|
||||
try {
|
||||
// project id
|
||||
if (!id) {
|
||||
return { status: 404, message: "Project ID is missing", token }
|
||||
}
|
||||
const getAllPhoto = await db.select().from(uploads).where(eq(uploads.projectId, id));
|
||||
if (getAllPhoto.length === 0) {
|
||||
return { status: 200, message: "No photos found for the given project ID", data: [], token }
|
||||
}
|
||||
return { status: 200, message: "All photos retrieved successfully", data: getAllPhoto, token };
|
||||
|
||||
} catch (error: any) {
|
||||
console.log(`Error getting photos: ${error.message}`);
|
||||
return { status: 500, message: "An error occurred while getting the photos", token }
|
||||
}
|
||||
}
|
||||
60
src/api/upload/upload.route.ts
Normal file
60
src/api/upload/upload.route.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { Elysia, t } from "elysia";
|
||||
import { deletePhoto, getAllPhoto, uploadPhoto } from "./upload.controller";
|
||||
import { verifyAuth } from "../../middlewares/auth.middlewares";
|
||||
|
||||
export const uploadRoutes = new Elysia({
|
||||
prefix: "/uploads",
|
||||
tags: ["Uploads"],
|
||||
detail: {
|
||||
description: "Routes for uploading and managing photos",
|
||||
}
|
||||
}).derive(async ({ cookie }) => {
|
||||
const authData = await verifyAuth(cookie);
|
||||
return { authData }; // Inject into context
|
||||
});
|
||||
|
||||
uploadRoutes.post("/add", async ({ body, authData }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const user_id: String | any = authData?.userId;
|
||||
const { id: project_id, file } = body;
|
||||
const response = await uploadPhoto(file, project_id, user_id, token);
|
||||
return response;
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
file: t.File(),
|
||||
id: t.String(),
|
||||
})
|
||||
});
|
||||
|
||||
uploadRoutes.delete("/delete", async ({ query, authData }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const { url } = query;
|
||||
const response = await deletePhoto(url, token);
|
||||
return response;
|
||||
}
|
||||
}, {
|
||||
query: t.Object({
|
||||
url: t.String(),
|
||||
})
|
||||
});
|
||||
|
||||
uploadRoutes.get("/getAll/:id", async ({ params: { id }, authData }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const response = await getAllPhoto(id, token);
|
||||
return response;
|
||||
}
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String()
|
||||
})
|
||||
});
|
||||
42
src/api/uploadShapes/upload.shapes.controller.ts
Normal file
42
src/api/uploadShapes/upload.shapes.controller.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db";
|
||||
import { shapes } from "../../db/schema";
|
||||
|
||||
export const uploadShapes = async (shape: string, token: string) => {
|
||||
// This function is responsible for uploading shapes to the server.
|
||||
try {
|
||||
const shapeUpload = await db.insert(shapes).values({ shapes: shape }).returning();
|
||||
return { status: 200, message: "Shape uploaded successfully", token, shapeUpload };
|
||||
} catch (error: any) {
|
||||
console.error("Error processing upload:", error);
|
||||
return { status: 500, message: "An error occurred while uploading the shape", token };
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteShape = async (shapeId: string, token: string) => {
|
||||
try {
|
||||
const shapeDelete = await db.delete(shapes).where(eq(shapes.id, shapeId)).returning();
|
||||
return { status: 200, message: "Shape deleted successfully", token, shapeDelete };
|
||||
} catch (error: any) {
|
||||
console.error("Error deleting shape:", error);
|
||||
return { status: 500, message: "An error occurred while deleting the shape", token };
|
||||
}
|
||||
};
|
||||
|
||||
export const getShapes = async (token: string) => {
|
||||
try {
|
||||
const allShapes = await db.select().from(shapes);
|
||||
|
||||
if (allShapes.length === 0) {
|
||||
return { status: 404, message: "No shapes found", token };
|
||||
}
|
||||
|
||||
else {
|
||||
return { status: 200, message: "Shapes retrieved successfully", token, allShapes };
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error getting shapes:", error);
|
||||
return { status: 500, message: "An error occurred while getting the shapes", token };
|
||||
}
|
||||
};
|
||||
53
src/api/uploadShapes/upload.shapes.route.ts
Normal file
53
src/api/uploadShapes/upload.shapes.route.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import Elysia, { t } from "elysia";
|
||||
import { verifyAuth } from "../../middlewares/auth.middlewares";
|
||||
import { deleteShape, getShapes, uploadShapes } from "./upload.shapes.controller";
|
||||
|
||||
export const uploadShapesRoutes = new Elysia({
|
||||
prefix: "/upload-shapes",
|
||||
tags: ["Shape uploads"],
|
||||
detail: {
|
||||
description: "Routes for uploading and managing shapes",
|
||||
}
|
||||
}).derive(async ({ cookie }) => {
|
||||
const authData = await verifyAuth(cookie);
|
||||
return { authData }; // Inject into context
|
||||
});
|
||||
|
||||
uploadShapesRoutes.post("/add", async ({ body, authData }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const { shape } = body;
|
||||
const response = await uploadShapes(shape, token);
|
||||
return response;
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
shape: t.String(),
|
||||
})
|
||||
});
|
||||
|
||||
uploadShapesRoutes.delete("/delete/:id", async ({ authData, params: { id } }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const response = await deleteShape(id, token);
|
||||
return response;
|
||||
}
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String()
|
||||
}),
|
||||
});
|
||||
|
||||
uploadShapesRoutes.get("/get", async ({ authData }) => {
|
||||
if (authData?.status !== 200)
|
||||
return authData;
|
||||
else {
|
||||
const token = authData?.token;
|
||||
const response = await getShapes(token);
|
||||
return response;
|
||||
}
|
||||
});
|
||||
59
src/app.ts
Normal file
59
src/app.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Elysia } from "elysia";
|
||||
import swagger from '@elysiajs/swagger';
|
||||
|
||||
import { ENV } from "./config/env";
|
||||
import cors from "@elysiajs/cors";
|
||||
import { api } from "./api";
|
||||
|
||||
const allowedOrigins = [
|
||||
"http://localhost:5175",
|
||||
"http://localhost:5173",
|
||||
// allowed canvas backend (user) origins(for user)
|
||||
"https://localhost:3001",
|
||||
];
|
||||
|
||||
const app = new Elysia({
|
||||
prefix: "",
|
||||
tags: ["Default"],
|
||||
})
|
||||
.use(cors({
|
||||
origin: allowedOrigins,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept", "Origin", "Access-Control-Allow-Origin"],
|
||||
credentials: true,
|
||||
}))
|
||||
.use(swagger({
|
||||
path: "/api/docs",
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Canvas API",
|
||||
version: "1.0.0",
|
||||
description: "Canvas API Documentation",
|
||||
},
|
||||
tags: [
|
||||
{
|
||||
name: "Projects",
|
||||
description: "All APIs related to Projects",
|
||||
},
|
||||
{
|
||||
name: "Uploads",
|
||||
description: "All APIs related to Uploads"
|
||||
}
|
||||
],
|
||||
}
|
||||
}))
|
||||
.onError(({ code, error }) => {
|
||||
if (code === 'NOT_FOUND')
|
||||
return 'Not Found :(';
|
||||
console.log("hello from app.ts under error");
|
||||
console.error(error)
|
||||
});
|
||||
|
||||
// all routes here
|
||||
app.use(api);
|
||||
|
||||
app.listen(ENV.SERVER_PORT, () => {
|
||||
console.log(`🦊 Elysia is running at ${ENV.SERVER_URL}:${ENV.SERVER_PORT}`)
|
||||
})
|
||||
|
||||
|
||||
22
src/config/env.ts
Normal file
22
src/config/env.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import 'dotenv/config'
|
||||
|
||||
export const ENV = {
|
||||
SERVER_URL: process.env.SERVER_URL,
|
||||
SERVER_PORT: process.env.SERVER_PORT || 5000,
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||
MINIO_ENDPOINT: process.env.MINIO_ENDPOINT,
|
||||
MINIO_PORT: process.env.MINIO_PORT,
|
||||
JWT_ACCESS_TOKEN_SECRET: process.env.JWT_ACCESS_TOKEN_SECRET,
|
||||
JWT_REFRESH_TOKEN_SECRET: process.env.JWT_REFRESH_TOKEN_SECRET,
|
||||
JWT_EMAIL_TOKEN_SECRET: process.env.JWT_EMAIL_TOKEN_SECRET,
|
||||
JWT_EMAIL_RESET_PASSWORD_SECRET: process.env.JWT_EMAIL_RESET_PASSWORD_SECRET,
|
||||
USER_CANVAS_JWT_ACCESS_TOKEN_SECRET: process.env.USER_CANVAS_JWT_ACCESS_TOKEN_SECRET,
|
||||
MAIL_HOST: process.env.MAIL_HOST,
|
||||
MAIL_PORT: process.env.MAIL_PORT,
|
||||
MAIL_USER: process.env.MAIL_USER,
|
||||
MAIL_PASS: process.env.MAIL_PASS,
|
||||
PEXELS_URL: process.env.PEXELS_URL,
|
||||
PEXELS_ACCESS_KEY: process.env.PEXELS_ACCESS_KEY,
|
||||
}
|
||||
10
src/config/minioClient.ts
Normal file
10
src/config/minioClient.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Client } from "minio";
|
||||
import { ENV } from "../config/env";
|
||||
|
||||
export const minioClient = new Client({
|
||||
endPoint: ENV.MINIO_ENDPOINT!,
|
||||
port: ENV.MINIO_PORT,
|
||||
useSSL: false,
|
||||
accessKey: ENV.MINIO_ACCESS_KEY,
|
||||
secretKey: ENV.MINIO_SECRET_KEY,
|
||||
})
|
||||
5
src/db/index.ts
Normal file
5
src/db/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
|
||||
import { ENV } from "../config/env";
|
||||
|
||||
export const db = drizzle(ENV.DATABASE_URL!);
|
||||
49
src/db/schema.ts
Normal file
49
src/db/schema.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { boolean, json, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
||||
|
||||
export const users = pgTable("users", {
|
||||
id: uuid("user_id").defaultRandom().primaryKey(),
|
||||
email: text("email").notNull(),
|
||||
name: text("name"),
|
||||
password: text("password").notNull(),
|
||||
is_active: boolean("is_active").notNull().default(true),
|
||||
is_verified: boolean("is_verified").notNull().default(false),
|
||||
is_admin: boolean("is_admin").notNull().default(false),
|
||||
refresh_token: text("refresh_token"),
|
||||
});
|
||||
|
||||
export const projects = pgTable("projects", {
|
||||
id: uuid("project_id").defaultRandom().primaryKey(),
|
||||
userId: uuid().references(() => users.id),
|
||||
category: uuid().references(() => category.id),
|
||||
object: json(),
|
||||
name: text("name"),
|
||||
description: text("description"),
|
||||
is_public: boolean("is_public").notNull().default(true),
|
||||
preview_url: text("preview_url"),
|
||||
created_at: timestamp("created_at").defaultNow(),
|
||||
updated_at: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const uploads = pgTable("uploads", {
|
||||
id: uuid("upload_id").defaultRandom().primaryKey(),
|
||||
filename: text("filename").notNull(),
|
||||
url: text("url").notNull(),
|
||||
projectId: uuid().references(() => projects.id),
|
||||
created_at: timestamp("created_at").defaultNow(),
|
||||
updated_at: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const shapes = pgTable("shapes", {
|
||||
id: uuid("shape_id").defaultRandom().primaryKey(),
|
||||
shapes: text("shapes").notNull(),
|
||||
created_at: timestamp("created_at").defaultNow(),
|
||||
updated_at: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const category = pgTable("project_category", {
|
||||
id: uuid("category_id").defaultRandom().primaryKey(),
|
||||
user_id: uuid().references(() => users.id),
|
||||
category: text("category").notNull(),
|
||||
created_at: timestamp("created_at").defaultNow(),
|
||||
updated_at: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
177
src/helper/auth/auth.helper.ts
Normal file
177
src/helper/auth/auth.helper.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db";
|
||||
import { users } from "../../db/schema";
|
||||
import { ENV } from "../../config/env";
|
||||
// @ts-ignore
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export const checkUserInDb = async (email: string, password: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
can_register?: boolean;
|
||||
can_login?: boolean;
|
||||
email?: string;
|
||||
}> => {
|
||||
try {
|
||||
// function isAdspillarEmail(email: string) {
|
||||
// const regex = /^[a-zA-Z0-9._%+-]+@adspillar\.com$/;
|
||||
// return regex.test(email);
|
||||
// }
|
||||
|
||||
// if (!isAdspillarEmail(email)) {
|
||||
// return { success: false, message: "Invalid email domain", can_register: false, can_login: false };
|
||||
// }
|
||||
|
||||
// else {
|
||||
// const findUser = await db.select({
|
||||
// email: users.email,
|
||||
// password: users.password,
|
||||
// is_active: users.is_active,
|
||||
// is_verified: users.is_verified,
|
||||
// refresh_token: users.refresh_token,
|
||||
// }).from(users).where(eq(users.email, email));
|
||||
|
||||
// if (!findUser[0]) {
|
||||
// return { success: true, message: "User not found", can_register: true };
|
||||
// }
|
||||
|
||||
// const hash = findUser[0].password;
|
||||
// const isMatch = await Bun.password.verify(password, hash);
|
||||
|
||||
// if (isMatch && findUser[0].is_verified && findUser[0].is_active) {
|
||||
// return {
|
||||
// success: true,
|
||||
// message: "User verified successfully",
|
||||
// can_login: true,
|
||||
// email: findUser[0].email // Ensure email is included
|
||||
// };
|
||||
// }
|
||||
// else if (isMatch && findUser[0].is_verified === false && findUser[0].is_active) {
|
||||
// return { success: false, message: "User not verified", can_login: false };
|
||||
// }
|
||||
// else if (isMatch && findUser[0].is_active === false && findUser[0].is_verified) {
|
||||
// return { success: false, message: "User not active", can_login: false };
|
||||
// }
|
||||
// else {
|
||||
// return { success: false, message: "Invalid password", can_login: false };
|
||||
// }
|
||||
// }
|
||||
|
||||
const findUser = await db.select({
|
||||
email: users.email,
|
||||
password: users.password,
|
||||
is_active: users.is_active,
|
||||
is_verified: users.is_verified,
|
||||
refresh_token: users.refresh_token,
|
||||
}).from(users).where(eq(users.email, email));
|
||||
|
||||
if (!findUser[0]) {
|
||||
return { success: true, message: "Wrong credentials", can_register: true };
|
||||
}
|
||||
|
||||
const hash = findUser[0].password;
|
||||
const isMatch = await Bun.password.verify(password, hash);
|
||||
|
||||
if (isMatch && findUser[0].is_verified && findUser[0].is_active) {
|
||||
return {
|
||||
success: true,
|
||||
message: "User verified successfully",
|
||||
can_login: true,
|
||||
email: findUser[0].email, // Ensure email is included
|
||||
can_register: false
|
||||
};
|
||||
}
|
||||
else if (isMatch && findUser[0].is_verified === false && findUser[0].is_active) {
|
||||
return { success: false, message: "User not verified", can_login: false, can_register: false };
|
||||
}
|
||||
else if (isMatch && findUser[0].is_active === false && findUser[0].is_verified) {
|
||||
return { success: false, message: "User not active", can_login: false, can_register: false };
|
||||
}
|
||||
else {
|
||||
return { success: false, message: "Invalid credentials", can_login: false, can_register: false };
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.log("Error verifying user:", error);
|
||||
return { success: false, message: "Error verifying user" };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const storeRefreshToken = async (email: string, refreshToken: string): Promise<{ success: boolean; message: string }> => {
|
||||
try {
|
||||
await db.update(users).set({ refresh_token: refreshToken }).where(eq(users.email, email));
|
||||
return { success: true, message: "Refresh token stored successfully" };
|
||||
} catch (error) {
|
||||
console.log("Error storing refresh token:", error);
|
||||
return { success: false, message: "Error storing refresh token" };
|
||||
}
|
||||
}
|
||||
|
||||
export const sendVerificationEmail = async (email: string, token: string, set: any) => {
|
||||
const sendEmail = async (email: string, token: string) => {
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: ENV.MAIL_HOST,
|
||||
port: ENV.MAIL_PORT,
|
||||
auth: {
|
||||
user: ENV.MAIL_USER,
|
||||
pass: ENV.MAIL_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
const url = `${ENV.SERVER_URL}:${ENV.SERVER_PORT}/api/auth/verify?token=${token}`;
|
||||
const mailOptions = {
|
||||
from: ENV.MAIL_USER,
|
||||
to: email,
|
||||
subject: 'Verify Your Email Address',
|
||||
html: `<p>Please verify your email by clicking the following link:</p>
|
||||
<p><a href="${url}">Verify email</a></p>
|
||||
<p>This link will be valid for the next 10 minutes.</p>`,
|
||||
};
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
return { status: 200, message: "Verification email sent, link will valid till next 10 minutes" };
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
return { status: 500, message: "Internal server error, unable to send email" };
|
||||
}
|
||||
};
|
||||
const emailResponse = await sendEmail(email, token);
|
||||
set.status = emailResponse.status;
|
||||
return emailResponse;
|
||||
}
|
||||
|
||||
export const sendResetPasswordEmail = async (email: string, token: string, set: any) => {
|
||||
const sendEmail = async (email: string, token: string) => {
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: ENV.MAIL_HOST,
|
||||
port: ENV.MAIL_PORT,
|
||||
auth: {
|
||||
user: ENV.MAIL_USER,
|
||||
pass: ENV.MAIL_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
const url = `${ENV.SERVER_URL}:${ENV.SERVER_PORT}/api/auth/reset-password?token=${token}`;
|
||||
const mailOptions = {
|
||||
from: ENV.MAIL_USER,
|
||||
to: email,
|
||||
subject: 'Reset Your Password',
|
||||
html: `<p>Please reset your password by clicking the following link:</p>
|
||||
<p><a href="${url}">Reset password</a></p>
|
||||
<p>This link will be valid for the next 10 minutes.</p>`,
|
||||
};
|
||||
await transporter.sendMail(mailOptions);
|
||||
return { status: 200, message: "Reset password email sent, link will valid till next 10 minutes" };
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
return { status: 500, message: "Internal server error, unable to send email" };
|
||||
}
|
||||
};
|
||||
const emailResponse = await sendEmail(email, token);
|
||||
set.status = emailResponse.status;
|
||||
return emailResponse;
|
||||
}
|
||||
|
||||
24
src/helper/projects/createProject.ts
Normal file
24
src/helper/projects/createProject.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { db } from "../../db";
|
||||
import { projects } from "../../db/schema";
|
||||
|
||||
export const createEmptyProject = async (userId: string): Promise<{ id: string }> => {
|
||||
try {
|
||||
// Insert a new row with default values
|
||||
const [newProject] = await db
|
||||
.insert(projects)
|
||||
.values({
|
||||
userId: userId,
|
||||
object: {}, // Empty object as default
|
||||
name: "", // Empty name
|
||||
description: "", // Empty description
|
||||
preview_url: "", // Empty preview URL
|
||||
is_public: true, // Add default value for is_public
|
||||
})
|
||||
.returning({ id: projects.id }); // Returning the ID of the created project
|
||||
// Return the newly created project's ID
|
||||
return { id: newProject.id };
|
||||
} catch (error) {
|
||||
console.error("Error creating an empty project:", error);
|
||||
throw new Error("Failed to create an empty project");
|
||||
}
|
||||
};
|
||||
34
src/helper/upload/createBucket.ts
Normal file
34
src/helper/upload/createBucket.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { minioClient } from "../../config/minioClient";
|
||||
|
||||
export const createBucket = async (bucketName: string) => {
|
||||
try {
|
||||
const bucketExists = await minioClient.bucketExists(bucketName);
|
||||
if (!bucketExists) {
|
||||
// Create the bucket
|
||||
await minioClient.makeBucket(bucketName, "us-east-1");
|
||||
|
||||
// Set the bucket policy to make it public
|
||||
const bucketPolicy = JSON.stringify({
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: "*",
|
||||
Action: ["s3:GetObject"],
|
||||
Resource: [`arn:aws:s3:::${bucketName}/*`],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await minioClient.setBucketPolicy(bucketName, bucketPolicy);
|
||||
|
||||
return bucketName; // Return the bucket name if created successfully
|
||||
} else {
|
||||
return bucketName; // Return the bucket name if it already exists
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error creating or configuring bucket:", error);
|
||||
// Optionally rethrow the error with additional context
|
||||
throw new Error(`Error creating bucket "${bucketName}": ${error.message}`);
|
||||
}
|
||||
};
|
||||
30
src/helper/upload/removeBucket.ts
Normal file
30
src/helper/upload/removeBucket.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { minioClient } from "../../config/minioClient";
|
||||
|
||||
export const removeBucket = async (bucketName: string) => {
|
||||
try {
|
||||
// Check if the bucket exists before proceeding
|
||||
const bucketExists = await minioClient.bucketExists(bucketName);
|
||||
if (!bucketExists) {
|
||||
return { status: 404, message: `Bucket ${bucketName} does not exist` };
|
||||
}
|
||||
|
||||
// List objects in the bucket, which returns a stream
|
||||
const objects = minioClient.listObjects(bucketName);
|
||||
|
||||
// Iterate over the stream of objects using 'for await...of'
|
||||
for await (const obj of objects) {
|
||||
await minioClient.removeObject(bucketName, obj.name);
|
||||
console.log(`Removed object: ${obj.name}`);
|
||||
}
|
||||
|
||||
// Now remove the bucket after clearing all objects
|
||||
await minioClient.removeBucket(bucketName);
|
||||
|
||||
return { status: 200, message: `Bucket ${bucketName} and its data removed successfully` };
|
||||
} catch (error: any) {
|
||||
console.log(`Error removing bucket ${bucketName}: ${error.message}`);
|
||||
return { status: 500, message: `Error removing bucket ${bucketName}: ${error.message}` };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
16
src/helper/upload/removeFromMinio.ts
Normal file
16
src/helper/upload/removeFromMinio.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { minioClient } from "../../config/minioClient";
|
||||
|
||||
interface RemoveFromMinioResponse {
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export const removeFromMinio = async (bucketName: string, objectName: string): Promise<RemoveFromMinioResponse> => {
|
||||
try {
|
||||
// Remove the object from MinIO
|
||||
await minioClient.removeObject(bucketName, objectName);
|
||||
return { msg: `Successfully removed ${objectName}` };
|
||||
} catch (error: any) {
|
||||
console.error("Error removing object from MinIO:", error);
|
||||
throw new Error(`Failed to remove ${objectName} from bucket ${bucketName}: ${error.message}`);
|
||||
}
|
||||
};
|
||||
18
src/helper/upload/uploadToMinio.ts
Normal file
18
src/helper/upload/uploadToMinio.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { minioClient } from "../../config/minioClient";
|
||||
|
||||
export const uploadToMinio = async (file: File, bucketName: string, objectName: string) => {
|
||||
const buffer = Buffer.from(await file.arrayBuffer()); // Convert file to buffer
|
||||
try {
|
||||
// Ensure the file is uploaded to MinIO
|
||||
await minioClient.putObject(bucketName, objectName, buffer);
|
||||
|
||||
// Construct the public URL to access the uploaded file
|
||||
const publicUrl = `${minioClient.protocol}//${minioClient.host}:${minioClient.port}/${bucketName}/${objectName}`;
|
||||
|
||||
return { url: publicUrl };
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error uploading file to MinIO:", error);
|
||||
throw new Error(`Error uploading file: ${error.message}`);
|
||||
}
|
||||
};
|
||||
71
src/html/alreadyVerify.html
Normal file
71
src/html/alreadyVerify.html
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Already Verified</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f5f0ff;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #28a745;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 15px 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
background: #ff3366;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #cc284f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<h1>✅ Verified User!</h1>
|
||||
<p>You are already verified. Please log in to continue.</p>
|
||||
<a href="http://localhost:5175/login" class="btn">Log In</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
70
src/html/error.html
Normal file
70
src/html/error.html
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Verification Failed</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f5f0ff;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: red;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 15px 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
background: #ff3366;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #cc284f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>❌ Verification Failed!</h1>
|
||||
<p>Something went wrong. Please try again later or contact support.</p>
|
||||
<a href="http://localhost:5175/login" class="btn">Go Back</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
129
src/html/resetPassword.html
Normal file
129
src/html/resetPassword.html
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reset Password</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f5f0ff;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
||||
width: 350px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #ff6a88;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #ff3366;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #ff3366;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #cc284f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<h2>Reset Your Password</h2>
|
||||
<hr><br>
|
||||
<form id="reset-form">
|
||||
<div class="input-group">
|
||||
<input type="password" id="password" placeholder="Enter new password" required />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="password" id="confirm-password" placeholder="Confirm new password" required />
|
||||
<p class="error" id="error-message">Passwords do not match!</p>
|
||||
</div>
|
||||
<button type="submit">Reset Password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById("reset-form").addEventListener("submit", async function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
const password = document.getElementById("password").value;
|
||||
const confirmPassword = document.getElementById("confirm-password").value;
|
||||
const errorMessage = document.getElementById("error-message");
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
errorMessage.style.display = "block";
|
||||
return;
|
||||
} else {
|
||||
errorMessage.style.display = "none";
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get("token");
|
||||
|
||||
const response = await fetch(`http://localhost:3000/api/auth/reset-password?token=${token}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
const data = await response.json();
|
||||
window.location.href="http://localhost:5175/login";
|
||||
alert(data.message);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
69
src/html/success.html
Normal file
69
src/html/success.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Verification Successful</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f5f0ff;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #28a745;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 15px 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
background: #ff3366;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #cc284f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>✅ Verification Successful!</h1>
|
||||
<p>Your email has been verified. You can now log in.</p>
|
||||
<a href="http://localhost:5175/login" class="btn">Log In</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
76
src/middlewares/auth.middlewares.ts
Normal file
76
src/middlewares/auth.middlewares.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { ENV } from "../config/env";
|
||||
// @ts-ignore
|
||||
import jwt from "jsonwebtoken";
|
||||
import { users } from "../db/schema";
|
||||
import { db } from "../db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const verifyAuth = async (cookie: any) => {
|
||||
const accessToken = cookie?.access_token?.value;
|
||||
const refreshToken = cookie?.refresh_token?.value;
|
||||
|
||||
if (accessToken !== undefined) {
|
||||
try {
|
||||
// Verify and decode access token
|
||||
const decoded = jwt.verify(accessToken, ENV.JWT_ACCESS_TOKEN_SECRET) as { email: string };
|
||||
|
||||
if (!decoded) {
|
||||
return { status: 401, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
// Find user in the database
|
||||
const findUser = await db.select().from(users).where(eq(users.email, decoded.email));
|
||||
if (!findUser.length) {
|
||||
return { status: 401, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
return { status: 200, message: "Token verified", token: accessToken, userId: findUser[0].id };
|
||||
} catch (error) {
|
||||
// Handle verification errors
|
||||
console.error("Access token verification error:", error);
|
||||
// Continue to refresh token logic
|
||||
}
|
||||
}
|
||||
|
||||
// If access token is missing or invalid, verify refresh token
|
||||
else if (accessToken === undefined && refreshToken !== undefined) {
|
||||
try {
|
||||
const decoded = jwt.verify(refreshToken, ENV.JWT_REFRESH_TOKEN_SECRET) as { email: string };
|
||||
|
||||
if (!decoded) {
|
||||
return { status: 401, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
// Find user and check refresh token
|
||||
const findUser = await db.select().from(users).where(eq(users.email, decoded.email));
|
||||
if (!findUser.length || findUser[0].refresh_token !== refreshToken) {
|
||||
return { status: 401, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
// Generate a new access token - only include userId as requested
|
||||
const newAccessToken = jwt.sign(
|
||||
{ email: findUser[0].email },
|
||||
ENV.JWT_ACCESS_TOKEN_SECRET,
|
||||
{ expiresIn: "3h" }
|
||||
);
|
||||
|
||||
// Update access token in cookies
|
||||
cookie.access_token.set({
|
||||
value: newAccessToken,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "none",
|
||||
path: "/",
|
||||
maxAge: 3 * 60 * 60, // 3 hours in seconds
|
||||
});
|
||||
|
||||
return { status: 200, message: "Token refreshed", token: newAccessToken, userId: findUser[0].id };
|
||||
} catch (error) {
|
||||
console.error("Refresh token verification error:", error);
|
||||
return { status: 401, message: "Unauthorized" };
|
||||
}
|
||||
}
|
||||
else {
|
||||
return { status: 401, message: "Unauthorized - No valid tokens" };
|
||||
}
|
||||
};
|
||||
103
tsconfig.json
Normal file
103
tsconfig.json
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "ES2022", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue