aaex-cli 1.1.0 → 1.2.0
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/create-aaex-app.js +8 -1
- package/package.json +9 -1
- package/template/.aaex/BuildApiRoutes.js +42 -28
- package/template/.aaex/api/auth/login.ts +57 -0
- package/template/.aaex/api/auth/register.ts +34 -0
- package/template/.aaex/api/auth/validate.ts +18 -0
- package/template/.aaex/framework/database/mongodb.ts +29 -0
- package/template/.aaex/framework/entry-server.tsx +1 -1
- package/template/.aaex/server/server.js +21 -11
- package/template/.aaex/utils/ServerLoadCssImports.ts +51 -0
- package/template/.aaex/utils/cookies.ts +63 -0
- package/template/.env +4 -0
- package/template/index.html +2 -1
- package/template/src/hooks/useAuth.tsx +48 -0
- package/template/src/index.css +7 -0
- package/template/src/models/User.ts +13 -0
- package/template/src/pages/index.tsx +17 -1
- package/template/src/pages/login.tsx +144 -0
- package/template/src/pages/register.tsx +168 -0
- package/template/src/routeTypes.ts +5 -0
- package/template/src/routes.ts +10 -0
- package/template/tsconfig.api.json +17 -0
- package/template/vite.config.ts +6 -5
package/create-aaex-app.js
CHANGED
|
@@ -60,8 +60,9 @@ async function createPackageJson() {
|
|
|
60
60
|
"npm run build:client && npm run build:server && npm run build:api",
|
|
61
61
|
"build:client": "vite build --outDir dist/client",
|
|
62
62
|
"build:server":
|
|
63
|
-
"vite build --ssr .aaex/framework/server
|
|
63
|
+
"vite build --ssr .aaex/framework/entry-server.tsx --outDir dist/server",
|
|
64
64
|
"build:api": "tsc --project tsconfig.api.json",
|
|
65
|
+
"build:utils": "tsc --project tsconfig.utils.json",
|
|
65
66
|
preview: "cross-env NODE_ENV=production node .aaex/server/server.js",
|
|
66
67
|
},
|
|
67
68
|
dependencies: {
|
|
@@ -73,6 +74,10 @@ async function createPackageJson() {
|
|
|
73
74
|
compression: "^1.8.1",
|
|
74
75
|
sirv: "^3.0.2",
|
|
75
76
|
"aaex-file-router": "^1.4.4",
|
|
77
|
+
jsonwebtoken: "^9.0.3",
|
|
78
|
+
mongodb: "^7.0.0",
|
|
79
|
+
bcrypt: "^6.0.0",
|
|
80
|
+
dotenv: "^17.2.3",
|
|
76
81
|
},
|
|
77
82
|
devDependencies: {
|
|
78
83
|
typescript: "~5.9.2",
|
|
@@ -82,6 +87,8 @@ async function createPackageJson() {
|
|
|
82
87
|
"@vitejs/plugin-react": "^5.0.2",
|
|
83
88
|
vite: "^7.1.5",
|
|
84
89
|
"cross-env": "^10.0.0",
|
|
90
|
+
"@types/bcrypt": "^6.0.0",
|
|
91
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
85
92
|
},
|
|
86
93
|
};
|
|
87
94
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aaex-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Command line interface for creating aaexjs app",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -12,8 +12,16 @@
|
|
|
12
12
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
+
"@types/bcrypt": "^6.0.0",
|
|
16
|
+
"@types/express": "^5.0.6",
|
|
17
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
18
|
+
"@types/node": "^24.10.1",
|
|
15
19
|
"@vitejs/plugin-react": "^5.1.1",
|
|
16
20
|
"aaex-file-router": "^1.4.5",
|
|
21
|
+
"bcrypt": "^6.0.0",
|
|
22
|
+
"express": "^5.2.1",
|
|
23
|
+
"jsonwebtoken": "^9.0.3",
|
|
24
|
+
"mongodb": "^7.0.0",
|
|
17
25
|
"react": "^19.2.1",
|
|
18
26
|
"react-dom": "^19.2.1",
|
|
19
27
|
"react-router": "^7.10.1",
|
|
@@ -1,53 +1,67 @@
|
|
|
1
1
|
import { FileScanner } from "aaex-file-router/core";
|
|
2
2
|
import { match } from "path-to-regexp";
|
|
3
3
|
|
|
4
|
-
// ---
|
|
5
|
-
|
|
4
|
+
// --- scan folders ---
|
|
5
|
+
//wrapper for the FilesScanner and output data
|
|
6
|
+
async function scanApiFolder(folder) {
|
|
7
|
+
const scanner = new FileScanner(folder);
|
|
8
|
+
return await scanner.get_file_data();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const userApiFolder = "src/api";
|
|
6
12
|
|
|
7
|
-
const
|
|
13
|
+
const internalApiFolder = ".aaex/api";
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
//collect file data as easily parsed arrays
|
|
16
|
+
const [userFiles, internalFiles] = await Promise.all([
|
|
17
|
+
scanApiFolder(userApiFolder),
|
|
18
|
+
scanApiFolder(internalApiFolder),
|
|
19
|
+
]);
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
// --- combine files ---
|
|
22
|
+
const fileMap = new Map();
|
|
23
|
+
internalFiles.forEach((f) => fileMap.set(f.relative_path, f));
|
|
24
|
+
userFiles.forEach((f) => fileMap.set(f.relative_path, f)); //overrides internal api with user defined routes
|
|
25
|
+
const combinedFiles = Array.from(fileMap.values());
|
|
12
26
|
|
|
13
|
-
// ---
|
|
14
|
-
|
|
27
|
+
// --- build route list ---
|
|
28
|
+
/**Builds route object from the scanned files */
|
|
29
|
+
function buildApiRoutes(files) {
|
|
15
30
|
const routes = [];
|
|
16
31
|
|
|
17
32
|
function walk(node, currentPath) {
|
|
33
|
+
//recursivly iterate over child routes
|
|
18
34
|
if (node.isDirectory) {
|
|
19
|
-
node.children?.forEach((
|
|
20
|
-
walk(child, currentPath + "/" + node.name);
|
|
21
|
-
});
|
|
35
|
+
node.children?.forEach((c) => walk(c, currentPath + "/" + node.name));
|
|
22
36
|
return;
|
|
23
37
|
}
|
|
24
|
-
|
|
25
38
|
const filePath = currentPath + "/" + node.name;
|
|
26
|
-
|
|
39
|
+
//build route path from filePath
|
|
27
40
|
let route = filePath
|
|
28
|
-
.replace(/^.*src\/api/, "/api") //
|
|
29
|
-
.replace(/\.ts$/, "")
|
|
30
|
-
.replace(/\[(.+?)\]/g, ":$1")
|
|
31
|
-
.replace("/index", "");
|
|
32
|
-
|
|
33
|
-
routes.push({
|
|
34
|
-
route,
|
|
35
|
-
filePath: node.relative_path,
|
|
36
|
-
});
|
|
41
|
+
.replace(/^.*(src\/api|api)/, "/api") //removed parent folder like src
|
|
42
|
+
.replace(/\.ts|js$/, "") //removes file extension
|
|
43
|
+
.replace(/\[(.+?)\]/g, ":$1") //converts [slug] to :slug
|
|
44
|
+
.replace("/index", "");
|
|
45
|
+
|
|
46
|
+
routes.push({ route, filePath: node.relative_path });
|
|
37
47
|
}
|
|
38
48
|
|
|
39
|
-
files.forEach((
|
|
49
|
+
files.forEach((file) => walk(file, ""));
|
|
40
50
|
return routes;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
const routes = buildApiRoutes(combinedFiles);
|
|
54
|
+
|
|
55
|
+
// --- match path to route ---
|
|
56
|
+
/** Matches the given path to API routes generated from the api folder + the aaex/api folder*/
|
|
57
|
+
function pathToRoute(pathname) {
|
|
45
58
|
for (const r of routes) {
|
|
46
59
|
const matcher = match(r.route, { decode: decodeURIComponent });
|
|
47
|
-
const matched = matcher(
|
|
48
|
-
if (matched) {
|
|
49
|
-
return { route: r, params: matched.params }; // return params for dynamic segments
|
|
50
|
-
}
|
|
60
|
+
const matched = matcher(pathname);
|
|
61
|
+
if (matched) return { route: r, params: matched.params };
|
|
51
62
|
}
|
|
52
63
|
return null;
|
|
53
64
|
}
|
|
65
|
+
|
|
66
|
+
// --- export ---
|
|
67
|
+
export { routes, pathToRoute };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { connectToDatabase } from "../../framework/database/mongodb";
|
|
2
|
+
import type { Request, Response } from "express";
|
|
3
|
+
import bcrypt from "bcrypt";
|
|
4
|
+
import jwt from "jsonwebtoken";
|
|
5
|
+
import { LoginUser } from "../../../src/models/User";
|
|
6
|
+
|
|
7
|
+
export const POST = async (req: Request, res: Response) => {
|
|
8
|
+
const { email, password }: LoginUser = req.body;
|
|
9
|
+
|
|
10
|
+
if (!email || !password) {
|
|
11
|
+
return res.status(400).json({ error: "Missing fields!" });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { db } = await connectToDatabase();
|
|
15
|
+
|
|
16
|
+
if (!process.env.JWT_SECRET) {
|
|
17
|
+
console.error("Missing: JWT_SECRET from environment variables");
|
|
18
|
+
return res
|
|
19
|
+
.status(500)
|
|
20
|
+
.json({ error: "Internal server error! Try again later" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
24
|
+
const user = await db.collection("users").findOne({ email: normalizedEmail });
|
|
25
|
+
|
|
26
|
+
if (!user) {
|
|
27
|
+
return res.status(400).json({ error: "Invalid email or password" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const compared = await bcrypt.compare(password, user.password);
|
|
31
|
+
|
|
32
|
+
if (!compared) {
|
|
33
|
+
return res.status(400).json({ error: "Invalid email or password" });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const expiration = process.env.JWT_EXP ?? "24h";
|
|
37
|
+
|
|
38
|
+
const token = jwt.sign(
|
|
39
|
+
{
|
|
40
|
+
id: user._id.toString(),
|
|
41
|
+
email: user.email,
|
|
42
|
+
username: user.username,
|
|
43
|
+
},
|
|
44
|
+
process.env.JWT_SECRET as string,
|
|
45
|
+
{ expiresIn: expiration as any } //fixes stupid thing where it wont accept the sring variable because its not number | ms.stringvlue | undefined
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return res.status(200).json({
|
|
49
|
+
ok: true,
|
|
50
|
+
user: {
|
|
51
|
+
id: user._id.toString(),
|
|
52
|
+
name: user.username,
|
|
53
|
+
email: user.email,
|
|
54
|
+
},
|
|
55
|
+
token,
|
|
56
|
+
});
|
|
57
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import bcrypt from "bcrypt";
|
|
3
|
+
import { connectToDatabase } from "../../framework/database/mongodb";
|
|
4
|
+
import { CreateUser } from "../../../src/models/User";
|
|
5
|
+
export const POST = async (req: Request, res: Response) => {
|
|
6
|
+
|
|
7
|
+
const { email, username, password, confirmPass }: CreateUser = req.body;
|
|
8
|
+
|
|
9
|
+
if (!username || !email || !password || !confirmPass)
|
|
10
|
+
return res.status(400).json({ error: "Missing fields" });
|
|
11
|
+
|
|
12
|
+
if (password !== confirmPass)
|
|
13
|
+
return res.status(400).json({ error: "Passwords do not match" });
|
|
14
|
+
|
|
15
|
+
const { db } = await connectToDatabase();
|
|
16
|
+
|
|
17
|
+
const exists = await db.collection("users").findOne({ email });
|
|
18
|
+
if (exists)
|
|
19
|
+
return res
|
|
20
|
+
.status(409)
|
|
21
|
+
.json({ error: "User with that email already exists" });
|
|
22
|
+
|
|
23
|
+
const salt = 10;
|
|
24
|
+
const hashed = await bcrypt.hash(password, salt);
|
|
25
|
+
|
|
26
|
+
await db.collection("users").insertOne({
|
|
27
|
+
username,
|
|
28
|
+
email,
|
|
29
|
+
password: hashed,
|
|
30
|
+
createdAt: new Date(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return res.status(201).json({ ok: true });
|
|
34
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
import type { Request, Response } from "express";
|
|
3
|
+
|
|
4
|
+
export async function POST(req: Request, res: Response) {
|
|
5
|
+
const { token } = req.body;
|
|
6
|
+
|
|
7
|
+
if (!process.env.JWT_SECRET) {
|
|
8
|
+
console.error("Missing: JWT_SECRET from environment");
|
|
9
|
+
return res.status(500).json({error: "Internal server error!"});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
14
|
+
return res.status(200).json({ valid: true, user: decoded });
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return res.status(401).json({ valid: false });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MongoClient, Db } from "mongodb";
|
|
2
|
+
|
|
3
|
+
const MONGO_URI = process.env.MONGO_URI || "mongodb://localhost:27017";
|
|
4
|
+
const DB_NAME = process.env.DB_NAME || "mydatabase";
|
|
5
|
+
|
|
6
|
+
if (!MONGO_URI) {
|
|
7
|
+
throw new Error("Please define the MONGO_URI environment variable inside .env");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let cachedClient: MongoClient | null = null;
|
|
11
|
+
let cachedDb: Db | null = null;
|
|
12
|
+
|
|
13
|
+
export async function connectToDatabase(): Promise<{ client: MongoClient; db: Db }> {
|
|
14
|
+
// Return cached connection if it exists
|
|
15
|
+
if (cachedClient && cachedDb) {
|
|
16
|
+
return { client: cachedClient, db: cachedDb };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const client = new MongoClient(MONGO_URI);
|
|
20
|
+
await client.connect();
|
|
21
|
+
const db = client.db(DB_NAME);
|
|
22
|
+
|
|
23
|
+
cachedClient = client;
|
|
24
|
+
cachedDb = db;
|
|
25
|
+
|
|
26
|
+
console.log("MongoDB connected:", DB_NAME);
|
|
27
|
+
|
|
28
|
+
return { client, db };
|
|
29
|
+
}
|
|
@@ -2,15 +2,17 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import express from "express";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import dotenv from "dotenv";
|
|
6
|
+
dotenv.config();
|
|
5
7
|
|
|
6
8
|
// server.js is now in .aaex/server/
|
|
7
|
-
const frameworkRoot = path.resolve(".aaex"); // absolute path to .aaex
|
|
8
9
|
const projectRoot = path.resolve("."); // root of the project
|
|
9
10
|
|
|
10
11
|
// Import BuildApiRoutes
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
import * as BuildApiRoutes from "../BuildApiRoutes.js";
|
|
13
|
+
|
|
14
|
+
const apiRoutes = BuildApiRoutes.default; // default export
|
|
15
|
+
const PathToRoute = BuildApiRoutes.pathToRoute; // named export
|
|
14
16
|
|
|
15
17
|
// Constants
|
|
16
18
|
const isProduction = process.env.NODE_ENV === "production";
|
|
@@ -46,14 +48,16 @@ if (!isProduction) {
|
|
|
46
48
|
);
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
// parse JSON bodies
|
|
52
|
+
app.use(express.json());
|
|
53
|
+
|
|
54
|
+
// parse URL-encoded bodies (optional, for form POSTs)
|
|
55
|
+
app.use(express.urlencoded({ extended: true }));
|
|
56
|
+
|
|
49
57
|
// API routing
|
|
50
58
|
app.use("/api", async (req, res) => {
|
|
51
|
-
console.log(req.path);
|
|
52
|
-
|
|
53
59
|
const routeMatch = PathToRoute(req.path, apiRoutes);
|
|
54
60
|
|
|
55
|
-
console.log(routeMatch);
|
|
56
|
-
|
|
57
61
|
if (!routeMatch)
|
|
58
62
|
return res.status(404).json({ error: "API route not found" });
|
|
59
63
|
|
|
@@ -62,7 +66,11 @@ app.use("/api", async (req, res) => {
|
|
|
62
66
|
|
|
63
67
|
if (!isProduction) {
|
|
64
68
|
// DEV: Vite handles TS/JS loading
|
|
65
|
-
|
|
69
|
+
if (route.filePath.split("/")[0] == ".aaex") {
|
|
70
|
+
modulePath = `${route.filePath.replace(/^src\/api/, "")}`;
|
|
71
|
+
} else {
|
|
72
|
+
modulePath = `/src/api${route.filePath.replace(/^src\/api/, "")}`;
|
|
73
|
+
}
|
|
66
74
|
} else {
|
|
67
75
|
// PROD: bundled JS
|
|
68
76
|
modulePath = pathToFileURL(
|
|
@@ -93,9 +101,11 @@ app.use("/api", async (req, res) => {
|
|
|
93
101
|
});
|
|
94
102
|
|
|
95
103
|
// SSR HTML
|
|
96
|
-
app.use(
|
|
104
|
+
app.use(/.*/, async (req, res) => {
|
|
97
105
|
try {
|
|
98
|
-
|
|
106
|
+
let url = req.originalUrl;
|
|
107
|
+
|
|
108
|
+
console.log("url", url);
|
|
99
109
|
|
|
100
110
|
let template;
|
|
101
111
|
let render;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//credit to https://github.com/vitejs/vite/issues/16515
|
|
2
|
+
|
|
3
|
+
import type {Plugin, ViteDevServer} from 'vite';
|
|
4
|
+
|
|
5
|
+
const virtualCssPath = '/@virtual:ssr-css.css';
|
|
6
|
+
|
|
7
|
+
const collectedStyles = new Map<string, string>();
|
|
8
|
+
|
|
9
|
+
export function pluginSsrDevFoucFix(): Plugin {
|
|
10
|
+
let server: ViteDevServer;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
name: 'ssr-dev-FOUC-fix',
|
|
14
|
+
apply: 'serve',
|
|
15
|
+
transform(code: string, id: string) {
|
|
16
|
+
if (id.includes('node_modules')) return null;
|
|
17
|
+
if (id.includes('.css')) {
|
|
18
|
+
collectedStyles.set(id, code);
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
},
|
|
22
|
+
configureServer(server_) {
|
|
23
|
+
server = server_;
|
|
24
|
+
|
|
25
|
+
server.middlewares.use((req, _res, next) => {
|
|
26
|
+
if (req.url === virtualCssPath) {
|
|
27
|
+
_res.setHeader('Content-Type', 'text/css');
|
|
28
|
+
_res.write(Array.from(collectedStyles.values()).join('\n'));
|
|
29
|
+
_res.end();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
next();
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
transformIndexHtml: {
|
|
37
|
+
handler: async () => {
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
tag: 'link',
|
|
41
|
+
injectTo: 'head',
|
|
42
|
+
attrs: {
|
|
43
|
+
rel: 'stylesheet',
|
|
44
|
+
href: virtualCssPath,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function setCookie(
|
|
2
|
+
name: string,
|
|
3
|
+
value: string,
|
|
4
|
+
days = 7,
|
|
5
|
+
options: {
|
|
6
|
+
path?: string;
|
|
7
|
+
secure?: boolean;
|
|
8
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
9
|
+
domain?: string;
|
|
10
|
+
} = {}
|
|
11
|
+
) {
|
|
12
|
+
const maxAge = days * 24 * 60 * 60;
|
|
13
|
+
const encoded = encodeURIComponent(value);
|
|
14
|
+
|
|
15
|
+
let cookie = `${name}=${encoded}; max-age=${maxAge}; path=${
|
|
16
|
+
options.path ?? "/"
|
|
17
|
+
}`;
|
|
18
|
+
|
|
19
|
+
if (options.secure) cookie += "; secure";
|
|
20
|
+
if (options.sameSite) cookie += `; samesite=${options.sameSite}`;
|
|
21
|
+
if (options.domain) cookie += `; domain=${options.domain}`;
|
|
22
|
+
|
|
23
|
+
document.cookie = cookie;
|
|
24
|
+
}
|
|
25
|
+
export function getCookie(name: string) {
|
|
26
|
+
const cookies = document.cookie.split("; ");
|
|
27
|
+
|
|
28
|
+
for (const c of cookies) {
|
|
29
|
+
const [key, ...rest] = c.split("=");
|
|
30
|
+
if (key === name) {
|
|
31
|
+
return decodeURIComponent(rest.join("="));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cookieToJSON(cookie: string) {
|
|
39
|
+
const parts = cookie.split("; ");
|
|
40
|
+
const result: Record<string, string> = {};
|
|
41
|
+
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
const [key, ...rest] = part.split("=");
|
|
44
|
+
result[key] = rest.join("=");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getCookieAsJSON(name: string) {
|
|
51
|
+
const raw = getCookie(name);
|
|
52
|
+
if (!raw) return null;
|
|
53
|
+
return cookieToJSON(raw);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function deleteCookie(
|
|
57
|
+
name: string,
|
|
58
|
+
options: { path?: string; domain?: string } = {}
|
|
59
|
+
) {
|
|
60
|
+
let cookie = `${name}=; max-age=0; path=${options.path ?? "/"}`;
|
|
61
|
+
if (options.domain) cookie += `; domain=${options.domain}`;
|
|
62
|
+
document.cookie = cookie;
|
|
63
|
+
}
|
package/template/.env
ADDED
package/template/index.html
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<link rel="stylesheet" href="./src/index.css" />
|
|
7
8
|
<title>AaExJS</title>
|
|
8
9
|
<!--app-head-->
|
|
9
10
|
</head>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { getCookie } from "../../.aaex/utils/cookies";
|
|
3
|
+
import { CookieUser } from "../models/User";
|
|
4
|
+
|
|
5
|
+
export function useAuth() {
|
|
6
|
+
const [user, setUser] = useState<CookieUser>({
|
|
7
|
+
username: "",
|
|
8
|
+
email: "",
|
|
9
|
+
id: "",
|
|
10
|
+
});
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [valid, setValid] = useState(false);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
async function checkAuth() {
|
|
16
|
+
const token = getCookie("token");
|
|
17
|
+
if (!token) {
|
|
18
|
+
setLoading(false);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch("/api/auth/validate", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify({ token }),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
|
|
31
|
+
if (data.valid) {
|
|
32
|
+
setValid(true);
|
|
33
|
+
setUser(data.user);
|
|
34
|
+
} else {
|
|
35
|
+
setValid(false);
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
setValid(false);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
checkAuth();
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return { user, valid, loading };
|
|
48
|
+
}
|
package/template/src/index.css
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type User = {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
username: string;
|
|
5
|
+
password: string;
|
|
6
|
+
confirmPass: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type CreateUser = Omit<User, "id">; //should not be edited
|
|
10
|
+
|
|
11
|
+
export type LoginUser = Pick<User, "email" | "password">; //should not be edited
|
|
12
|
+
|
|
13
|
+
export type CookieUser = Omit<User, "password" | "confirmPass">;
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
import { useAuth } from "../hooks/useAuth";
|
|
2
|
+
import { FileLink } from "aaex-file-router";
|
|
3
|
+
import { FileRoutes } from "../routeTypes";
|
|
4
|
+
|
|
1
5
|
export default function Home() {
|
|
2
|
-
|
|
6
|
+
const { user, valid, loading } = useAuth();
|
|
7
|
+
|
|
8
|
+
console.log(user, valid, loading);
|
|
9
|
+
|
|
10
|
+
if (loading) return <p>Checking authentication...</p>;
|
|
11
|
+
if (!valid)
|
|
12
|
+
return (
|
|
13
|
+
<p>
|
|
14
|
+
You are not logged in! <br/> <FileLink<FileRoutes> to="/login">Login</FileLink>
|
|
15
|
+
</p>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return <p>Welcome, {user ? user.username : ""}</p>;
|
|
3
19
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { setCookie } from "../../.aaex/utils/cookies";
|
|
3
|
+
import { LoginUser } from "../models/User";
|
|
4
|
+
|
|
5
|
+
export default function Login() {
|
|
6
|
+
const [fields, setFields] = useState<LoginUser>({
|
|
7
|
+
email: "",
|
|
8
|
+
password: "",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState<string | null>(null);
|
|
13
|
+
const [success, setSuccess] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
16
|
+
const { name, value } = e.target;
|
|
17
|
+
setFields((prev) => ({ ...prev, [name]: value }));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
setError(null);
|
|
23
|
+
setSuccess(null);
|
|
24
|
+
setLoading(true);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch("/api/auth/login", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify(fields),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
setError(data.error || "Invalid credentials");
|
|
37
|
+
} else {
|
|
38
|
+
setSuccess("Logged in!");
|
|
39
|
+
|
|
40
|
+
// Store token + user in cookies
|
|
41
|
+
setCookie("token", data.token);
|
|
42
|
+
setCookie("user", JSON.stringify(data.user));
|
|
43
|
+
|
|
44
|
+
// Optional: redirect
|
|
45
|
+
window.location.href = "/";
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
setError("Server error");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setLoading(false);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div style={styles.page}>
|
|
56
|
+
<form onSubmit={handleSubmit} style={styles.card}>
|
|
57
|
+
<h2 style={styles.title}>Login</h2>
|
|
58
|
+
|
|
59
|
+
<input
|
|
60
|
+
name="email"
|
|
61
|
+
type="email"
|
|
62
|
+
placeholder="Email"
|
|
63
|
+
value={fields.email}
|
|
64
|
+
onChange={handleChange}
|
|
65
|
+
required
|
|
66
|
+
style={styles.input}
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
<input
|
|
70
|
+
name="password"
|
|
71
|
+
type="password"
|
|
72
|
+
placeholder="Password"
|
|
73
|
+
value={fields.password}
|
|
74
|
+
onChange={handleChange}
|
|
75
|
+
required
|
|
76
|
+
style={styles.input}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<button type="submit" disabled={loading} style={styles.button}>
|
|
80
|
+
{loading ? "Logging in..." : "Login"}
|
|
81
|
+
</button>
|
|
82
|
+
|
|
83
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
84
|
+
{success && <div style={styles.success}>{success}</div>}
|
|
85
|
+
</form>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
91
|
+
page: {
|
|
92
|
+
minHeight: "100vh",
|
|
93
|
+
display: "flex",
|
|
94
|
+
alignItems: "center",
|
|
95
|
+
justifyContent: "center",
|
|
96
|
+
padding: "20px",
|
|
97
|
+
},
|
|
98
|
+
card: {
|
|
99
|
+
width: "100%",
|
|
100
|
+
maxWidth: "420px",
|
|
101
|
+
padding: "30px",
|
|
102
|
+
borderRadius: "12px",
|
|
103
|
+
boxShadow: "0 8px 20px rgba(0,0,0,0.06)",
|
|
104
|
+
display: "flex",
|
|
105
|
+
flexDirection: "column",
|
|
106
|
+
gap: "15px",
|
|
107
|
+
},
|
|
108
|
+
title: {
|
|
109
|
+
textAlign: "center",
|
|
110
|
+
marginBottom: "5px",
|
|
111
|
+
fontSize: "1.5rem",
|
|
112
|
+
},
|
|
113
|
+
input: {
|
|
114
|
+
padding: "12px 15px",
|
|
115
|
+
borderRadius: "8px",
|
|
116
|
+
border: "1px solid #ddd",
|
|
117
|
+
fontSize: "1rem",
|
|
118
|
+
outline: "none",
|
|
119
|
+
transition: "all 0.2s",
|
|
120
|
+
},
|
|
121
|
+
button: {
|
|
122
|
+
marginTop: "10px",
|
|
123
|
+
padding: "12px",
|
|
124
|
+
background: "#4f46e5",
|
|
125
|
+
color: "white",
|
|
126
|
+
border: "none",
|
|
127
|
+
fontSize: "1rem",
|
|
128
|
+
borderRadius: "8px",
|
|
129
|
+
cursor: "pointer",
|
|
130
|
+
transition: "0.2s",
|
|
131
|
+
},
|
|
132
|
+
error: {
|
|
133
|
+
marginTop: "10px",
|
|
134
|
+
color: "red",
|
|
135
|
+
textAlign: "center",
|
|
136
|
+
fontSize: "0.9rem",
|
|
137
|
+
},
|
|
138
|
+
success: {
|
|
139
|
+
marginTop: "10px",
|
|
140
|
+
color: "green",
|
|
141
|
+
textAlign: "center",
|
|
142
|
+
fontSize: "0.9rem",
|
|
143
|
+
},
|
|
144
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { CreateUser } from "../models/User";
|
|
3
|
+
|
|
4
|
+
export default function Register() {
|
|
5
|
+
const [fields, setFields] = useState<CreateUser>({
|
|
6
|
+
email: "",
|
|
7
|
+
password: "",
|
|
8
|
+
confirmPass: "",
|
|
9
|
+
username: "",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
const [success, setSuccess] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
17
|
+
const { name, value } = e.target;
|
|
18
|
+
|
|
19
|
+
setFields((prev) => ({
|
|
20
|
+
...prev,
|
|
21
|
+
[name]: value,
|
|
22
|
+
}));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
setError(null);
|
|
28
|
+
setSuccess(null);
|
|
29
|
+
setLoading(true);
|
|
30
|
+
|
|
31
|
+
if (fields.password !== fields.confirmPass) {
|
|
32
|
+
setError("Passwords do not match.");
|
|
33
|
+
setLoading(false);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch("/api/auth/register", {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify(fields),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
setError(data.error || "Unknown error");
|
|
48
|
+
} else {
|
|
49
|
+
setSuccess("Account created successfully!");
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
setError("Server error");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setLoading(false);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div style={styles.page}>
|
|
60
|
+
<form onSubmit={handleSubmit} style={styles.card}>
|
|
61
|
+
<h2 style={styles.title}>Register</h2>
|
|
62
|
+
|
|
63
|
+
<input
|
|
64
|
+
name="email"
|
|
65
|
+
type="email"
|
|
66
|
+
placeholder="Email"
|
|
67
|
+
value={fields.email}
|
|
68
|
+
onChange={handleChange}
|
|
69
|
+
required
|
|
70
|
+
style={styles.input}
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<input
|
|
74
|
+
name="username"
|
|
75
|
+
type="text"
|
|
76
|
+
placeholder="Username"
|
|
77
|
+
value={fields.username}
|
|
78
|
+
onChange={handleChange}
|
|
79
|
+
required
|
|
80
|
+
style={styles.input}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<input
|
|
84
|
+
name="password"
|
|
85
|
+
type="password"
|
|
86
|
+
placeholder="Password"
|
|
87
|
+
value={fields.password}
|
|
88
|
+
onChange={handleChange}
|
|
89
|
+
required
|
|
90
|
+
style={styles.input}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
<input
|
|
94
|
+
name="confirmPass"
|
|
95
|
+
type="password"
|
|
96
|
+
placeholder="Confirm Password"
|
|
97
|
+
value={fields.confirmPass}
|
|
98
|
+
onChange={handleChange}
|
|
99
|
+
required
|
|
100
|
+
style={styles.input}
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
<button type="submit" disabled={loading} style={styles.button}>
|
|
104
|
+
{loading ? "Registering..." : "Register"}
|
|
105
|
+
</button>
|
|
106
|
+
|
|
107
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
108
|
+
{success && <div style={styles.success}>{success}</div>}
|
|
109
|
+
</form>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
115
|
+
page: {
|
|
116
|
+
minHeight: "100vh",
|
|
117
|
+
display: "flex",
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
justifyContent: "center",
|
|
120
|
+
padding: "20px",
|
|
121
|
+
},
|
|
122
|
+
card: {
|
|
123
|
+
width: "100%",
|
|
124
|
+
maxWidth: "420px",
|
|
125
|
+
padding: "30px",
|
|
126
|
+
borderRadius: "12px",
|
|
127
|
+
boxShadow: "0 8px 20px rgba(0,0,0,0.06)",
|
|
128
|
+
display: "flex",
|
|
129
|
+
flexDirection: "column",
|
|
130
|
+
gap: "15px",
|
|
131
|
+
},
|
|
132
|
+
title: {
|
|
133
|
+
textAlign: "center",
|
|
134
|
+
marginBottom: "5px",
|
|
135
|
+
fontSize: "1.5rem",
|
|
136
|
+
},
|
|
137
|
+
input: {
|
|
138
|
+
padding: "12px 15px",
|
|
139
|
+
borderRadius: "8px",
|
|
140
|
+
border: "1px solid #ddd",
|
|
141
|
+
fontSize: "1rem",
|
|
142
|
+
outline: "none",
|
|
143
|
+
transition: "all 0.2s",
|
|
144
|
+
},
|
|
145
|
+
button: {
|
|
146
|
+
marginTop: "10px",
|
|
147
|
+
padding: "12px",
|
|
148
|
+
background: "#4f46e5",
|
|
149
|
+
color: "white",
|
|
150
|
+
border: "none",
|
|
151
|
+
fontSize: "1rem",
|
|
152
|
+
borderRadius: "8px",
|
|
153
|
+
cursor: "pointer",
|
|
154
|
+
transition: "0.2s",
|
|
155
|
+
},
|
|
156
|
+
error: {
|
|
157
|
+
marginTop: "10px",
|
|
158
|
+
color: "red",
|
|
159
|
+
textAlign: "center",
|
|
160
|
+
fontSize: "0.9rem",
|
|
161
|
+
},
|
|
162
|
+
success: {
|
|
163
|
+
marginTop: "10px",
|
|
164
|
+
color: "green",
|
|
165
|
+
textAlign: "center",
|
|
166
|
+
fontSize: "0.9rem",
|
|
167
|
+
},
|
|
168
|
+
};
|
package/template/src/routes.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
//* AUTO GENERATED: DO NOT EDIT
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import Index from './pages/index.tsx';
|
|
4
|
+
import Login from './pages/login.tsx';
|
|
5
|
+
import Register from './pages/register.tsx';
|
|
4
6
|
import type { RouteObject } from 'react-router-dom';
|
|
5
7
|
|
|
6
8
|
const routes: RouteObject[] = [
|
|
7
9
|
{
|
|
8
10
|
"path": "",
|
|
9
11
|
"element": React.createElement(Index)
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "login",
|
|
15
|
+
"element": React.createElement(Login)
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "register",
|
|
19
|
+
"element": React.createElement(Register)
|
|
10
20
|
}
|
|
11
21
|
];
|
|
12
22
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true
|
|
9
|
+
},
|
|
10
|
+
"include": [
|
|
11
|
+
".aaex/api","src/api"
|
|
12
|
+
],
|
|
13
|
+
"exclude": [
|
|
14
|
+
"node_modules",
|
|
15
|
+
"dist"
|
|
16
|
+
]
|
|
17
|
+
}
|
package/template/vite.config.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { defineConfig } from
|
|
2
|
-
import react from
|
|
3
|
-
import { aaexFileRouter } from
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import { aaexFileRouter } from "aaex-file-router/plugin";
|
|
4
|
+
import { pluginSsrDevFoucFix } from "./.aaex/utils/ServerLoadCssImports";
|
|
4
5
|
|
|
5
6
|
// https://vite.dev/config/
|
|
6
7
|
export default defineConfig({
|
|
7
|
-
plugins: [react(),aaexFileRouter()],
|
|
8
|
-
})
|
|
8
|
+
plugins: [react(), aaexFileRouter(), pluginSsrDevFoucFix()],
|
|
9
|
+
});
|