aaex-cli 1.1.0 → 1.4.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/.TODO +4 -0
- package/README.md +4 -3
- package/create-aaex-app.js +9 -2
- package/package.json +11 -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-client.tsx +6 -6
- package/template/.aaex/framework/entry-server.tsx +3 -6
- package/template/.aaex/matchServerRoutes.js +60 -0
- package/template/.aaex/server/server.js +59 -14
- package/template/.aaex/utils/ServerLoadCssImports.ts +51 -0
- package/template/.aaex/utils/cookies.ts +63 -0
- package/template/.env +5 -0
- package/template/index.html +2 -1
- package/template/src/App.tsx +21 -10
- package/template/src/client-routes.ts +33 -0
- 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 +49 -2
- package/template/src/pages/login.tsx +144 -0
- package/template/src/pages/register.tsx +168 -0
- package/template/src/pages/test/[slug].tsx +7 -0
- package/template/src/routeTypes.ts +7 -0
- package/template/src/server-routes.ts +37 -0
- package/template/tsconfig.aaex.json +20 -0
- package/template/vite.config.ts +6 -5
- package/template/src/routes.ts +0 -13
package/.TODO
ADDED
package/README.md
CHANGED
|
@@ -7,13 +7,14 @@ Light weight SSR framework for react with filebased page and api routing. Builds
|
|
|
7
7
|
- File routing using aaex-file-router (can be used seperatly)
|
|
8
8
|
- API routing using hybrid solution only available in the full framework
|
|
9
9
|
- SSR rendering using vites native functions + additional functionality
|
|
10
|
-
-full typescript support (currently only typescript)
|
|
10
|
+
- full typescript support (currently only typescript)
|
|
11
11
|
- all vite plugins that work with ssr should work
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
## Usage
|
|
14
15
|
```sh
|
|
15
16
|
npx create-aaex-app <project-name>
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
## V1.
|
|
19
|
-
Added
|
|
19
|
+
## V1.4
|
|
20
|
+
Added server side data loading
|
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: {
|
|
@@ -72,7 +73,11 @@ async function createPackageJson() {
|
|
|
72
73
|
express: "^5.1.0",
|
|
73
74
|
compression: "^1.8.1",
|
|
74
75
|
sirv: "^3.0.2",
|
|
75
|
-
"aaex-file-router": "^1.
|
|
76
|
+
"aaex-file-router": "^1.5.0",
|
|
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.4.0",
|
|
4
4
|
"description": "Command line interface for creating aaexjs app",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -12,8 +12,18 @@
|
|
|
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",
|
|
19
|
+
"@types/react": "^19.2.7",
|
|
20
|
+
"@types/react-dom": "^19.2.3",
|
|
15
21
|
"@vitejs/plugin-react": "^5.1.1",
|
|
16
22
|
"aaex-file-router": "^1.4.5",
|
|
23
|
+
"bcrypt": "^6.0.0",
|
|
24
|
+
"express": "^5.2.1",
|
|
25
|
+
"jsonwebtoken": "^9.0.3",
|
|
26
|
+
"mongodb": "^7.0.0",
|
|
17
27
|
"react": "^19.2.1",
|
|
18
28
|
"react-dom": "^19.2.1",
|
|
19
29
|
"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
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import "../../src/index.css"
|
|
2
|
-
import { StrictMode
|
|
1
|
+
import "../../src/index.css";
|
|
2
|
+
import { StrictMode } from "react";
|
|
3
3
|
import { hydrateRoot } from "react-dom/client";
|
|
4
|
-
import App from "../../src/App"
|
|
4
|
+
import App from "../../src/App";
|
|
5
5
|
import { BrowserRouter } from "react-router";
|
|
6
6
|
|
|
7
|
+
const initialData = (window as any).__INITIAL_DATA__;
|
|
8
|
+
|
|
7
9
|
hydrateRoot(
|
|
8
10
|
document.getElementById("root") as HTMLElement,
|
|
9
11
|
<StrictMode>
|
|
10
12
|
<BrowserRouter>
|
|
11
|
-
<
|
|
12
|
-
<App />
|
|
13
|
-
</Suspense>
|
|
13
|
+
<App initialData={initialData} />
|
|
14
14
|
</BrowserRouter>
|
|
15
15
|
</StrictMode>
|
|
16
16
|
);
|
|
@@ -3,17 +3,14 @@ import { renderToString } from "react-dom/server";
|
|
|
3
3
|
import { StaticRouter } from "react-router";
|
|
4
4
|
import App from "../../src/App";
|
|
5
5
|
|
|
6
|
-
export function render(_url: string) {
|
|
7
|
-
const url =
|
|
6
|
+
export function render(_url: string, initialData= {}) {
|
|
7
|
+
const url = `${_url}`;
|
|
8
8
|
|
|
9
|
-
// call your SSR function or API here and pass the result as props
|
|
10
9
|
|
|
11
10
|
const html = renderToString(
|
|
12
11
|
<StrictMode>
|
|
13
12
|
<StaticRouter location={url}>
|
|
14
|
-
|
|
15
|
-
<App />
|
|
16
|
-
</Suspense>
|
|
13
|
+
<App initialData={initialData} />
|
|
17
14
|
</StaticRouter>
|
|
18
15
|
</StrictMode>
|
|
19
16
|
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export default function routeMatcher(routes, url) {
|
|
2
|
+
const segments = url.split("/").filter(Boolean); // "test/hej" → ["test", "hej"]
|
|
3
|
+
|
|
4
|
+
return matchLevel(routes, segments);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function matchLevel(routes, segments) {
|
|
8
|
+
for (const route of routes) {
|
|
9
|
+
const result = matchRoute(route, segments);
|
|
10
|
+
|
|
11
|
+
if (result) return result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function matchRoute(route, segments) {
|
|
18
|
+
const isParam = /^:[a-zA-Z0-9_]+$/;
|
|
19
|
+
|
|
20
|
+
const [current, ...rest] = segments;
|
|
21
|
+
|
|
22
|
+
// Root index route
|
|
23
|
+
if (route.path === "" && segments.length === 0) {
|
|
24
|
+
return { route, params: {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Static match
|
|
28
|
+
if (route.path === current) {
|
|
29
|
+
if (rest.length === 0) {
|
|
30
|
+
return { route, params: {} };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Has children → go deeper
|
|
34
|
+
if (route.children) {
|
|
35
|
+
return matchLevel(route.children, rest);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Dynamic match → :slug , :id osv
|
|
40
|
+
if (isParam.test(route.path)) {
|
|
41
|
+
const paramName = route.path.slice(1);
|
|
42
|
+
|
|
43
|
+
if (rest.length === 0) {
|
|
44
|
+
return { route, params: { [paramName]: current } };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Has children → go deeper
|
|
48
|
+
if (route.children) {
|
|
49
|
+
const matchedChild = matchLevel(route.children, rest);
|
|
50
|
+
if (matchedChild) {
|
|
51
|
+
return {
|
|
52
|
+
route: matchedChild.route,
|
|
53
|
+
params: { [paramName]: current, ...matchedChild.params },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
@@ -2,15 +2,18 @@ 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
|
+
let serverRoutes;
|
|
10
11
|
// Import BuildApiRoutes
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
import * as BuildApiRoutes from "../BuildApiRoutes.js";
|
|
13
|
+
import routeMatcher from "../matchServerRoutes.js";
|
|
14
|
+
|
|
15
|
+
const apiRoutes = BuildApiRoutes.default; // default export
|
|
16
|
+
const PathToRoute = BuildApiRoutes.pathToRoute; // named export
|
|
14
17
|
|
|
15
18
|
// Constants
|
|
16
19
|
const isProduction = process.env.NODE_ENV === "production";
|
|
@@ -35,6 +38,7 @@ if (!isProduction) {
|
|
|
35
38
|
root: projectRoot,
|
|
36
39
|
base,
|
|
37
40
|
});
|
|
41
|
+
serverRoutes = (await vite.ssrLoadModule("/src/server-routes.ts")).default;
|
|
38
42
|
app.use(vite.middlewares);
|
|
39
43
|
} else {
|
|
40
44
|
const compression = (await import("compression")).default;
|
|
@@ -44,16 +48,24 @@ if (!isProduction) {
|
|
|
44
48
|
base,
|
|
45
49
|
sirv(path.join(projectRoot, "dist/client"), { extensions: [] })
|
|
46
50
|
);
|
|
51
|
+
|
|
52
|
+
const serverRoutesModule = await import(
|
|
53
|
+
pathToFileURL(path.join(projectRoot, "dist/src/server-routes.js")).href
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
serverRoutes = serverRoutesModule.default;
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
// parse JSON bodies
|
|
60
|
+
app.use(express.json());
|
|
61
|
+
|
|
62
|
+
// parse URL-encoded bodies (optional, for form POSTs)
|
|
63
|
+
app.use(express.urlencoded({ extended: true }));
|
|
64
|
+
|
|
49
65
|
// API routing
|
|
50
66
|
app.use("/api", async (req, res) => {
|
|
51
|
-
console.log(req.path);
|
|
52
|
-
|
|
53
67
|
const routeMatch = PathToRoute(req.path, apiRoutes);
|
|
54
68
|
|
|
55
|
-
console.log(routeMatch);
|
|
56
|
-
|
|
57
69
|
if (!routeMatch)
|
|
58
70
|
return res.status(404).json({ error: "API route not found" });
|
|
59
71
|
|
|
@@ -62,7 +74,11 @@ app.use("/api", async (req, res) => {
|
|
|
62
74
|
|
|
63
75
|
if (!isProduction) {
|
|
64
76
|
// DEV: Vite handles TS/JS loading
|
|
65
|
-
|
|
77
|
+
if (route.filePath.split("/")[0] == ".aaex") {
|
|
78
|
+
modulePath = `${route.filePath.replace(/^src\/api/, "")}`;
|
|
79
|
+
} else {
|
|
80
|
+
modulePath = `/src/api${route.filePath.replace(/^src\/api/, "")}`;
|
|
81
|
+
}
|
|
66
82
|
} else {
|
|
67
83
|
// PROD: bundled JS
|
|
68
84
|
modulePath = pathToFileURL(
|
|
@@ -93,9 +109,24 @@ app.use("/api", async (req, res) => {
|
|
|
93
109
|
});
|
|
94
110
|
|
|
95
111
|
// SSR HTML
|
|
96
|
-
app.use(
|
|
112
|
+
app.use(/.*/, async (req, res) => {
|
|
97
113
|
try {
|
|
98
|
-
|
|
114
|
+
let url = req.originalUrl;
|
|
115
|
+
|
|
116
|
+
const routeMatch = routeMatcher(serverRoutes, url);
|
|
117
|
+
|
|
118
|
+
if (!routeMatch) {
|
|
119
|
+
return res.status(404).send("Not found");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Dynamicly import the file
|
|
123
|
+
const mod = await vite.ssrLoadModule(routeMatch.route.modulePath);
|
|
124
|
+
|
|
125
|
+
// Call load if it exist
|
|
126
|
+
let initialData = {};
|
|
127
|
+
if (mod.load) {
|
|
128
|
+
initialData = await mod.load(routeMatch.params);
|
|
129
|
+
}
|
|
99
130
|
|
|
100
131
|
let template;
|
|
101
132
|
let render;
|
|
@@ -114,11 +145,25 @@ app.use("*all", async (req, res) => {
|
|
|
114
145
|
).render;
|
|
115
146
|
}
|
|
116
147
|
|
|
117
|
-
const rendered = await render(url);
|
|
148
|
+
const rendered = await render(url, initialData);
|
|
149
|
+
|
|
150
|
+
function XSSPrevention(unsafeString) {
|
|
151
|
+
return unsafeString.replace(/</g, "//u003c");
|
|
152
|
+
// .replace(/>/g, ">")
|
|
153
|
+
// .replace(/'/g, "'")
|
|
154
|
+
// .replace(/"/g, """);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const serializedData = JSON.stringify(initialData);
|
|
158
|
+
const safeData = XSSPrevention(serializedData);
|
|
118
159
|
|
|
119
160
|
const html = template
|
|
120
161
|
.replace("<!--app-head-->", rendered.head ?? "")
|
|
121
|
-
.replace("<!--app-html-->", rendered.html ?? "")
|
|
162
|
+
.replace("<!--app-html-->", rendered.html ?? "")
|
|
163
|
+
.replace(
|
|
164
|
+
"<!--initial-data-->",
|
|
165
|
+
`<script>window.__INITIAL_DATA__ = ${safeData}</script>`
|
|
166
|
+
);
|
|
122
167
|
|
|
123
168
|
res.status(200).set({ "Content-Type": "text/html" }).send(html);
|
|
124
169
|
} catch (e) {
|
|
@@ -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>
|
package/template/src/App.tsx
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
import "./App.css";
|
|
2
|
-
import { Route,
|
|
3
|
-
import
|
|
2
|
+
import { Route, Routes } from "react-router";
|
|
3
|
+
import serverRoutes from "./server-routes";
|
|
4
|
+
import { createElement } from "react";
|
|
4
5
|
|
|
5
|
-
function renderRoutes(routesArray:
|
|
6
|
+
function renderRoutes(routesArray: any[], initialData: any) {
|
|
6
7
|
return routesArray.map((route) => {
|
|
7
|
-
|
|
8
|
+
const element = createElement(route.element as any, { ...initialData });
|
|
9
|
+
|
|
10
|
+
if (route.children?.length) {
|
|
8
11
|
return (
|
|
9
|
-
<Route
|
|
10
|
-
{renderRoutes(route.children)}
|
|
12
|
+
<Route path={route.path} element={element}>
|
|
13
|
+
{renderRoutes(route.children, initialData)}
|
|
11
14
|
</Route>
|
|
12
15
|
);
|
|
13
|
-
} else {
|
|
14
|
-
return <Route path={route.path} element={route.element} />;
|
|
15
16
|
}
|
|
17
|
+
|
|
18
|
+
return <Route path={route.path} element={element} />;
|
|
16
19
|
});
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
interface AppProps {
|
|
23
|
+
initialData?: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function App({ initialData }: AppProps) {
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<Routes>{renderRoutes(serverRoutes, initialData)}</Routes>;
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
export default App;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//* AUTO GENERATED: DO NOT EDIT
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import Index from './pages/index.tsx';
|
|
4
|
+
import Login from './pages/login.tsx';
|
|
5
|
+
import Register from './pages/register.tsx';
|
|
6
|
+
import Slug from './pages/test/[slug].tsx';
|
|
7
|
+
import type { RouteObject } from 'react-router-dom';
|
|
8
|
+
|
|
9
|
+
const routes: RouteObject[] = [
|
|
10
|
+
{
|
|
11
|
+
"path": "",
|
|
12
|
+
"element": React.createElement(Index)
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "login",
|
|
16
|
+
"element": React.createElement(Login)
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "register",
|
|
20
|
+
"element": React.createElement(Register)
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "test",
|
|
24
|
+
"children": [
|
|
25
|
+
{
|
|
26
|
+
"path": ":slug",
|
|
27
|
+
"element": React.createElement(Slug)
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export default routes;
|
|
@@ -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,50 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { useAuth } from "../hooks/useAuth.js";
|
|
2
|
+
import { FileLink } from "aaex-file-router";
|
|
3
|
+
import { FileRoutes } from "../routeTypes.js";
|
|
4
|
+
|
|
5
|
+
/** ------- Ideally same type as the API imported from /types or /interfaces ------- */
|
|
6
|
+
type PageData = {
|
|
7
|
+
hello: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type PageError = {
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** -------- */
|
|
15
|
+
export async function load(): Promise<PageData | PageError> {
|
|
16
|
+
const res = await fetch("http://localhost:5173/api/helloworld");
|
|
17
|
+
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
return { error: "internal server error" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const data = await res.json();
|
|
23
|
+
|
|
24
|
+
if (data) {
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { error: "Unknown error" };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function Home({ hello }: PageData) {
|
|
32
|
+
const { user, valid, loading } = useAuth();
|
|
33
|
+
|
|
34
|
+
if (loading) return <p>Checking authentication...</p>;
|
|
35
|
+
if (!valid)
|
|
36
|
+
return (
|
|
37
|
+
<p>
|
|
38
|
+
You are not logged in! <br />
|
|
39
|
+
<FileLink<FileRoutes> to="/login">Login</FileLink>
|
|
40
|
+
</p>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<p>Welcome, {user ? user.username : ""} </p>
|
|
46
|
+
<br />
|
|
47
|
+
<p>Server rendered content: {hello}</p>
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
3
50
|
}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//* AUTO GENERATED: DO NOT EDIT
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import Index from './pages/index.tsx';
|
|
4
|
+
import Login from './pages/login.tsx';
|
|
5
|
+
import Register from './pages/register.tsx';
|
|
6
|
+
import Slug from './pages/test/[slug].tsx';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const serverRoutes: any[] = [
|
|
10
|
+
{
|
|
11
|
+
"path": "",
|
|
12
|
+
"element": Index,
|
|
13
|
+
"modulePath": "C:/Users/tmraa/OneDrive/Dokument/AaExJS-documentation/test-app/src/pages/index.tsx"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "login",
|
|
17
|
+
"element": Login,
|
|
18
|
+
"modulePath": "C:/Users/tmraa/OneDrive/Dokument/AaExJS-documentation/test-app/src/pages/login.tsx"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "register",
|
|
22
|
+
"element": Register,
|
|
23
|
+
"modulePath": "C:/Users/tmraa/OneDrive/Dokument/AaExJS-documentation/test-app/src/pages/register.tsx"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "test",
|
|
27
|
+
"children": [
|
|
28
|
+
{
|
|
29
|
+
"path": ":slug",
|
|
30
|
+
"element": Slug,
|
|
31
|
+
"modulePath": "C:/Users/tmraa/OneDrive/Dokument/AaExJS-documentation/test-app/src/pages/test/[slug].tsx"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export default serverRoutes;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"esModuleInterop": true
|
|
11
|
+
},
|
|
12
|
+
"include": [
|
|
13
|
+
".aaex/api",
|
|
14
|
+
"src/api",
|
|
15
|
+
".aaex/utils",
|
|
16
|
+
"src/server-routes.ts",
|
|
17
|
+
"src/hooks"
|
|
18
|
+
],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
package/template/vite.config.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { defineConfig } from
|
|
2
|
-
import react from
|
|
3
|
-
import {
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import { aaexServerRouter } 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(),
|
|
8
|
-
})
|
|
8
|
+
plugins: [react(), aaexServerRouter(), pluginSsrDevFoucFix()],
|
|
9
|
+
});
|
package/template/src/routes.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
//* AUTO GENERATED: DO NOT EDIT
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import Index from './pages/index.tsx';
|
|
4
|
-
import type { RouteObject } from 'react-router-dom';
|
|
5
|
-
|
|
6
|
-
const routes: RouteObject[] = [
|
|
7
|
-
{
|
|
8
|
-
"path": "",
|
|
9
|
-
"element": React.createElement(Index)
|
|
10
|
-
}
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
export default routes;
|