Add authentication system with session-based auth
All checks were successful
Build and Push Backend / build (push) Successful in 20s
All checks were successful
Build and Push Backend / build (push) Successful in 20s
- Implement register, login, logout, and me endpoints - Use bcryptjs for password hashing - HTTPOnly secure cookies for sessions (Lucia Auth pattern) - Users and sessions tables with proper relations - 7-day session duration with auto-expiry Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
bun.lock
6
bun.lock
@@ -8,6 +8,7 @@
|
|||||||
"@kubernetes/client-node": "^1.4.0",
|
"@kubernetes/client-node": "^1.4.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"bullmq": "^5.66.5",
|
"bullmq": "^5.66.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"zod": "^4.3.5",
|
"zod": "^4.3.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
@@ -153,6 +155,8 @@
|
|||||||
|
|
||||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||||
|
|
||||||
|
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
|
||||||
|
|
||||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||||
@@ -235,6 +239,8 @@
|
|||||||
|
|
||||||
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
||||||
|
|
||||||
|
"bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
|||||||
24
drizzle/migrations/0001_opposite_warbird.sql
Normal file
24
drizzle/migrations/0001_opposite_warbird.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE `sessions` (
|
||||||
|
`id` varchar(255) NOT NULL,
|
||||||
|
`user_id` varchar(36) NOT NULL,
|
||||||
|
`expires_at` timestamp NOT NULL,
|
||||||
|
`created_at` timestamp DEFAULT (now()),
|
||||||
|
CONSTRAINT `sessions_id` PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` varchar(36) NOT NULL,
|
||||||
|
`email` varchar(255) NOT NULL,
|
||||||
|
`username` varchar(100) NOT NULL,
|
||||||
|
`password_hash` varchar(255) NOT NULL,
|
||||||
|
`created_at` timestamp DEFAULT (now()),
|
||||||
|
`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT `users_id` PRIMARY KEY(`id`),
|
||||||
|
CONSTRAINT `users_email_unique` UNIQUE(`email`),
|
||||||
|
CONSTRAINT `users_username_unique` UNIQUE(`username`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `sessions` ADD CONSTRAINT `sessions_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_user_id` ON `sessions` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_expires_at` ON `sessions` (`expires_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_email` ON `users` (`email`);
|
||||||
578
drizzle/migrations/meta/0001_snapshot.json
Normal file
578
drizzle/migrations/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "ada6a640-177d-4c35-b7e0-e1d20c9adabb",
|
||||||
|
"prevId": "3f9067f3-0e1d-440b-917e-e2c879a5e283",
|
||||||
|
"tables": {
|
||||||
|
"agents": {
|
||||||
|
"name": "agents",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pod_name": {
|
||||||
|
"name": "pod_name",
|
||||||
|
"type": "varchar(253)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"k8s_namespace": {
|
||||||
|
"name": "k8s_namespace",
|
||||||
|
"type": "varchar(63)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'agents'"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "enum('idle','busy','error','offline')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'idle'"
|
||||||
|
},
|
||||||
|
"current_task_id": {
|
||||||
|
"name": "current_task_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tasks_completed": {
|
||||||
|
"name": "tasks_completed",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"last_heartbeat": {
|
||||||
|
"name": "last_heartbeat",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_status": {
|
||||||
|
"name": "idx_status",
|
||||||
|
"columns": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"agents_id": {
|
||||||
|
"name": "agents_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"agents_pod_name_unique": {
|
||||||
|
"name": "agents_pod_name_unique",
|
||||||
|
"columns": [
|
||||||
|
"pod_name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"name": "projects",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_repo_id": {
|
||||||
|
"name": "gitea_repo_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_repo_url": {
|
||||||
|
"name": "gitea_repo_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_owner": {
|
||||||
|
"name": "gitea_owner",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_repo_name": {
|
||||||
|
"name": "gitea_repo_name",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"default_branch": {
|
||||||
|
"name": "default_branch",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'main'"
|
||||||
|
},
|
||||||
|
"k8s_namespace": {
|
||||||
|
"name": "k8s_namespace",
|
||||||
|
"type": "varchar(63)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"docker_image": {
|
||||||
|
"name": "docker_image",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"env_vars": {
|
||||||
|
"name": "env_vars",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"replicas": {
|
||||||
|
"name": "replicas",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"cpu_limit": {
|
||||||
|
"name": "cpu_limit",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'500m'"
|
||||||
|
},
|
||||||
|
"memory_limit": {
|
||||||
|
"name": "memory_limit",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'512Mi'"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "enum('active','paused','archived')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'active'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_status": {
|
||||||
|
"name": "idx_status",
|
||||||
|
"columns": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"projects_id": {
|
||||||
|
"name": "projects_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"projects_k8s_namespace_unique": {
|
||||||
|
"name": "projects_k8s_namespace_unique",
|
||||||
|
"columns": [
|
||||||
|
"k8s_namespace"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_user_id": {
|
||||||
|
"name": "idx_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_expires_at": {
|
||||||
|
"name": "idx_expires_at",
|
||||||
|
"columns": [
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"sessions_user_id_users_id_fk": {
|
||||||
|
"name": "sessions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "sessions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"sessions_id": {
|
||||||
|
"name": "sessions_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"name": "tasks",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"name": "priority",
|
||||||
|
"type": "enum('low','medium','high','urgent')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'medium'"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"name": "state",
|
||||||
|
"type": "enum('backlog','in_progress','needs_input','ready_to_test','approved','staging','production')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'backlog'"
|
||||||
|
},
|
||||||
|
"assigned_agent_id": {
|
||||||
|
"name": "assigned_agent_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"branch_name": {
|
||||||
|
"name": "branch_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pr_url": {
|
||||||
|
"name": "pr_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"preview_url": {
|
||||||
|
"name": "preview_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_project_state": {
|
||||||
|
"name": "idx_project_state",
|
||||||
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"tasks_project_id_projects_id_fk": {
|
||||||
|
"name": "tasks_project_id_projects_id_fk",
|
||||||
|
"tableFrom": "tasks",
|
||||||
|
"tableTo": "projects",
|
||||||
|
"columnsFrom": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"tasks_assigned_agent_id_agents_id_fk": {
|
||||||
|
"name": "tasks_assigned_agent_id_agents_id_fk",
|
||||||
|
"tableFrom": "tasks",
|
||||||
|
"tableTo": "agents",
|
||||||
|
"columnsFrom": [
|
||||||
|
"assigned_agent_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"tasks_id": {
|
||||||
|
"name": "tasks_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password_hash": {
|
||||||
|
"name": "password_hash",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_email": {
|
||||||
|
"name": "idx_email",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"users_id": {
|
||||||
|
"name": "users_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"users_username_unique": {
|
||||||
|
"name": "users_username_unique",
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"tables": {},
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1768858601280,
|
"when": 1768858601280,
|
||||||
"tag": "0000_charming_stature",
|
"tag": "0000_charming_stature",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1768868010676,
|
||||||
|
"tag": "0001_opposite_warbird",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"format": "prettier --write src/**/*.ts"
|
"format": "prettier --write src/**/*.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"@kubernetes/client-node": "^1.4.0",
|
"@kubernetes/client-node": "^1.4.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"bullmq": "^5.66.5",
|
"bullmq": "^5.66.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
|||||||
392
src/api/routes/auth.ts
Normal file
392
src/api/routes/auth.ts
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
/**
|
||||||
|
* Authentication Routes
|
||||||
|
* Using session-based auth with HTTPOnly cookies (Lucia Auth pattern)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db } from '../../db/client'
|
||||||
|
import { users, sessions } from '../../db/schema'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
|
// Session duration: 7 days
|
||||||
|
const SESSION_DURATION_MS = 7 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
// Cookie options
|
||||||
|
const COOKIE_OPTIONS = {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax' as const,
|
||||||
|
path: '/',
|
||||||
|
maxAge: SESSION_DURATION_MS / 1000, // in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash password using bcrypt
|
||||||
|
*/
|
||||||
|
async function hashPassword(password: string): Promise<string> {
|
||||||
|
return await bcrypt.hash(password, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify password against hash
|
||||||
|
*/
|
||||||
|
async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||||
|
return await bcrypt.compare(password, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random session ID
|
||||||
|
*/
|
||||||
|
function generateSessionId(): string {
|
||||||
|
return randomUUID().replace(/-/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create session in database and return session cookie header
|
||||||
|
*/
|
||||||
|
async function createSession(userId: string): Promise<{ sessionId: string; cookieHeader: string }> {
|
||||||
|
const sessionId = generateSessionId()
|
||||||
|
const expiresAt = new Date(Date.now() + SESSION_DURATION_MS)
|
||||||
|
|
||||||
|
await db.insert(sessions).values({
|
||||||
|
id: sessionId,
|
||||||
|
userId,
|
||||||
|
expiresAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
const cookieHeader = serializeCookie('session', sessionId, COOKIE_OPTIONS)
|
||||||
|
|
||||||
|
return { sessionId, cookieHeader }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate session from cookie and return user
|
||||||
|
*/
|
||||||
|
async function validateSession(sessionId: string) {
|
||||||
|
const [session] = await db
|
||||||
|
.select()
|
||||||
|
.from(sessions)
|
||||||
|
.where(eq(sessions.id, sessionId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session expired
|
||||||
|
if (session.expiresAt < new Date()) {
|
||||||
|
await db.delete(sessions).where(eq(sessions.id, sessionId))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
const [user] = await db
|
||||||
|
.select({
|
||||||
|
id: users.id,
|
||||||
|
email: users.email,
|
||||||
|
username: users.username,
|
||||||
|
createdAt: users.createdAt,
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, session.userId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
return user || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete session from database
|
||||||
|
*/
|
||||||
|
async function deleteSession(sessionId: string) {
|
||||||
|
await db.delete(sessions).where(eq(sessions.id, sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize cookie header
|
||||||
|
*/
|
||||||
|
function serializeCookie(
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
options: typeof COOKIE_OPTIONS
|
||||||
|
): string {
|
||||||
|
const parts = [`${name}=${value}`]
|
||||||
|
|
||||||
|
if (options.httpOnly) parts.push('HttpOnly')
|
||||||
|
if (options.secure) parts.push('Secure')
|
||||||
|
if (options.sameSite) parts.push(`SameSite=${options.sameSite}`)
|
||||||
|
if (options.path) parts.push(`Path=${options.path}`)
|
||||||
|
if (options.maxAge) parts.push(`Max-Age=${options.maxAge}`)
|
||||||
|
|
||||||
|
return parts.join('; ')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse cookies from request header
|
||||||
|
*/
|
||||||
|
function parseCookies(cookieHeader: string | null): Record<string, string> {
|
||||||
|
if (!cookieHeader) return {}
|
||||||
|
|
||||||
|
return cookieHeader.split(';').reduce((cookies, cookie) => {
|
||||||
|
const [name, value] = cookie.trim().split('=')
|
||||||
|
if (name && value) {
|
||||||
|
cookies[name] = value
|
||||||
|
}
|
||||||
|
return cookies
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session ID from request cookies
|
||||||
|
*/
|
||||||
|
function getSessionId(req: Request): string | null {
|
||||||
|
const cookieHeader = req.headers.get('cookie')
|
||||||
|
const cookies = parseCookies(cookieHeader)
|
||||||
|
return cookies.session || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/register
|
||||||
|
* Create new user account
|
||||||
|
*/
|
||||||
|
async function handleRegister(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const { email, username, password } = body
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!email || !username || !password) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Email, username, and password are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Password must be at least 8 characters' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
const [existingUser] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.email, email))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'User with this email already exists' },
|
||||||
|
{ status: 409 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
const userId = randomUUID()
|
||||||
|
const passwordHash = await hashPassword(password)
|
||||||
|
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: userId,
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
passwordHash,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
const { cookieHeader } = await createSession(userId)
|
||||||
|
|
||||||
|
// Return user (without password hash)
|
||||||
|
const [user] = await db
|
||||||
|
.select({
|
||||||
|
id: users.id,
|
||||||
|
email: users.email,
|
||||||
|
username: users.username,
|
||||||
|
createdAt: users.createdAt,
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, userId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: { user },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 201,
|
||||||
|
headers: {
|
||||||
|
'Set-Cookie': cookieHeader,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Registration error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Registration failed' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/login
|
||||||
|
* Authenticate user and create session
|
||||||
|
*/
|
||||||
|
async function handleLogin(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const { email, password } = body
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Email and password are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.email, email))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Invalid email or password' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
const valid = await verifyPassword(password, user.passwordHash)
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Invalid email or password' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
const { cookieHeader } = await createSession(user.id)
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
username: user.username,
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Set-Cookie': cookieHeader,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
return Response.json({ success: false, message: 'Login failed' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/logout
|
||||||
|
* Delete session
|
||||||
|
*/
|
||||||
|
async function handleLogout(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const sessionId = getSessionId(req)
|
||||||
|
|
||||||
|
if (sessionId) {
|
||||||
|
await deleteSession(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cookie
|
||||||
|
const clearCookieHeader = serializeCookie('session', '', {
|
||||||
|
...COOKIE_OPTIONS,
|
||||||
|
maxAge: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{ success: true, message: 'Logged out successfully' },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Set-Cookie': clearCookieHeader,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Logout error:', error)
|
||||||
|
return Response.json({ success: false, message: 'Logout failed' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/auth/me
|
||||||
|
* Get current user from session
|
||||||
|
*/
|
||||||
|
async function handleMe(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const sessionId = getSessionId(req)
|
||||||
|
|
||||||
|
if (!sessionId) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Not authenticated' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await validateSession(sessionId)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Invalid or expired session' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
data: user,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Get current user error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Failed to get current user' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route handler for /api/auth/*
|
||||||
|
*/
|
||||||
|
export async function handleAuthRoutes(req: Request, url: URL): Promise<Response> {
|
||||||
|
const path = url.pathname.replace('/api/auth', '')
|
||||||
|
|
||||||
|
if (path === '/register' && req.method === 'POST') {
|
||||||
|
return handleRegister(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/login' && req.method === 'POST') {
|
||||||
|
return handleLogin(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/logout' && req.method === 'POST') {
|
||||||
|
return handleLogout(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/me' && req.method === 'GET') {
|
||||||
|
return handleMe(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ message: 'Not found' }, { status: 404 })
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
* Exports all route handlers
|
* Exports all route handlers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { handleAuthRoutes } from './auth'
|
||||||
export { handleProjectRoutes } from './projects'
|
export { handleProjectRoutes } from './projects'
|
||||||
export { handleTaskRoutes } from './tasks'
|
export { handleTaskRoutes } from './tasks'
|
||||||
export { handleAgentRoutes } from './agents'
|
export { handleAgentRoutes } from './agents'
|
||||||
|
|||||||
60
src/db/apply-migrations.ts
Normal file
60
src/db/apply-migrations.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Apply pending migrations directly
|
||||||
|
*/
|
||||||
|
|
||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
async function applyMigrations() {
|
||||||
|
console.log('📝 Applying pending migrations...')
|
||||||
|
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306'),
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the latest migration file
|
||||||
|
const migrationPath = join(import.meta.dir, '../../drizzle/migrations/0001_opposite_warbird.sql')
|
||||||
|
const migrationSQL = readFileSync(migrationPath, 'utf-8')
|
||||||
|
|
||||||
|
// Split by statement-breakpoint and execute each statement
|
||||||
|
const statements = migrationSQL
|
||||||
|
.split('-->')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s && !s.startsWith('statement-breakpoint'))
|
||||||
|
|
||||||
|
console.log(`Found ${statements.length} statements to execute`)
|
||||||
|
|
||||||
|
for (const statement of statements) {
|
||||||
|
if (statement) {
|
||||||
|
try {
|
||||||
|
await connection.execute(statement)
|
||||||
|
console.log('✅ Executed statement')
|
||||||
|
} catch (error: any) {
|
||||||
|
// Skip if table already exists
|
||||||
|
if (error.code === 'ER_TABLE_EXISTS_ERROR' || error.errno === 1050) {
|
||||||
|
console.log('⚠️ Table already exists, skipping...')
|
||||||
|
} else if (error.code === 'ER_DUP_KEYNAME' || error.errno === 1061) {
|
||||||
|
console.log('⚠️ Index already exists, skipping...')
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Migrations applied successfully')
|
||||||
|
await connection.end()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Migration failed:', error)
|
||||||
|
await connection.end()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await applyMigrations()
|
||||||
76
src/db/force-migrate.ts
Normal file
76
src/db/force-migrate.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Force apply all migration statements
|
||||||
|
*/
|
||||||
|
|
||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
|
||||||
|
const statements = [
|
||||||
|
`CREATE TABLE IF NOT EXISTS \`sessions\` (
|
||||||
|
\`id\` varchar(255) NOT NULL,
|
||||||
|
\`user_id\` varchar(36) NOT NULL,
|
||||||
|
\`expires_at\` timestamp NOT NULL,
|
||||||
|
\`created_at\` timestamp DEFAULT (now()),
|
||||||
|
CONSTRAINT \`sessions_id\` PRIMARY KEY(\`id\`)
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS \`users\` (
|
||||||
|
\`id\` varchar(36) NOT NULL,
|
||||||
|
\`email\` varchar(255) NOT NULL,
|
||||||
|
\`username\` varchar(100) NOT NULL,
|
||||||
|
\`password_hash\` varchar(255) NOT NULL,
|
||||||
|
\`created_at\` timestamp DEFAULT (now()),
|
||||||
|
\`updated_at\` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT \`users_id\` PRIMARY KEY(\`id\`),
|
||||||
|
CONSTRAINT \`users_email_unique\` UNIQUE(\`email\`),
|
||||||
|
CONSTRAINT \`users_username_unique\` UNIQUE(\`username\`)
|
||||||
|
)`,
|
||||||
|
`ALTER TABLE \`sessions\`
|
||||||
|
ADD CONSTRAINT \`sessions_user_id_users_id_fk\`
|
||||||
|
FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`)
|
||||||
|
ON DELETE cascade ON UPDATE no action`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS \`idx_user_id\` ON \`sessions\` (\`user_id\`)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS \`idx_expires_at\` ON \`sessions\` (\`expires_at\`)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS \`idx_email\` ON \`users\` (\`email\`)`,
|
||||||
|
]
|
||||||
|
|
||||||
|
async function forceMigrate() {
|
||||||
|
console.log('🔧 Force migrating database...')
|
||||||
|
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306'),
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const [index, statement] of statements.entries()) {
|
||||||
|
console.log(`Executing statement ${index + 1}/${statements.length}...`)
|
||||||
|
try {
|
||||||
|
await connection.execute(statement)
|
||||||
|
console.log('✅ Success')
|
||||||
|
} catch (error: any) {
|
||||||
|
if (
|
||||||
|
error.code === 'ER_TABLE_EXISTS_ERROR' ||
|
||||||
|
error.code === 'ER_DUP_KEYNAME' ||
|
||||||
|
error.errno === 1050 ||
|
||||||
|
error.errno === 1061
|
||||||
|
) {
|
||||||
|
console.log('⚠️ Already exists, skipping...')
|
||||||
|
} else {
|
||||||
|
console.error('Error:', error.message)
|
||||||
|
// Continue with other statements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ All migrations applied')
|
||||||
|
await connection.end()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Migration failed:', error)
|
||||||
|
await connection.end()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await forceMigrate()
|
||||||
@@ -17,6 +17,39 @@ import {
|
|||||||
index,
|
index,
|
||||||
} from 'drizzle-orm/mysql-core'
|
} from 'drizzle-orm/mysql-core'
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// USERS TABLE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const users = mysqlTable('users', {
|
||||||
|
id: varchar('id', { length: 36 }).primaryKey(),
|
||||||
|
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||||
|
username: varchar('username', { length: 100 }).notNull().unique(),
|
||||||
|
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
createdAt: timestamp('created_at').defaultNow(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||||
|
}, (table) => ({
|
||||||
|
emailIdx: index('idx_email').on(table.email),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SESSIONS TABLE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const sessions = mysqlTable('sessions', {
|
||||||
|
id: varchar('id', { length: 255 }).primaryKey(),
|
||||||
|
userId: varchar('user_id', { length: 36 }).notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
expiresAt: timestamp('expires_at').notNull(),
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
createdAt: timestamp('created_at').defaultNow(),
|
||||||
|
}, (table) => ({
|
||||||
|
userIdIdx: index('idx_user_id').on(table.userId),
|
||||||
|
expiresAtIdx: index('idx_expires_at').on(table.expiresAt),
|
||||||
|
}))
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// PROJECTS TABLE
|
// PROJECTS TABLE
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
11
src/index.ts
11
src/index.ts
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { runMigrations } from './db/migrate'
|
import { runMigrations } from './db/migrate'
|
||||||
import { testConnection } from './db/client'
|
import { testConnection } from './db/client'
|
||||||
import { handleProjectRoutes, handleTaskRoutes, handleAgentRoutes } from './api/routes'
|
import { handleAuthRoutes, handleProjectRoutes, handleTaskRoutes, handleAgentRoutes } from './api/routes'
|
||||||
|
|
||||||
console.log('🚀 Starting AiWorker Backend...')
|
console.log('🚀 Starting AiWorker Backend...')
|
||||||
console.log(`Bun version: ${Bun.version}`)
|
console.log(`Bun version: ${Bun.version}`)
|
||||||
@@ -41,6 +41,11 @@ const server = Bun.serve({
|
|||||||
return handleHealthCheck()
|
return handleHealthCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auth routes
|
||||||
|
if (url.pathname.startsWith('/api/auth')) {
|
||||||
|
return handleAuthRoutes(req, url)
|
||||||
|
}
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
if (url.pathname.startsWith('/api/projects')) {
|
if (url.pathname.startsWith('/api/projects')) {
|
||||||
return handleProjectRoutes(req, url)
|
return handleProjectRoutes(req, url)
|
||||||
@@ -61,6 +66,10 @@ const server = Bun.serve({
|
|||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
endpoints: [
|
endpoints: [
|
||||||
'GET /api/health',
|
'GET /api/health',
|
||||||
|
'POST /api/auth/register',
|
||||||
|
'POST /api/auth/login',
|
||||||
|
'POST /api/auth/logout',
|
||||||
|
'GET /api/auth/me',
|
||||||
'GET /api/projects',
|
'GET /api/projects',
|
||||||
'GET /api/projects/:id',
|
'GET /api/projects/:id',
|
||||||
'POST /api/projects',
|
'POST /api/projects',
|
||||||
|
|||||||
37
test-auth.ts
Normal file
37
test-auth.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Test authentication directly
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db } from './src/db/client'
|
||||||
|
import { users } from './src/db/schema'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
|
async function testAuth() {
|
||||||
|
try {
|
||||||
|
console.log('Testing auth...')
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const passwordHash = await bcrypt.hash('test123', 10)
|
||||||
|
console.log('Password hash:', passwordHash)
|
||||||
|
|
||||||
|
// Insert user
|
||||||
|
const userId = randomUUID()
|
||||||
|
console.log('Inserting user with ID:', userId)
|
||||||
|
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: userId,
|
||||||
|
email: 'test@test.com',
|
||||||
|
username: 'testuser',
|
||||||
|
passwordHash,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('User inserted successfully!')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
testAuth()
|
||||||
Reference in New Issue
Block a user