create-corz 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +232 -0
- package/package.json +15 -0
- package/templates/backend/README.md +35 -0
- package/templates/backend/package.json +21 -0
- package/templates/backend/src/config/db.js +66 -0
- package/templates/backend/src/controllers/auth.js +19 -0
- package/templates/backend/src/middleware/auth.js +18 -0
- package/templates/backend/src/routes/auth.js +19 -0
- package/templates/backend/src/scripts/dbupdate.js +36 -0
- package/templates/backend/src/server.js +16 -0
- package/templates/backend/src/utilities/helpers.js +5 -0
- package/templates/database/schema.sql +5 -0
- package/templates/frontend/index.html +12 -0
- package/templates/frontend/package.json +24 -0
- package/templates/frontend/postcss.config.js +6 -0
- package/templates/frontend/src/App.jsx +37 -0
- package/templates/frontend/src/api/auth.js +6 -0
- package/templates/frontend/src/api/client.js +15 -0
- package/templates/frontend/src/components/ui/Button.jsx +10 -0
- package/templates/frontend/src/components/ui/Input.jsx +8 -0
- package/templates/frontend/src/index.css +3 -0
- package/templates/frontend/src/main.jsx +13 -0
- package/templates/frontend/src/pages/Login.jsx +86 -0
- package/templates/frontend/tailwind.config.js +8 -0
- package/templates/frontend/vite.config.js +11 -0
package/index.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path = require("path")
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const cp = require("child_process")
|
|
5
|
+
const prompts = require("@clack/prompts")
|
|
6
|
+
const { EOL } = require("os")
|
|
7
|
+
|
|
8
|
+
const GREEN = "\x1b[32m"
|
|
9
|
+
const RED = "\x1b[31m"
|
|
10
|
+
const BOLD = "\x1b[1m"
|
|
11
|
+
const DIM = "\x1b[2m"
|
|
12
|
+
const RESET = "\x1b[0m"
|
|
13
|
+
|
|
14
|
+
function generateJwtSecret() {
|
|
15
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
16
|
+
let result = ""
|
|
17
|
+
for (let i = 0; i < 64; i++) {
|
|
18
|
+
result += chars[Math.floor(Math.random() * chars.length)]
|
|
19
|
+
}
|
|
20
|
+
return result
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function copyDir(src, dest, replace) {
|
|
24
|
+
const entries = fs.readdirSync(src, { withFileTypes: true })
|
|
25
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const srcPath = path.join(src, entry.name)
|
|
28
|
+
const destPath = path.join(dest, entry.name)
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
copyDir(srcPath, destPath, replace)
|
|
31
|
+
} else {
|
|
32
|
+
let content = fs.readFileSync(srcPath, "utf-8")
|
|
33
|
+
for (const [key, value] of Object.entries(replace)) {
|
|
34
|
+
content = content.split(key).join(value)
|
|
35
|
+
}
|
|
36
|
+
fs.writeFileSync(destPath, content, "utf-8")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runAsync(cmd, opts) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const child = cp.spawn(cmd, [], { shell: true, stdio: "ignore", ...opts })
|
|
44
|
+
child.on("error", reject)
|
|
45
|
+
child.on("exit", (code) => {
|
|
46
|
+
if (code === 0) resolve()
|
|
47
|
+
else reject(new Error("exit code " + code))
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getBar(elapsed, estimate) {
|
|
53
|
+
const w = 8
|
|
54
|
+
const pct = Math.min(Math.floor((elapsed / estimate) * 99), 99)
|
|
55
|
+
const fill = Math.round((pct / 100) * w)
|
|
56
|
+
return DIM + "[" + GREEN + "\u2588".repeat(fill) + " ".repeat(w - fill) + "]" + RESET + " " + String(pct).padStart(3) + "%"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
process.stderr.write(EOL)
|
|
61
|
+
process.stderr.write(" create-corz " + DIM + "\u2014 fullstack app scaffolder" + RESET + EOL)
|
|
62
|
+
process.stderr.write(EOL)
|
|
63
|
+
|
|
64
|
+
const folderName = await prompts.text({
|
|
65
|
+
message: "Folder name",
|
|
66
|
+
validate: (x) => (x?.trim() ? undefined : "Folder name is required"),
|
|
67
|
+
})
|
|
68
|
+
if (prompts.isCancel(folderName)) process.exit(0)
|
|
69
|
+
|
|
70
|
+
const projectName = await prompts.text({
|
|
71
|
+
message: "Project name",
|
|
72
|
+
validate: (x) => (x?.trim() ? undefined : "Project name is required"),
|
|
73
|
+
})
|
|
74
|
+
if (prompts.isCancel(projectName)) process.exit(0)
|
|
75
|
+
|
|
76
|
+
const databaseName = await prompts.text({
|
|
77
|
+
message: "Database name",
|
|
78
|
+
validate: (x) => (x?.trim() ? undefined : "Database name is required"),
|
|
79
|
+
})
|
|
80
|
+
if (prompts.isCancel(databaseName)) process.exit(0)
|
|
81
|
+
|
|
82
|
+
const backendDir = "backend"
|
|
83
|
+
const frontendDir = "frontend"
|
|
84
|
+
const target = path.resolve(folderName.trim())
|
|
85
|
+
const templateDir = path.resolve(__dirname, "templates")
|
|
86
|
+
|
|
87
|
+
const jwtSecret = generateJwtSecret()
|
|
88
|
+
const port = "3001"
|
|
89
|
+
const replace = {
|
|
90
|
+
"{{projectName}}": projectName.trim(),
|
|
91
|
+
"{{databaseName}}": databaseName.trim(),
|
|
92
|
+
"{{backendFolder}}": backendDir.trim(),
|
|
93
|
+
"{{frontendFolder}}": frontendDir.trim(),
|
|
94
|
+
"{{port}}": port,
|
|
95
|
+
"{{jwtSecret}}": jwtSecret,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
fs.mkdirSync(target, { recursive: true })
|
|
100
|
+
copyDir(path.join(templateDir, "backend"), path.join(target, backendDir.trim()), replace)
|
|
101
|
+
copyDir(path.join(templateDir, "database"), path.join(target, "database"), replace)
|
|
102
|
+
copyDir(path.join(templateDir, "frontend"), path.join(target, frontendDir.trim()), replace)
|
|
103
|
+
} catch (err) {
|
|
104
|
+
process.stderr.write(" " + RED + "\u2716" + RESET + " Scaffolding failed" + EOL)
|
|
105
|
+
process.stderr.write(" " + RED + err.message + RESET + EOL)
|
|
106
|
+
process.exit(1)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
process.stderr.write(" " + GREEN + "\u2714" + RESET + " Project files created" + EOL)
|
|
110
|
+
process.stderr.write(EOL)
|
|
111
|
+
|
|
112
|
+
const tasks = [
|
|
113
|
+
{
|
|
114
|
+
label: "Installing backend dependencies",
|
|
115
|
+
doneLabel: "Backend dependencies installed",
|
|
116
|
+
estimate: 120000,
|
|
117
|
+
run: () => runAsync("npm install", { cwd: path.join(target, backendDir.trim()), timeout: 120000 }),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
label: "Installing frontend dependencies",
|
|
121
|
+
doneLabel: "Frontend dependencies installed",
|
|
122
|
+
estimate: 120000,
|
|
123
|
+
run: () => runAsync("npm install", { cwd: path.join(target, frontendDir.trim()), timeout: 120000 }),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
label: "Installing corz CLI",
|
|
127
|
+
doneLabel: "corz CLI installed",
|
|
128
|
+
failedLabel: "corz CLI install skipped",
|
|
129
|
+
estimate: 60000,
|
|
130
|
+
run: () => runAsync("npm install -g corz", { timeout: 60000 }),
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
135
|
+
const started = Date.now()
|
|
136
|
+
const state = tasks.map((t) => ({
|
|
137
|
+
...t,
|
|
138
|
+
start: Date.now(),
|
|
139
|
+
endTime: 0,
|
|
140
|
+
done: false,
|
|
141
|
+
failed: false,
|
|
142
|
+
}))
|
|
143
|
+
|
|
144
|
+
for (const _ of state) {
|
|
145
|
+
process.stderr.write(" " + EOL)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const promises = state.map((s) =>
|
|
149
|
+
s
|
|
150
|
+
.run()
|
|
151
|
+
.then(
|
|
152
|
+
() => {
|
|
153
|
+
s.done = true
|
|
154
|
+
s.endTime = Date.now()
|
|
155
|
+
},
|
|
156
|
+
() => {
|
|
157
|
+
s.done = true
|
|
158
|
+
s.failed = true
|
|
159
|
+
s.endTime = Date.now()
|
|
160
|
+
},
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
function renderLine(s) {
|
|
165
|
+
const now = s.done ? s.endTime : Date.now()
|
|
166
|
+
const elapsed = ((now - s.start) / 1000).toFixed(1) + "s"
|
|
167
|
+
if (s.done) {
|
|
168
|
+
const pct = 100
|
|
169
|
+
const sym = s.failed ? RED + "\u2716" + RESET : GREEN + "\u2714" + RESET
|
|
170
|
+
const label = s.failed ? (s.failedLabel || s.label) : (s.doneLabel || s.label)
|
|
171
|
+
const bar = DIM + "[" + GREEN + "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588" + DIM + "]" + RESET + " " + pct + "%"
|
|
172
|
+
return " " + sym + " " + label + " " + DIM + elapsed + RESET + " " + bar + "\x1b[0K"
|
|
173
|
+
}
|
|
174
|
+
const bar = getBar(Date.now() - s.start, s.estimate)
|
|
175
|
+
return " " + frames[fi % frames.length] + " " + s.label + " " + DIM + elapsed + RESET + " " + bar + "\x1b[0K"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let fi = 0
|
|
179
|
+
let stopped = false
|
|
180
|
+
const interval = setInterval(() => {
|
|
181
|
+
if (stopped) return
|
|
182
|
+
fi++
|
|
183
|
+
process.stderr.write("\x1b[" + state.length + "A")
|
|
184
|
+
for (const s of state) {
|
|
185
|
+
process.stderr.write(renderLine(s) + EOL)
|
|
186
|
+
}
|
|
187
|
+
if (state.every((s) => s.done)) {
|
|
188
|
+
stopped = true
|
|
189
|
+
clearInterval(interval)
|
|
190
|
+
}
|
|
191
|
+
}, 150)
|
|
192
|
+
|
|
193
|
+
await Promise.allSettled(promises)
|
|
194
|
+
if (!stopped) {
|
|
195
|
+
stopped = true
|
|
196
|
+
clearInterval(interval)
|
|
197
|
+
process.stderr.write("\x1b[" + state.length + "A")
|
|
198
|
+
for (const s of state) {
|
|
199
|
+
process.stderr.write(renderLine(s) + EOL)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const totalTime = ((Date.now() - started) / 1000).toFixed(1)
|
|
204
|
+
process.stderr.write(EOL)
|
|
205
|
+
process.stderr.write(
|
|
206
|
+
" " + GREEN + "\u2714" + RESET + " " + BOLD + 'Project "' + projectName.trim() + '" created!' + RESET + " " + DIM + "completed in " + totalTime + "s" + RESET + EOL,
|
|
207
|
+
)
|
|
208
|
+
process.stderr.write(EOL)
|
|
209
|
+
|
|
210
|
+
process.stderr.write(" " + BOLD + "1. Start backend:" + RESET + EOL)
|
|
211
|
+
process.stderr.write(" cd " + folderName.trim() + "/" + backendDir.trim() + EOL)
|
|
212
|
+
process.stderr.write(" " + DIM + "Edit .env" + RESET + " with your MySQL credentials" + EOL)
|
|
213
|
+
process.stderr.write(" " + DIM + "npm run dev" + RESET + " " + DIM + "\u2192 backend on :" + port + RESET + EOL)
|
|
214
|
+
process.stderr.write(EOL)
|
|
215
|
+
process.stderr.write(" " + BOLD + "2. Start frontend (new terminal):" + RESET + EOL)
|
|
216
|
+
process.stderr.write(" cd " + folderName.trim() + "/" + frontendDir.trim() + EOL)
|
|
217
|
+
process.stderr.write(" " + DIM + "npm run dev" + RESET + " " + DIM + "\u2192 frontend on :5173" + RESET + EOL)
|
|
218
|
+
process.stderr.write(EOL)
|
|
219
|
+
process.stderr.write(" " + BOLD + "\u2501".repeat(45) + RESET + EOL)
|
|
220
|
+
process.stderr.write(" " + BOLD + "\u2726 Use corz ai to edit your current project." + RESET + EOL)
|
|
221
|
+
process.stderr.write(" " + DIM + "corz> \"add the employee page and backend\"" + RESET + EOL)
|
|
222
|
+
process.stderr.write(" " + DIM + "corz> \"change the design like white and green for buttons\"" + RESET + EOL)
|
|
223
|
+
process.stderr.write(EOL)
|
|
224
|
+
process.stderr.write(" " + DIM + "\u2192 Unlock corz ai with " + RESET + GREEN + BOLD + "corz activate" + RESET + EOL)
|
|
225
|
+
process.stderr.write(" " + BOLD + "\u2501".repeat(45) + RESET + EOL)
|
|
226
|
+
process.stderr.write(EOL)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
main().catch((err) => {
|
|
230
|
+
process.stderr.write(RED + err.message + RESET + EOL)
|
|
231
|
+
process.exit(1)
|
|
232
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-corz",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Scaffold a fullstack React + Express + MySQL app in seconds",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-corz": "./index.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": ["corz", "fullstack", "scaffold", "react", "express", "mysql"],
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@clack/prompts": "^1.0.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# {{projectName}} Backend
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
1. Create the database:
|
|
6
|
+
```bash
|
|
7
|
+
mysql -u root < database/schema.sql
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
2. Configure environment:
|
|
11
|
+
```bash
|
|
12
|
+
cp .env.example .env
|
|
13
|
+
```
|
|
14
|
+
Edit `.env` with your MySQL credentials.
|
|
15
|
+
|
|
16
|
+
3. Install dependencies:
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
4. Start the server:
|
|
22
|
+
```bash
|
|
23
|
+
npm run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Default User
|
|
27
|
+
|
|
28
|
+
- Username: `user`
|
|
29
|
+
- Password: `user123`
|
|
30
|
+
|
|
31
|
+
## API Endpoints
|
|
32
|
+
|
|
33
|
+
| Method | Path | Description |
|
|
34
|
+
|--------|------|-------------|
|
|
35
|
+
| POST | `/api/login` | Login with username and password |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}-backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "nodemon src/server.js",
|
|
7
|
+
"start": "node src/server.js",
|
|
8
|
+
"dbupdate": "node src/scripts/dbupdate.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"cors": "^2.8.5",
|
|
12
|
+
"dotenv": "^16.4.7",
|
|
13
|
+
"express": "^4.21.2",
|
|
14
|
+
"bcrypt": "^5.1.1",
|
|
15
|
+
"jsonwebtoken": "^9.0.2",
|
|
16
|
+
"mysql2": "^3.12.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"nodemon": "^3.1.9"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const mysql = require("mysql2/promise")
|
|
2
|
+
const fs = require("fs")
|
|
3
|
+
const path = require("path")
|
|
4
|
+
const bcrypt = require("bcrypt")
|
|
5
|
+
|
|
6
|
+
let pool = null
|
|
7
|
+
const status = { ready: false, code: null }
|
|
8
|
+
|
|
9
|
+
async function bootstrap() {
|
|
10
|
+
try {
|
|
11
|
+
const conn = await mysql.createConnection({
|
|
12
|
+
host: process.env.DB_HOST,
|
|
13
|
+
user: process.env.DB_USER,
|
|
14
|
+
password: process.env.DB_PASS,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
await conn.query(`CREATE DATABASE IF NOT EXISTS \`${process.env.DB_NAME}\``)
|
|
18
|
+
await conn.query(`USE \`${process.env.DB_NAME}\``)
|
|
19
|
+
|
|
20
|
+
const schemaPath = path.resolve(__dirname, "../../../database/schema.sql")
|
|
21
|
+
const raw = fs.readFileSync(schemaPath, "utf8")
|
|
22
|
+
for (const stmt of raw.split(";").map(s => s.trim()).filter(Boolean)) {
|
|
23
|
+
await conn.query(stmt)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await conn.end()
|
|
27
|
+
|
|
28
|
+
pool = mysql.createPool({
|
|
29
|
+
host: process.env.DB_HOST,
|
|
30
|
+
user: process.env.DB_USER,
|
|
31
|
+
password: process.env.DB_PASS,
|
|
32
|
+
database: process.env.DB_NAME,
|
|
33
|
+
waitForConnections: true,
|
|
34
|
+
connectionLimit: 10,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const [rows] = await pool.query("SELECT COUNT(*) AS count FROM users")
|
|
38
|
+
if (rows[0].count === 0) {
|
|
39
|
+
const hash = await bcrypt.hash("user123", 10)
|
|
40
|
+
await pool.query("INSERT INTO users (username, password) VALUES (?, ?)", ["user", hash])
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
status.ready = true
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND" || err.code === "ER_ACCESS_DENIED_ERROR") {
|
|
46
|
+
status.code = "...dbcon"
|
|
47
|
+
} else {
|
|
48
|
+
status.code = "...notexist"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getPool() {
|
|
54
|
+
if (!status.ready) {
|
|
55
|
+
const err = new Error(status.code || "...dbcon")
|
|
56
|
+
err.code = status.code || "...dbcon"
|
|
57
|
+
throw err
|
|
58
|
+
}
|
|
59
|
+
return pool
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getStatus() {
|
|
63
|
+
return status
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { bootstrap, getPool, getStatus }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const jwt = require("jsonwebtoken")
|
|
2
|
+
const bcrypt = require("bcrypt")
|
|
3
|
+
|
|
4
|
+
async function login(db, { username, password }) {
|
|
5
|
+
if (!username || !password) throw new Error("Username and password are required")
|
|
6
|
+
|
|
7
|
+
const [rows] = await db.query("SELECT * FROM users WHERE username = ?", [username])
|
|
8
|
+
if (rows.length === 0) throw new Error("Invalid username or password")
|
|
9
|
+
|
|
10
|
+
const user = rows[0]
|
|
11
|
+
const match = await bcrypt.compare(password, user.password)
|
|
12
|
+
if (!match) throw new Error("Invalid username or password")
|
|
13
|
+
|
|
14
|
+
const token = jwt.sign({ id: user.id, username: user.username }, process.env.JWT_SECRET, { expiresIn: "7d" })
|
|
15
|
+
|
|
16
|
+
return { token, user: { id: user.id, username: user.username } }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { login }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const jwt = require("jsonwebtoken")
|
|
2
|
+
|
|
3
|
+
function auth(req, res, next) {
|
|
4
|
+
const header = req.headers.authorization
|
|
5
|
+
if (!header || !header.startsWith("Bearer ")) {
|
|
6
|
+
return res.status(401).json({ error: "No token provided" })
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const decoded = jwt.verify(header.split(" ")[1], process.env.JWT_SECRET)
|
|
11
|
+
req.user = decoded
|
|
12
|
+
next()
|
|
13
|
+
} catch {
|
|
14
|
+
res.status(401).json({ error: "Invalid token" })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = auth
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const express = require("express")
|
|
2
|
+
const router = express.Router()
|
|
3
|
+
const { getPool, getStatus } = require("../config/db")
|
|
4
|
+
const { login } = require("../controllers/auth")
|
|
5
|
+
|
|
6
|
+
router.post("/login", async (req, res) => {
|
|
7
|
+
const st = getStatus()
|
|
8
|
+
if (!st.ready) {
|
|
9
|
+
return res.status(503).json({ error: st.code || "...dbcon" })
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const result = await login(getPool(), req.body)
|
|
13
|
+
res.json(result)
|
|
14
|
+
} catch (err) {
|
|
15
|
+
res.status(400).json({ error: err.message })
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
module.exports = router
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require("dotenv").config()
|
|
2
|
+
const mysql = require("mysql2/promise")
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const path = require("path")
|
|
5
|
+
const bcrypt = require("bcrypt")
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const conn = await mysql.createConnection({
|
|
9
|
+
host: process.env.DB_HOST,
|
|
10
|
+
user: process.env.DB_USER,
|
|
11
|
+
password: process.env.DB_PASS,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
await conn.query(`CREATE DATABASE IF NOT EXISTS \`${process.env.DB_NAME}\``)
|
|
15
|
+
await conn.query(`USE \`${process.env.DB_NAME}\``)
|
|
16
|
+
|
|
17
|
+
const schemaPath = path.resolve(__dirname, "../../../database/schema.sql")
|
|
18
|
+
const raw = fs.readFileSync(schemaPath, "utf8")
|
|
19
|
+
for (const stmt of raw.split(";").map(s => s.trim()).filter(Boolean)) {
|
|
20
|
+
await conn.query(stmt)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const [rows] = await conn.query("SELECT COUNT(*) AS count FROM users")
|
|
24
|
+
if (rows[0].count === 0) {
|
|
25
|
+
const hash = await bcrypt.hash("user123", 10)
|
|
26
|
+
await conn.query("INSERT INTO users (username, password) VALUES (?, ?)", ["user", hash])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await conn.end()
|
|
30
|
+
console.log("Database updated successfully")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main().catch((err) => {
|
|
34
|
+
console.error("Database update failed:", err.message)
|
|
35
|
+
process.exit(1)
|
|
36
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const express = require("express")
|
|
2
|
+
const cors = require("cors")
|
|
3
|
+
require("dotenv").config()
|
|
4
|
+
const { bootstrap } = require("./config/db")
|
|
5
|
+
const authRoutes = require("./routes/auth")
|
|
6
|
+
|
|
7
|
+
const app = express()
|
|
8
|
+
app.use(cors())
|
|
9
|
+
app.use(express.json())
|
|
10
|
+
app.use("/api", authRoutes)
|
|
11
|
+
|
|
12
|
+
const PORT = process.env.PORT || {{port}}
|
|
13
|
+
|
|
14
|
+
bootstrap().then(() => {
|
|
15
|
+
app.listen(PORT)
|
|
16
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{projectName}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.7.9",
|
|
13
|
+
"react": "^18.3.1",
|
|
14
|
+
"react-dom": "^18.3.1",
|
|
15
|
+
"react-router-dom": "^6.28.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
19
|
+
"autoprefixer": "^10.4.20",
|
|
20
|
+
"postcss": "^8.4.49",
|
|
21
|
+
"tailwindcss": "^3.4.17",
|
|
22
|
+
"vite": "^5.4.11"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import Login from "./pages/Login"
|
|
3
|
+
|
|
4
|
+
function App() {
|
|
5
|
+
const [token, setToken] = useState(localStorage.getItem("token"))
|
|
6
|
+
|
|
7
|
+
function handleLogin(newToken) {
|
|
8
|
+
localStorage.setItem("token", newToken)
|
|
9
|
+
setToken(newToken)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function handleLogout() {
|
|
13
|
+
localStorage.removeItem("token")
|
|
14
|
+
setToken(null)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (token) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
|
20
|
+
<div className="text-center">
|
|
21
|
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">Welcome!</h1>
|
|
22
|
+
<p className="text-gray-500 mb-6">You are now signed in.</p>
|
|
23
|
+
<button
|
|
24
|
+
onClick={handleLogout}
|
|
25
|
+
className="px-6 py-2 bg-black text-white rounded-lg hover:bg-gray-800 text-sm font-medium"
|
|
26
|
+
>
|
|
27
|
+
Sign Out
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return <Login onLogin={handleLogin} />
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default App
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import axios from "axios"
|
|
2
|
+
|
|
3
|
+
const client = axios.create({
|
|
4
|
+
baseURL: "/api",
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
client.interceptors.request.use((config) => {
|
|
8
|
+
const token = localStorage.getItem("token")
|
|
9
|
+
if (token) {
|
|
10
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
11
|
+
}
|
|
12
|
+
return config
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export default client
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import ReactDOM from "react-dom/client"
|
|
3
|
+
import { BrowserRouter } from "react-router-dom"
|
|
4
|
+
import App from "./App"
|
|
5
|
+
import "./index.css"
|
|
6
|
+
|
|
7
|
+
ReactDOM.createRoot(document.getElementById("root")).render(
|
|
8
|
+
<React.StrictMode>
|
|
9
|
+
<BrowserRouter>
|
|
10
|
+
<App />
|
|
11
|
+
</BrowserRouter>
|
|
12
|
+
</React.StrictMode>
|
|
13
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { login as loginApi } from "../api/auth"
|
|
3
|
+
|
|
4
|
+
export default function Login({ onLogin }) {
|
|
5
|
+
const [username, setUsername] = useState("")
|
|
6
|
+
const [password, setPassword] = useState("")
|
|
7
|
+
const [error, setError] = useState("")
|
|
8
|
+
const [loading, setLoading] = useState(false)
|
|
9
|
+
|
|
10
|
+
async function handleSubmit(e) {
|
|
11
|
+
e.preventDefault()
|
|
12
|
+
setError("")
|
|
13
|
+
setLoading(true)
|
|
14
|
+
try {
|
|
15
|
+
const data = await loginApi(username, password)
|
|
16
|
+
onLogin(data.token)
|
|
17
|
+
} catch (err) {
|
|
18
|
+
setError(err.response?.data?.error || "Invalid username or password")
|
|
19
|
+
} finally {
|
|
20
|
+
setLoading(false)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="min-h-screen bg-[#f5f0eb] flex items-center justify-center p-4 sm:p-6">
|
|
26
|
+
<div className="w-full max-w-sm mx-auto">
|
|
27
|
+
<div className="text-center mb-8 sm:mb-10">
|
|
28
|
+
<h1 className="text-[22px] sm:text-2xl font-bold text-[#1e3a5f] tracking-tight">{{projectName}}</h1>
|
|
29
|
+
<p className="text-gray-500 text-sm sm:text-[15px] mt-1">Sign in to your account</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-[0_4px_24px_rgba(0,0,0,0.06)] p-6 sm:p-8">
|
|
33
|
+
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-5">
|
|
34
|
+
<div>
|
|
35
|
+
<label className="block text-[13px] sm:text-sm font-medium text-gray-700 mb-1.5">Username</label>
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
value={username}
|
|
39
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
40
|
+
className="w-full border border-gray-200 bg-gray-50/50 rounded-xl px-4 py-[10px] sm:py-2.5 text-sm sm:text-[15px] focus:outline-none focus:ring-2 focus:ring-[#1e3a5f]/20 focus:border-[#1e3a5f] focus:bg-white transition-all duration-200 placeholder:text-gray-400"
|
|
41
|
+
placeholder="Enter your username"
|
|
42
|
+
required
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div>
|
|
47
|
+
<label className="block text-[13px] sm:text-sm font-medium text-gray-700 mb-1.5">Password</label>
|
|
48
|
+
<input
|
|
49
|
+
type="password"
|
|
50
|
+
value={password}
|
|
51
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
52
|
+
className="w-full border border-gray-200 bg-gray-50/50 rounded-xl px-4 py-[10px] sm:py-2.5 text-sm sm:text-[15px] focus:outline-none focus:ring-2 focus:ring-[#1e3a5f]/20 focus:border-[#1e3a5f] focus:bg-white transition-all duration-200 placeholder:text-gray-400"
|
|
53
|
+
placeholder="Enter your password"
|
|
54
|
+
required
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{error && (
|
|
59
|
+
<div className="bg-red-50 border border-red-100 rounded-xl px-3 py-2.5 sm:px-4">
|
|
60
|
+
<p className="text-red-500 text-[13px] sm:text-sm text-center font-medium">{error}</p>
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
<button
|
|
65
|
+
type="submit"
|
|
66
|
+
disabled={loading}
|
|
67
|
+
className="w-full bg-[#1e3a5f] text-white rounded-xl py-[10px] sm:py-2.5 text-sm sm:text-[15px] font-semibold hover:bg-[#162d4a] active:bg-[#0f2238] disabled:opacity-50 transition-all duration-200 shadow-[0_1px_2px_rgba(0,0,0,0.05)]"
|
|
68
|
+
>
|
|
69
|
+
{loading ? (
|
|
70
|
+
<span className="inline-flex items-center justify-center gap-2">
|
|
71
|
+
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
|
|
72
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
73
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
74
|
+
</svg>
|
|
75
|
+
Signing in...
|
|
76
|
+
</span>
|
|
77
|
+
) : (
|
|
78
|
+
"Sign In"
|
|
79
|
+
)}
|
|
80
|
+
</button>
|
|
81
|
+
</form>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|