Spaces:
Running
Running
milestone2 commit
Browse files- .dockerignore +14 -0
- .gitignore +6 -0
- Dockerfile +41 -0
- ormconfig.ts +19 -0
- package-lock.json +0 -0
- package.json +42 -0
- src/entity/User.ts +24 -0
- src/entity/WardrobeItem.ts +28 -0
- src/index.ts +34 -0
- src/middleware/auth.ts +35 -0
- src/migrations/1700000000000-InitWardrobe.ts +37 -0
- src/routes/auth.ts +129 -0
- src/routes/profile.ts +44 -0
- src/routes/suggest.ts +44 -0
- src/routes/upload.ts +46 -0
- src/scripts/run-migrations.ts +24 -0
- src/types/index.d.ts +40 -0
- src/utils/cloudinary.ts +12 -0
- src/utils/dataSource.ts +26 -0
- src/utils/hfClient.ts +25 -0
- start.sh +16 -0
- tsconfig.json +21 -0
.dockerignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
dist
|
| 3 |
+
.env
|
| 4 |
+
.env.local
|
| 5 |
+
.git
|
| 6 |
+
.gitignore
|
| 7 |
+
*.md
|
| 8 |
+
.vscode
|
| 9 |
+
.idea
|
| 10 |
+
*.log
|
| 11 |
+
npm-debug.log*
|
| 12 |
+
yarn-debug.log*
|
| 13 |
+
yarn-error.log*
|
| 14 |
+
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
| 3 |
+
.env
|
| 4 |
+
*.log
|
| 5 |
+
.DS_Store
|
| 6 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Base Image
|
| 2 |
+
FROM node:20-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Set NODE_ENV for production
|
| 8 |
+
ENV NODE_ENV=production
|
| 9 |
+
|
| 10 |
+
# Install system dependencies (needed for some npm packages)
|
| 11 |
+
RUN apt-get update && apt-get install -y \
|
| 12 |
+
python3 \
|
| 13 |
+
make \
|
| 14 |
+
g++ \
|
| 15 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
# Copy package files
|
| 18 |
+
COPY package.json package-lock.json ./
|
| 19 |
+
|
| 20 |
+
# Install all dependencies (needed for build)
|
| 21 |
+
RUN npm ci && npm cache clean --force
|
| 22 |
+
|
| 23 |
+
# Copy source code
|
| 24 |
+
COPY . .
|
| 25 |
+
|
| 26 |
+
# Build TypeScript (migrations will be compiled to dist/migrations/*.js)
|
| 27 |
+
RUN npm run build
|
| 28 |
+
|
| 29 |
+
# Remove dev dependencies after build
|
| 30 |
+
RUN npm prune --production
|
| 31 |
+
|
| 32 |
+
# Copy and setup startup script
|
| 33 |
+
COPY start.sh ./start.sh
|
| 34 |
+
RUN chmod +x ./start.sh
|
| 35 |
+
|
| 36 |
+
# Expose port (Hugging Face Spaces uses 7860)
|
| 37 |
+
EXPOSE 7860
|
| 38 |
+
|
| 39 |
+
# Use the startup script
|
| 40 |
+
CMD ["./start.sh"]
|
| 41 |
+
|
ormconfig.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { DataSourceOptions } from "typeorm";
|
| 2 |
+
import dotenv from "dotenv";
|
| 3 |
+
dotenv.config();
|
| 4 |
+
|
| 5 |
+
export const config: DataSourceOptions = {
|
| 6 |
+
type: "postgres",
|
| 7 |
+
url: process.env.DATABASE_URL,
|
| 8 |
+
synchronize: false,
|
| 9 |
+
logging: true,
|
| 10 |
+
entities: ["src/entity/**/*.ts"],
|
| 11 |
+
migrations: ["src/migrations/*.ts"],
|
| 12 |
+
subscribers: [],
|
| 13 |
+
extra: {
|
| 14 |
+
ssl: process.env.DATABASE_URL?.includes("render.com") ? {
|
| 15 |
+
rejectUnauthorized: false,
|
| 16 |
+
} : false,
|
| 17 |
+
},
|
| 18 |
+
};
|
| 19 |
+
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "stylegpt-milestone2",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "--- title: StyleGPT Milestone2 emoji: 🌍 colorFrom: purple colorTo: red sdk: docker pinned: false ---",
|
| 5 |
+
"main": "index.ts",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "nodemon --exec ts-node src/index.ts",
|
| 8 |
+
"build": "tsc",
|
| 9 |
+
"start": "node dist/index.js",
|
| 10 |
+
"migration:run": "ts-node src/scripts/run-migrations.ts",
|
| 11 |
+
"migration:generate": "typeorm-ts-node-commonjs migration:generate -d src/utils/dataSource.ts",
|
| 12 |
+
"migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/utils/dataSource.ts"
|
| 13 |
+
},
|
| 14 |
+
"repository": {
|
| 15 |
+
"type": "git",
|
| 16 |
+
"url": "https://huggingface.co/spaces/nexusbert/StyleGPT-milestone2"
|
| 17 |
+
},
|
| 18 |
+
"author": "",
|
| 19 |
+
"license": "ISC",
|
| 20 |
+
"dependencies": {
|
| 21 |
+
"axios": "^1.13.1",
|
| 22 |
+
"bcrypt": "^5.1.1",
|
| 23 |
+
"cloudinary": "^2.8.0",
|
| 24 |
+
"cors": "^2.8.5",
|
| 25 |
+
"dotenv": "^17.2.3",
|
| 26 |
+
"express": "^5.1.0",
|
| 27 |
+
"jsonwebtoken": "^9.0.2",
|
| 28 |
+
"pg": "^8.16.3",
|
| 29 |
+
"reflect-metadata": "^0.2.2",
|
| 30 |
+
"typeorm": "^0.3.27"
|
| 31 |
+
},
|
| 32 |
+
"devDependencies": {
|
| 33 |
+
"@types/bcrypt": "^5.0.2",
|
| 34 |
+
"@types/cors": "^2.8.19",
|
| 35 |
+
"@types/express": "^5.0.5",
|
| 36 |
+
"@types/jsonwebtoken": "^9.0.10",
|
| 37 |
+
"@types/node": "^24.9.2",
|
| 38 |
+
"nodemon": "^3.1.10",
|
| 39 |
+
"ts-node": "^10.9.2",
|
| 40 |
+
"typescript": "^5.9.3"
|
| 41 |
+
}
|
| 42 |
+
}
|
src/entity/User.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from "typeorm";
|
| 2 |
+
import { WardrobeItem } from "./WardrobeItem";
|
| 3 |
+
|
| 4 |
+
@Entity()
|
| 5 |
+
export class User {
|
| 6 |
+
@PrimaryGeneratedColumn()
|
| 7 |
+
id!: number;
|
| 8 |
+
|
| 9 |
+
@Column({ unique: true })
|
| 10 |
+
email!: string;
|
| 11 |
+
|
| 12 |
+
@Column()
|
| 13 |
+
name!: string;
|
| 14 |
+
|
| 15 |
+
@Column()
|
| 16 |
+
password!: string;
|
| 17 |
+
|
| 18 |
+
@OneToMany(() => WardrobeItem, (wardrobeItem) => wardrobeItem.user)
|
| 19 |
+
wardrobeItems!: WardrobeItem[];
|
| 20 |
+
|
| 21 |
+
@CreateDateColumn()
|
| 22 |
+
createdAt!: Date;
|
| 23 |
+
}
|
| 24 |
+
|
src/entity/WardrobeItem.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from "typeorm";
|
| 2 |
+
import { User } from "./User";
|
| 3 |
+
|
| 4 |
+
@Entity()
|
| 5 |
+
export class WardrobeItem {
|
| 6 |
+
@PrimaryGeneratedColumn()
|
| 7 |
+
id!: number;
|
| 8 |
+
|
| 9 |
+
@Column()
|
| 10 |
+
imageUrl!: string;
|
| 11 |
+
|
| 12 |
+
@Column()
|
| 13 |
+
category!: string;
|
| 14 |
+
|
| 15 |
+
@Column()
|
| 16 |
+
style!: string;
|
| 17 |
+
|
| 18 |
+
@ManyToOne(() => User, (user) => user.wardrobeItems)
|
| 19 |
+
@JoinColumn({ name: "userId" })
|
| 20 |
+
user!: User;
|
| 21 |
+
|
| 22 |
+
@Column()
|
| 23 |
+
userId!: number;
|
| 24 |
+
|
| 25 |
+
@CreateDateColumn()
|
| 26 |
+
createdAt!: Date;
|
| 27 |
+
}
|
| 28 |
+
|
src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import "reflect-metadata";
|
| 2 |
+
import express from "express";
|
| 3 |
+
import cors from "cors";
|
| 4 |
+
import dotenv from "dotenv";
|
| 5 |
+
import { AppDataSource } from "./utils/dataSource";
|
| 6 |
+
import authRoute from "./routes/auth";
|
| 7 |
+
import uploadRoute from "./routes/upload";
|
| 8 |
+
import suggestRoute from "./routes/suggest";
|
| 9 |
+
import profileRoute from "./routes/profile";
|
| 10 |
+
|
| 11 |
+
dotenv.config();
|
| 12 |
+
|
| 13 |
+
const app = express();
|
| 14 |
+
app.use(cors());
|
| 15 |
+
app.use(express.json({ limit: "10mb" }));
|
| 16 |
+
|
| 17 |
+
app.use("/api/auth", authRoute);
|
| 18 |
+
app.use("/api/profile", profileRoute);
|
| 19 |
+
app.use("/api/upload", uploadRoute);
|
| 20 |
+
app.use("/api/suggest", suggestRoute);
|
| 21 |
+
|
| 22 |
+
app.get("/health", (req, res) => {
|
| 23 |
+
res.json({ status: "healthy", database: AppDataSource.isInitialized });
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
const PORT = process.env.PORT || 7860;
|
| 27 |
+
|
| 28 |
+
AppDataSource.initialize()
|
| 29 |
+
.then(() => {
|
| 30 |
+
console.log("✅ Database connected");
|
| 31 |
+
app.listen(PORT, () => console.log(`🧥 StyleGPT running on port ${PORT}`));
|
| 32 |
+
})
|
| 33 |
+
.catch((error) => console.error("❌ DB connection failed:", error));
|
| 34 |
+
|
src/middleware/auth.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response, NextFunction } from "express";
|
| 2 |
+
import jwt from "jsonwebtoken";
|
| 3 |
+
import dotenv from "dotenv";
|
| 4 |
+
dotenv.config();
|
| 5 |
+
|
| 6 |
+
export interface AuthRequest extends Request {
|
| 7 |
+
userId?: number;
|
| 8 |
+
userEmail?: string;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export const authenticateToken = (
|
| 12 |
+
req: AuthRequest,
|
| 13 |
+
res: Response,
|
| 14 |
+
next: NextFunction
|
| 15 |
+
) => {
|
| 16 |
+
const authHeader = req.headers["authorization"];
|
| 17 |
+
const token = authHeader && authHeader.split(" ")[1];
|
| 18 |
+
|
| 19 |
+
if (!token) {
|
| 20 |
+
return res.status(401).json({ success: false, error: "No token provided" });
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const jwtSecret = process.env.JWT_SECRET || "your-secret-key-change-in-production";
|
| 24 |
+
|
| 25 |
+
jwt.verify(token, jwtSecret, (err: any, decoded: any) => {
|
| 26 |
+
if (err) {
|
| 27 |
+
return res.status(403).json({ success: false, error: "Invalid or expired token" });
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
req.userId = decoded.userId;
|
| 31 |
+
req.userEmail = decoded.email;
|
| 32 |
+
next();
|
| 33 |
+
});
|
| 34 |
+
};
|
| 35 |
+
|
src/migrations/1700000000000-InitWardrobe.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
| 2 |
+
|
| 3 |
+
export class InitWardrobe1700000000000 implements MigrationInterface {
|
| 4 |
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
| 5 |
+
await queryRunner.query(`
|
| 6 |
+
CREATE TABLE IF NOT EXISTS "user" (
|
| 7 |
+
"id" SERIAL NOT NULL PRIMARY KEY,
|
| 8 |
+
"email" VARCHAR NOT NULL UNIQUE,
|
| 9 |
+
"name" VARCHAR NOT NULL,
|
| 10 |
+
"password" VARCHAR NOT NULL,
|
| 11 |
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now()
|
| 12 |
+
)
|
| 13 |
+
`);
|
| 14 |
+
|
| 15 |
+
await queryRunner.query(`
|
| 16 |
+
CREATE TABLE IF NOT EXISTS "wardrobe_item" (
|
| 17 |
+
"id" SERIAL NOT NULL PRIMARY KEY,
|
| 18 |
+
"imageUrl" VARCHAR NOT NULL,
|
| 19 |
+
"category" VARCHAR NOT NULL,
|
| 20 |
+
"style" VARCHAR NOT NULL,
|
| 21 |
+
"userId" INTEGER NOT NULL,
|
| 22 |
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
| 23 |
+
CONSTRAINT "FK_wardrobe_item_user" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE
|
| 24 |
+
)
|
| 25 |
+
`);
|
| 26 |
+
|
| 27 |
+
await queryRunner.query(`
|
| 28 |
+
CREATE INDEX IF NOT EXISTS "IDX_wardrobe_item_userId" ON "wardrobe_item" ("userId")
|
| 29 |
+
`);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
| 33 |
+
await queryRunner.query(`DROP TABLE IF EXISTS "wardrobe_item"`);
|
| 34 |
+
await queryRunner.query(`DROP TABLE IF EXISTS "user"`);
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
src/routes/auth.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from "express";
|
| 2 |
+
import bcrypt from "bcrypt";
|
| 3 |
+
import jwt from "jsonwebtoken";
|
| 4 |
+
import { AppDataSource } from "../utils/dataSource";
|
| 5 |
+
import { User } from "../entity/User";
|
| 6 |
+
import dotenv from "dotenv";
|
| 7 |
+
dotenv.config();
|
| 8 |
+
|
| 9 |
+
const router = express.Router();
|
| 10 |
+
|
| 11 |
+
router.post("/register", async (req, res) => {
|
| 12 |
+
try {
|
| 13 |
+
const { name, email, password } = req.body;
|
| 14 |
+
|
| 15 |
+
if (!name || !email || !password) {
|
| 16 |
+
return res.status(400).json({
|
| 17 |
+
success: false,
|
| 18 |
+
error: "Name, email, and password are required",
|
| 19 |
+
});
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
if (password.length < 6) {
|
| 23 |
+
return res.status(400).json({
|
| 24 |
+
success: false,
|
| 25 |
+
error: "Password must be at least 6 characters",
|
| 26 |
+
});
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const userRepo = AppDataSource.getRepository(User);
|
| 30 |
+
|
| 31 |
+
const existingUser = await userRepo.findOne({ where: { email } });
|
| 32 |
+
if (existingUser) {
|
| 33 |
+
return res.status(400).json({
|
| 34 |
+
success: false,
|
| 35 |
+
error: "Email already registered",
|
| 36 |
+
});
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
const hashedPassword = await bcrypt.hash(password, 10);
|
| 40 |
+
|
| 41 |
+
const newUser = userRepo.create({
|
| 42 |
+
name,
|
| 43 |
+
email,
|
| 44 |
+
password: hashedPassword,
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
await userRepo.save(newUser);
|
| 48 |
+
|
| 49 |
+
const jwtSecret = process.env.JWT_SECRET || "your-secret-key-change-in-production";
|
| 50 |
+
const token = jwt.sign(
|
| 51 |
+
{ userId: newUser.id, email: newUser.email },
|
| 52 |
+
jwtSecret,
|
| 53 |
+
{ expiresIn: "7d" }
|
| 54 |
+
);
|
| 55 |
+
|
| 56 |
+
res.status(201).json({
|
| 57 |
+
success: true,
|
| 58 |
+
user: {
|
| 59 |
+
id: newUser.id,
|
| 60 |
+
name: newUser.name,
|
| 61 |
+
email: newUser.email,
|
| 62 |
+
},
|
| 63 |
+
token,
|
| 64 |
+
});
|
| 65 |
+
} catch (error: any) {
|
| 66 |
+
console.error("Registration error:", error);
|
| 67 |
+
res.status(500).json({
|
| 68 |
+
success: false,
|
| 69 |
+
error: error.message || "Registration failed",
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
router.post("/login", async (req, res) => {
|
| 75 |
+
try {
|
| 76 |
+
const { email, password } = req.body;
|
| 77 |
+
|
| 78 |
+
if (!email || !password) {
|
| 79 |
+
return res.status(400).json({
|
| 80 |
+
success: false,
|
| 81 |
+
error: "Email and password are required",
|
| 82 |
+
});
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
const userRepo = AppDataSource.getRepository(User);
|
| 86 |
+
const user = await userRepo.findOne({ where: { email } });
|
| 87 |
+
|
| 88 |
+
if (!user) {
|
| 89 |
+
return res.status(401).json({
|
| 90 |
+
success: false,
|
| 91 |
+
error: "Invalid email or password",
|
| 92 |
+
});
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
const isPasswordValid = await bcrypt.compare(password, user.password);
|
| 96 |
+
if (!isPasswordValid) {
|
| 97 |
+
return res.status(401).json({
|
| 98 |
+
success: false,
|
| 99 |
+
error: "Invalid email or password",
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
const jwtSecret = process.env.JWT_SECRET || "your-secret-key-change-in-production";
|
| 104 |
+
const token = jwt.sign(
|
| 105 |
+
{ userId: user.id, email: user.email },
|
| 106 |
+
jwtSecret,
|
| 107 |
+
{ expiresIn: "7d" }
|
| 108 |
+
);
|
| 109 |
+
|
| 110 |
+
res.json({
|
| 111 |
+
success: true,
|
| 112 |
+
user: {
|
| 113 |
+
id: user.id,
|
| 114 |
+
name: user.name,
|
| 115 |
+
email: user.email,
|
| 116 |
+
},
|
| 117 |
+
token,
|
| 118 |
+
});
|
| 119 |
+
} catch (error: any) {
|
| 120 |
+
console.error("Login error:", error);
|
| 121 |
+
res.status(500).json({
|
| 122 |
+
success: false,
|
| 123 |
+
error: error.message || "Login failed",
|
| 124 |
+
});
|
| 125 |
+
}
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
export default router;
|
| 129 |
+
|
src/routes/profile.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from "express";
|
| 2 |
+
import { AppDataSource } from "../utils/dataSource";
|
| 3 |
+
import { User } from "../entity/User";
|
| 4 |
+
import { authenticateToken, AuthRequest } from "../middleware/auth";
|
| 5 |
+
|
| 6 |
+
const router = express.Router();
|
| 7 |
+
|
| 8 |
+
router.get("/", authenticateToken, async (req: AuthRequest, res) => {
|
| 9 |
+
try {
|
| 10 |
+
const userId = req.userId!;
|
| 11 |
+
|
| 12 |
+
const userRepo = AppDataSource.getRepository(User);
|
| 13 |
+
const user = await userRepo.findOne({
|
| 14 |
+
where: { id: userId },
|
| 15 |
+
select: ["id", "name", "email", "createdAt"],
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
if (!user) {
|
| 19 |
+
return res.status(404).json({
|
| 20 |
+
success: false,
|
| 21 |
+
error: "User not found",
|
| 22 |
+
});
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
res.json({
|
| 26 |
+
success: true,
|
| 27 |
+
user: {
|
| 28 |
+
id: user.id,
|
| 29 |
+
name: user.name,
|
| 30 |
+
email: user.email,
|
| 31 |
+
createdAt: user.createdAt,
|
| 32 |
+
},
|
| 33 |
+
});
|
| 34 |
+
} catch (error: any) {
|
| 35 |
+
console.error("Profile error:", error);
|
| 36 |
+
res.status(500).json({
|
| 37 |
+
success: false,
|
| 38 |
+
error: error.message || "Failed to fetch profile",
|
| 39 |
+
});
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
export default router;
|
| 44 |
+
|
src/routes/suggest.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from "express";
|
| 2 |
+
import axios from "axios";
|
| 3 |
+
import { AppDataSource } from "../utils/dataSource";
|
| 4 |
+
import { WardrobeItem } from "../entity/WardrobeItem";
|
| 5 |
+
import { authenticateToken, AuthRequest } from "../middleware/auth";
|
| 6 |
+
import dotenv from "dotenv";
|
| 7 |
+
dotenv.config();
|
| 8 |
+
|
| 9 |
+
const router = express.Router();
|
| 10 |
+
|
| 11 |
+
router.post("/", authenticateToken, async (req: AuthRequest, res) => {
|
| 12 |
+
try {
|
| 13 |
+
const { message, session_id } = req.body;
|
| 14 |
+
const userId = req.userId!;
|
| 15 |
+
|
| 16 |
+
if (!message) {
|
| 17 |
+
return res.status(400).json({ success: false, error: "message is required" });
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Get only the authenticated user's wardrobe
|
| 21 |
+
const itemRepo = AppDataSource.getRepository(WardrobeItem);
|
| 22 |
+
const wardrobe = await itemRepo.find({ where: { userId } });
|
| 23 |
+
|
| 24 |
+
const prompt = `
|
| 25 |
+
User Wardrobe: ${wardrobe.length > 0 ? wardrobe.map(w => `${w.category} (${w.style})`).join(", ") : "No items in wardrobe yet"}
|
| 26 |
+
User request: ${message}
|
| 27 |
+
Suggest a stylish outfit combination from the wardrobe.
|
| 28 |
+
`;
|
| 29 |
+
|
| 30 |
+
const response = await axios.post(
|
| 31 |
+
process.env.CHAT_ENDPOINT!,
|
| 32 |
+
{ message: prompt, session_id: session_id || `user_${userId}` },
|
| 33 |
+
{ headers: { "Content-Type": "application/json" } }
|
| 34 |
+
);
|
| 35 |
+
|
| 36 |
+
res.json({ success: true, suggestion: response.data });
|
| 37 |
+
} catch (error: any) {
|
| 38 |
+
console.error("Suggest error:", error);
|
| 39 |
+
res.status(500).json({ success: false, error: error.message || "Suggestion failed" });
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
export default router;
|
| 44 |
+
|
src/routes/upload.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from "express";
|
| 2 |
+
import cloudinary from "../utils/cloudinary";
|
| 3 |
+
import { classifyFashionImage } from "../utils/hfClient";
|
| 4 |
+
import { AppDataSource } from "../utils/dataSource";
|
| 5 |
+
import { WardrobeItem } from "../entity/WardrobeItem";
|
| 6 |
+
import { authenticateToken, AuthRequest } from "../middleware/auth";
|
| 7 |
+
|
| 8 |
+
const router = express.Router();
|
| 9 |
+
|
| 10 |
+
router.post("/", authenticateToken, async (req: AuthRequest, res) => {
|
| 11 |
+
try {
|
| 12 |
+
const { imageUrl } = req.body;
|
| 13 |
+
const userId = req.userId!;
|
| 14 |
+
|
| 15 |
+
if (!imageUrl) {
|
| 16 |
+
return res.status(400).json({ success: false, error: "imageUrl is required" });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Upload image to Cloudinary
|
| 20 |
+
const uploadResult = await cloudinary.uploader.upload(imageUrl, {
|
| 21 |
+
folder: "wardrobe",
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
// Classify using Fashion-CLIP
|
| 25 |
+
const fashionResult = await classifyFashionImage(uploadResult.secure_url);
|
| 26 |
+
const bestLabel = fashionResult[0]?.label || "unknown";
|
| 27 |
+
|
| 28 |
+
// Save in DB with user association
|
| 29 |
+
const itemRepo = AppDataSource.getRepository(WardrobeItem);
|
| 30 |
+
const newItem = itemRepo.create({
|
| 31 |
+
imageUrl: uploadResult.secure_url,
|
| 32 |
+
category: bestLabel,
|
| 33 |
+
style: bestLabel.includes("formal") ? "formal" : "casual",
|
| 34 |
+
userId: userId,
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
await itemRepo.save(newItem);
|
| 38 |
+
res.json({ success: true, item: newItem });
|
| 39 |
+
} catch (error: any) {
|
| 40 |
+
console.error("Upload error:", error);
|
| 41 |
+
res.status(500).json({ success: false, error: error.message || "Upload failed" });
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
export default router;
|
| 46 |
+
|
src/scripts/run-migrations.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import "reflect-metadata";
|
| 2 |
+
import { AppDataSource } from "../utils/dataSource";
|
| 3 |
+
|
| 4 |
+
async function runMigrations() {
|
| 5 |
+
try {
|
| 6 |
+
console.log("Connecting to database...");
|
| 7 |
+
await AppDataSource.initialize();
|
| 8 |
+
console.log("✅ Database connected");
|
| 9 |
+
|
| 10 |
+
console.log("Running migrations...");
|
| 11 |
+
await AppDataSource.runMigrations();
|
| 12 |
+
console.log("✅ Migrations completed successfully");
|
| 13 |
+
|
| 14 |
+
await AppDataSource.destroy();
|
| 15 |
+
process.exit(0);
|
| 16 |
+
} catch (error) {
|
| 17 |
+
console.error("❌ Migration failed:", error);
|
| 18 |
+
await AppDataSource.destroy();
|
| 19 |
+
process.exit(1);
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
runMigrations();
|
| 24 |
+
|
src/types/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface FashionClassification {
|
| 2 |
+
label: string;
|
| 3 |
+
score: number;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
export interface UploadResponse {
|
| 7 |
+
success: boolean;
|
| 8 |
+
item?: WardrobeItem;
|
| 9 |
+
error?: string;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export interface WardrobeItem {
|
| 13 |
+
id: number;
|
| 14 |
+
imageUrl: string;
|
| 15 |
+
category: string;
|
| 16 |
+
style: string;
|
| 17 |
+
userId: number;
|
| 18 |
+
createdAt: Date;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export interface User {
|
| 22 |
+
id: number;
|
| 23 |
+
name: string;
|
| 24 |
+
email: string;
|
| 25 |
+
createdAt: Date;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export interface AuthResponse {
|
| 29 |
+
success: boolean;
|
| 30 |
+
user?: User;
|
| 31 |
+
token?: string;
|
| 32 |
+
error?: string;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export interface SuggestResponse {
|
| 36 |
+
success: boolean;
|
| 37 |
+
suggestion?: any;
|
| 38 |
+
error?: string;
|
| 39 |
+
}
|
| 40 |
+
|
src/utils/cloudinary.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { v2 as cloudinary } from "cloudinary";
|
| 2 |
+
import dotenv from "dotenv";
|
| 3 |
+
dotenv.config();
|
| 4 |
+
|
| 5 |
+
cloudinary.config({
|
| 6 |
+
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
|
| 7 |
+
api_key: process.env.CLOUDINARY_API_KEY!,
|
| 8 |
+
api_secret: process.env.CLOUDINARY_API_SECRET!,
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
export default cloudinary;
|
| 12 |
+
|
src/utils/dataSource.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import "reflect-metadata";
|
| 2 |
+
import { DataSource } from "typeorm";
|
| 3 |
+
import { WardrobeItem } from "../entity/WardrobeItem";
|
| 4 |
+
import { User } from "../entity/User";
|
| 5 |
+
import dotenv from "dotenv";
|
| 6 |
+
dotenv.config();
|
| 7 |
+
|
| 8 |
+
export const AppDataSource = new DataSource({
|
| 9 |
+
type: "postgres",
|
| 10 |
+
url: process.env.DATABASE_URL,
|
| 11 |
+
synchronize: false,
|
| 12 |
+
logging: true,
|
| 13 |
+
entities: [User, WardrobeItem],
|
| 14 |
+
migrations: [
|
| 15 |
+
process.env.NODE_ENV === "production"
|
| 16 |
+
? "dist/migrations/*.js"
|
| 17 |
+
: "src/migrations/*.ts"
|
| 18 |
+
],
|
| 19 |
+
subscribers: [],
|
| 20 |
+
extra: {
|
| 21 |
+
ssl: process.env.DATABASE_URL?.includes("render.com") ? {
|
| 22 |
+
rejectUnauthorized: false,
|
| 23 |
+
} : false,
|
| 24 |
+
},
|
| 25 |
+
});
|
| 26 |
+
|
src/utils/hfClient.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios from "axios";
|
| 2 |
+
import dotenv from "dotenv";
|
| 3 |
+
dotenv.config();
|
| 4 |
+
|
| 5 |
+
const HF_API_KEY = process.env.HF_API_KEY;
|
| 6 |
+
|
| 7 |
+
export async function classifyFashionImage(imageUrl: string) {
|
| 8 |
+
const endpoint = "https://api-inference.huggingface.co/models/patrickjohncyh/fashion-clip";
|
| 9 |
+
|
| 10 |
+
const payload = {
|
| 11 |
+
inputs: imageUrl,
|
| 12 |
+
parameters: {
|
| 13 |
+
candidate_labels: ["shirt", "pants", "dress", "jacket", "formal", "casual", "streetwear"],
|
| 14 |
+
},
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
const headers = {
|
| 18 |
+
Authorization: `Bearer ${HF_API_KEY}`,
|
| 19 |
+
"Content-Type": "application/json",
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
const response = await axios.post(endpoint, payload, { headers });
|
| 23 |
+
return response.data;
|
| 24 |
+
}
|
| 25 |
+
|
start.sh
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
echo "🚀 Starting StyleGPT Milestone 2..."
|
| 5 |
+
|
| 6 |
+
# Run migrations using compiled JS
|
| 7 |
+
if [ -f "dist/scripts/run-migrations.js" ]; then
|
| 8 |
+
echo "📦 Running database migrations..."
|
| 9 |
+
node dist/scripts/run-migrations.js || echo "⚠️ Migrations completed or skipped"
|
| 10 |
+
else
|
| 11 |
+
echo "⚠️ Migration script not found, skipping..."
|
| 12 |
+
fi
|
| 13 |
+
|
| 14 |
+
# Start the application
|
| 15 |
+
echo "✅ Starting server on port ${PORT:-7860}..."
|
| 16 |
+
exec node dist/index.js
|
tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"module": "commonjs",
|
| 5 |
+
"lib": ["ES2020"],
|
| 6 |
+
"outDir": "./dist",
|
| 7 |
+
"rootDir": "./src",
|
| 8 |
+
"strict": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"skipLibCheck": true,
|
| 11 |
+
"forceConsistentCasingInFileNames": true,
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"moduleResolution": "node",
|
| 14 |
+
"experimentalDecorators": true,
|
| 15 |
+
"emitDecoratorMetadata": true,
|
| 16 |
+
"strictPropertyInitialization": false
|
| 17 |
+
},
|
| 18 |
+
"include": ["src/**/*"],
|
| 19 |
+
"exclude": ["node_modules", "dist"]
|
| 20 |
+
}
|
| 21 |
+
|