all requirement added

This commit is contained in:
Saimon8420 2025-03-01 14:13:26 +06:00
parent f3cc1147e2
commit d304e6e69d
42 changed files with 2638 additions and 0 deletions

29
.env Normal file
View 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
View 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

Binary file not shown.

11
drizzle.config.ts Normal file
View 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!,
},
});

View 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;

View 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": {}
}
}

View 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
View 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
View 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
View 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"
}

View 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
View 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(),
})
});

View 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 };
}
}

View 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(),
})
})

View 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" };
}
};

View 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
View 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);

View 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 };
}
}

View 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(),
})
})

View 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 };
}
};

View 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()
})
});

View 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 }
}
}

View 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()
})
});

View 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 };
}
};

View 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
View 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
View 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
View 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
View 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
View 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(),
});

View 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;
}

View 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");
}
};

View 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}`);
}
};

View 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}` };
}
};

View 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}`);
}
};

View 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}`);
}
};

View 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
View 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
View 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
View 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>

View 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
View 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. */
}
}