Updates
This commit is contained in:
parent
ef54906d9a
commit
40dacc0b62
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,3 +42,4 @@ next-env.d.ts
|
||||
/src/db/turboci-admin
|
||||
.backups
|
||||
/secrets
|
||||
.tmp
|
||||
@ -1,11 +0,0 @@
|
||||
import type { BunSQLiteConfig } from "@moduletrace/bun-sqlite/dist/types";
|
||||
|
||||
const BunSQLiteConfig: BunSQLiteConfig = {
|
||||
db_name: "turboci-admin",
|
||||
db_backup_dir: ".backups",
|
||||
db_schema_file_name: "db-schema.ts",
|
||||
db_dir: "./src/db",
|
||||
typedef_file_path: "./src/db/types.ts",
|
||||
};
|
||||
|
||||
export default BunSQLiteConfig;
|
||||
61
docker-compose.yaml
Normal file
61
docker-compose.yaml
Normal file
@ -0,0 +1,61 @@
|
||||
name: turboci-admin
|
||||
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:trixie
|
||||
env_file: .env
|
||||
depends_on:
|
||||
web:
|
||||
condition: service_healthy
|
||||
network_mode: host
|
||||
container_name: turboci-admin-nginx
|
||||
restart: always
|
||||
volumes:
|
||||
- ./docker/services/nginx/conf.d:/etc/nginx/conf.d
|
||||
- ./docker/services/nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
|
||||
web:
|
||||
build:
|
||||
context: ./docker/services/web
|
||||
dockerfile: Dockerfile
|
||||
env_file: .env
|
||||
network_mode: host
|
||||
container_name: turboci-admin-web
|
||||
volumes:
|
||||
- .:/app
|
||||
- /root/.turboci:/root/.turboci
|
||||
expose:
|
||||
- 3772
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "--silent", "--fail", "http://localhost:3772/"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 4s
|
||||
restart: always
|
||||
|
||||
websocket:
|
||||
build:
|
||||
context: ./docker/services/websocket
|
||||
dockerfile: Dockerfile
|
||||
env_file: .env
|
||||
container_name: turboci-admin-websocket
|
||||
hostname: turboci-admin-websocket
|
||||
volumes:
|
||||
- .:/app
|
||||
- /root/.turboci:/root/.turboci
|
||||
network_mode: host
|
||||
restart: always
|
||||
|
||||
cron:
|
||||
build:
|
||||
context: ./docker/services/cron
|
||||
dockerfile: Dockerfile
|
||||
env_file: .env
|
||||
container_name: turboci-admin-cron
|
||||
hostname: turboci-admin-cron
|
||||
volumes:
|
||||
- .:/app
|
||||
- /root/.turboci:/root/.turboci
|
||||
restart: on-failure:10
|
||||
network_mode: host
|
||||
16
docker/services/cron/Dockerfile
Normal file
16
docker/services/cron/Dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
FROM node:lts-trixie
|
||||
|
||||
RUN apt update && apt install -y ca-certificates curl zip unzip wget rsync openssh-client zlib1g
|
||||
|
||||
RUN update-ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN curl -fsSL https://bun.sh/install | bash
|
||||
ENV PATH="/root/.bun/bin:${PATH}"
|
||||
|
||||
COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
14
docker/services/cron/entrypoint.sh
Normal file
14
docker/services/cron/entrypoint.sh
Normal file
@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /app
|
||||
|
||||
if [[ -z "$NODE_ENV" ]]; then
|
||||
echo "NODE_ENV is not set. Defaulting to development."
|
||||
NODE_ENV="development"
|
||||
fi
|
||||
|
||||
if [[ "$NODE_ENV" == "production" ]]; then
|
||||
bun src/cron/index.ts
|
||||
else
|
||||
bun --watch src/cron/index.ts
|
||||
fi
|
||||
30
docker/services/nginx/conf.d/default.conf
Normal file
30
docker/services/nginx/conf.d/default.conf
Normal file
@ -0,0 +1,30 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 20M;
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://localhost:3773;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 60s;
|
||||
|
||||
proxy_buffering off;
|
||||
keepalive_timeout 75s;
|
||||
|
||||
tcp_nodelay on;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3772;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
33
docker/services/nginx/nginx.conf
Normal file
33
docker/services/nginx/nginx.conf
Normal file
@ -0,0 +1,33 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
|
||||
|
||||
#gzip on;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
16
docker/services/web/Dockerfile
Normal file
16
docker/services/web/Dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
FROM node:lts-trixie
|
||||
|
||||
RUN apt update && apt install -y ca-certificates curl zip unzip wget rsync openssh-client zlib1g wget
|
||||
|
||||
RUN update-ca-certificates
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN curl -fsSL https://bun.sh/install | bash
|
||||
ENV PATH="/root/.bun/bin:${PATH}"
|
||||
|
||||
COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
17
docker/services/web/entrypoint.sh
Normal file
17
docker/services/web/entrypoint.sh
Normal file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /app
|
||||
|
||||
rm -rf /app/node_modules
|
||||
rm -rf /app/.next
|
||||
|
||||
bun install
|
||||
npm rebuild better-sqlite3
|
||||
|
||||
if [ $NODE_ENV = "production" ]; then
|
||||
echo "Production Environment"
|
||||
bun next start -p ${PORT}
|
||||
else
|
||||
echo "Development Environment"
|
||||
bun next dev -p ${PORT}
|
||||
fi
|
||||
6
docker/services/websocket/Dockerfile
Normal file
6
docker/services/websocket/Dockerfile
Normal file
@ -0,0 +1,6 @@
|
||||
FROM oven/bun:debian
|
||||
|
||||
COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
14
docker/services/websocket/entrypoint.sh
Normal file
14
docker/services/websocket/entrypoint.sh
Normal file
@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /app
|
||||
|
||||
if [[ -z "$NODE_ENV" ]]; then
|
||||
echo "NODE_ENV is not set. Defaulting to development."
|
||||
NODE_ENV="development"
|
||||
fi
|
||||
|
||||
if [[ "$NODE_ENV" == "production" ]]; then
|
||||
bun src/websocket/index.ts
|
||||
else
|
||||
bun --watch src/websocket/index.ts
|
||||
fi
|
||||
5
next.config.js
Normal file
5
next.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@ -1,8 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
9
nsqlite.config.js
Normal file
9
nsqlite.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
const NSQLiteConfig = {
|
||||
db_name: "turboci-admin",
|
||||
db_backup_dir: ".backups",
|
||||
db_schema_file_name: "db-schema.js",
|
||||
db_dir: "./src/db",
|
||||
typedef_file_path: "./src/db/types.ts",
|
||||
};
|
||||
|
||||
export default NSQLiteConfig;
|
||||
21
package.json
21
package.json
@ -1,24 +1,34 @@
|
||||
{
|
||||
"name": "turboci-admin",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"pm2:dev": "pm2 start --name turboci-web 'bunx next dev' && pm2 start --name turboci-cron 'bun --watch src/cron/index.ts'",
|
||||
"pm2:start": "pm2 start --name turboci-web 'bunx next start' && pm2 start --name turboci-cron 'bun src/cron/index.ts'",
|
||||
"pm2:kill": "pm2 kill",
|
||||
"twui:update": "git submodule update --init twui",
|
||||
"twui:init": "git submodule update twui --init",
|
||||
"twui:update": "git submodule update --remote",
|
||||
"twui:add": "git submodule add https://git.tben.me/Moduletrace/tailwind-ui-library.git twui",
|
||||
"db:schema": "bunx bun-sqlite schema -t",
|
||||
"docker:start": "docker compose down && docker compose up --build -d",
|
||||
"docker:logs": "docker compose logs -f -n 50",
|
||||
"rebuild:better-sqlite3": "npm rebuild better-sqlite3",
|
||||
"chown:root-dir": "sudo chown -R archben:archben .",
|
||||
"build": "next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@moduletrace/bun-sqlite": "^1.0.5",
|
||||
"@moduletrace/datasquirel": "^5.7.57",
|
||||
"@moduletrace/nsqlite": "^1.0.9",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"bun": "^1.3.10",
|
||||
"dayjs": "^1.11.19",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-to-react": "^1.7.0",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.577.0",
|
||||
"mdx": "^0.3.1",
|
||||
"next": "16.1.6",
|
||||
"next": "14^",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"openai": "^6.25.0",
|
||||
"react": "19.2.3",
|
||||
@ -27,7 +37,8 @@
|
||||
"react-responsive-modal": "^7.1.0",
|
||||
"rehype-prism-plus": "^2.0.2",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/mdx": "^16.1.6",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
59
src/components/general/logo.tsx
Normal file
59
src/components/general/logo.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import Link from "next/link";
|
||||
import { useId } from "react";
|
||||
|
||||
export default function Logo() {
|
||||
const mainGradientId = useId();
|
||||
const foldGradientId = useId();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="TurboCI home"
|
||||
className="inline-flex items-center gap-3"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
aria-hidden="true"
|
||||
className="h-10 w-10 shrink-0"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={mainGradientId}
|
||||
x1="9"
|
||||
y1="9"
|
||||
x2="31"
|
||||
y2="34"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0%" stopColor="#9cf0c0" />
|
||||
<stop offset="100%" stopColor="#42d392" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={foldGradientId}
|
||||
x1="22"
|
||||
y1="22"
|
||||
x2="36"
|
||||
y2="40"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0%" stopColor="#2bc67e" />
|
||||
<stop offset="100%" stopColor="#1f8458" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<path
|
||||
d="M35 6H23.6c-2 0-3.8 1-4.8 2.7L7.5 28.3c-1.4 2.5.4 5.7 3.2 5.7h9.1c2 0 3.8-1 4.8-2.7l11.2-19.6C39.6 9.2 37.8 6 35 6Z"
|
||||
fill={`url(#${mainGradientId})`}
|
||||
/>
|
||||
<path
|
||||
d="M23.9 24h13.4c2.8 0 4.6 3.1 3.2 5.6l-4.1 7.1c-1 1.7-2.8 2.7-4.8 2.7H18.2c-2.8 0-4.6-3.1-3.2-5.6l4.1-7.1c1-1.7 2.8-2.7 4.8-2.7Z"
|
||||
fill={`url(#${foldGradientId})`}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="font-display text-[1.02rem] font-semibold tracking-tight text-foreground">
|
||||
TurboCI
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,85 @@
|
||||
import useLoginForm from "@/src/hooks/use-login-form";
|
||||
import Tag from "@/twui/components/elements/Tag";
|
||||
import Form from "@/twui/components/form/Form";
|
||||
import Input from "@/twui/components/form/Input";
|
||||
import Button from "@/twui/components/layout/Button";
|
||||
import Stack from "@/twui/components/layout/Stack";
|
||||
import z from "zod";
|
||||
|
||||
export default function LoginForm() {
|
||||
return <Stack></Stack>;
|
||||
const { loading, setLoginData, submitLogin, alert } = useLoginForm();
|
||||
|
||||
return (
|
||||
<Form
|
||||
className="w-full"
|
||||
submitHandler={() => {
|
||||
submitLogin();
|
||||
}}
|
||||
>
|
||||
<Stack className="w-full items-stretch gap-6">
|
||||
{alert?.text ? (
|
||||
<Tag
|
||||
color="error"
|
||||
variant="outlined"
|
||||
className="py-2 px-6 opacity-70"
|
||||
>
|
||||
{alert.text}
|
||||
</Tag>
|
||||
) : null}
|
||||
|
||||
<Input
|
||||
placeholder="Email Address or Username"
|
||||
title="Email/Username"
|
||||
changeHandler={(v) => {
|
||||
const email_schema = z.email();
|
||||
const is_email = email_schema.safeParse(v);
|
||||
|
||||
setLoginData((prev) => ({
|
||||
...prev,
|
||||
email: is_email.success ? v : undefined,
|
||||
username: is_email.success ? undefined : v,
|
||||
}));
|
||||
}}
|
||||
validity={
|
||||
alert?.field_name == "email-username"
|
||||
? {
|
||||
isValid: false,
|
||||
msg: alert?.text,
|
||||
}
|
||||
: {
|
||||
isValid: true,
|
||||
}
|
||||
}
|
||||
showLabel
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="Password"
|
||||
title="Password"
|
||||
type="password"
|
||||
changeHandler={(v) => {
|
||||
setLoginData((prev) => ({
|
||||
...prev,
|
||||
password: v,
|
||||
}));
|
||||
}}
|
||||
validity={
|
||||
alert?.field_name == "password"
|
||||
? {
|
||||
isValid: false,
|
||||
msg: alert?.text,
|
||||
}
|
||||
: {
|
||||
isValid: true,
|
||||
}
|
||||
}
|
||||
showLabel
|
||||
/>
|
||||
|
||||
<Button title="Login" loading={loading} onClick={submitLogin}>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>{" "}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import Paper from "@/twui/components/elements/Paper";
|
||||
import H2 from "@/twui/components/layout/H2";
|
||||
import Stack from "@/twui/components/layout/Stack";
|
||||
import LoginForm from "./(partials)/login-form";
|
||||
import Span from "@/twui/components/layout/Span";
|
||||
|
||||
export default function Main() {
|
||||
return (
|
||||
<Paper>
|
||||
<Stack>
|
||||
<H2>Home</H2>
|
||||
<Stack className="w-full items-center max-w-lg gap-10">
|
||||
<Stack className="gap-4">
|
||||
<H2>Login</H2>
|
||||
<Span>Welcome Back</Span>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<LoginForm />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
18
src/components/pages/auth/signup/(hooks)/use-signup-form.ts
Normal file
18
src/components/pages/auth/signup/(hooks)/use-signup-form.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { TurboCISignupFormObject } from "@/src/types";
|
||||
import useStatus from "@/twui/components/hooks/useStatus";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function useSignupForm() {
|
||||
const [newUser, setNewUser] = useState<TurboCISignupFormObject>({});
|
||||
const { loading, setLoading } = useStatus();
|
||||
const [isPasswordConfirmed, setIsPasswordConfirmed] = useState(false);
|
||||
|
||||
return {
|
||||
newUser,
|
||||
setNewUser,
|
||||
loading,
|
||||
setLoading,
|
||||
isPasswordConfirmed,
|
||||
setIsPasswordConfirmed,
|
||||
};
|
||||
}
|
||||
133
src/components/pages/auth/signup/(partials)/signup-form.tsx
Normal file
133
src/components/pages/auth/signup/(partials)/signup-form.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import Input from "@/twui/components/form/Input";
|
||||
import Button from "@/twui/components/layout/Button";
|
||||
import Stack from "@/twui/components/layout/Stack";
|
||||
import useSignupForm from "../(hooks)/use-signup-form";
|
||||
import fetchApi from "@/twui/components/utils/fetch/fetchApi";
|
||||
import { APIReqObject } from "@/src/types";
|
||||
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function SignupForm() {
|
||||
const {
|
||||
newUser,
|
||||
setNewUser,
|
||||
loading,
|
||||
setLoading,
|
||||
isPasswordConfirmed,
|
||||
setIsPasswordConfirmed,
|
||||
} = useSignupForm();
|
||||
|
||||
const is_password_valid = Boolean(
|
||||
isPasswordConfirmed &&
|
||||
Boolean(newUser.password?.match(/./)) &&
|
||||
Boolean(newUser.confirmed_password?.match(/./)),
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack className="w-full items-stretch">
|
||||
<Input
|
||||
placeholder="Eg. John"
|
||||
title="First Name"
|
||||
changeHandler={(v) => {
|
||||
setNewUser((prev) => ({
|
||||
...prev,
|
||||
first_name: v,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
<Input
|
||||
placeholder="Eg. Doe"
|
||||
title="Last Name"
|
||||
changeHandler={(v) => {
|
||||
setNewUser((prev) => ({
|
||||
...prev,
|
||||
last_name: v,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
<Input
|
||||
placeholder="Email Address or Username"
|
||||
title="Email/Username"
|
||||
type="email"
|
||||
changeHandler={(v) => {
|
||||
setNewUser((prev) => ({
|
||||
...prev,
|
||||
email: v,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
title="Password"
|
||||
type="password"
|
||||
changeHandler={(v) => {
|
||||
setNewUser((prev) => ({
|
||||
...prev,
|
||||
password: v,
|
||||
}));
|
||||
}}
|
||||
validity={{
|
||||
isValid:
|
||||
!Boolean(newUser.password?.match(/./)) ||
|
||||
!Boolean(newUser.confirmed_password?.match(/./))
|
||||
? true
|
||||
: is_password_valid,
|
||||
msg: `Passwords don't match`,
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
<Input
|
||||
placeholder="Confirm Password"
|
||||
title="Confirm Password"
|
||||
type="password"
|
||||
changeHandler={(v) => {
|
||||
setNewUser((prev) => ({
|
||||
...prev,
|
||||
confirmed_password: v,
|
||||
}));
|
||||
|
||||
setIsPasswordConfirmed(v == newUser.password);
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
<Button
|
||||
title="Login"
|
||||
onClick={() => {
|
||||
if (!is_password_valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm(`Create Super Admin Account?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
fetchApi<APIReqObject, APIResponseObject>(
|
||||
`/api/auth/signup`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
new_user: newUser,
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((res) => {
|
||||
console.log("res", res);
|
||||
|
||||
if (res.success) {
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.finally(() => {});
|
||||
}}
|
||||
loading={loading}
|
||||
>
|
||||
Signup
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
12
src/components/pages/auth/signup/index.tsx
Normal file
12
src/components/pages/auth/signup/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import H2 from "@/twui/components/layout/H2";
|
||||
import Stack from "@/twui/components/layout/Stack";
|
||||
import SignupForm from "./(partials)/signup-form";
|
||||
|
||||
export default function Main() {
|
||||
return (
|
||||
<Stack className="w-full items-center max-w-lg">
|
||||
<H2>Create Super Admin Account</H2>
|
||||
<SignupForm />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -4,7 +4,9 @@ import cronCheckServices from "./functions/check-services";
|
||||
while (true) {
|
||||
console.log(`Running Cron Services ...`);
|
||||
|
||||
await cronCheckServices();
|
||||
try {
|
||||
await cronCheckServices();
|
||||
} catch (error) {}
|
||||
|
||||
await Bun.sleep(AppData["CronInterval"]);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export const AppData = {
|
||||
TerminalBinName: "ttyd",
|
||||
AppName: "turboci-admin",
|
||||
CronInterval: 30000,
|
||||
max_instances: 200,
|
||||
max_clusters: 1000,
|
||||
@ -15,4 +16,8 @@ export const AppData = {
|
||||
private_server_batch_exec_size: 50,
|
||||
ssh_max_tries: 50,
|
||||
ssh_try_timeout_milliseconds: 5000,
|
||||
|
||||
AuthCookieName: `x-turboci-admin-auth-key`,
|
||||
AuthCSRFCookieName: `x-turboci-admin-csrf`,
|
||||
CookieExpirationTime: 1000 * 60 * 60 * 24 * 7, // One Week
|
||||
} as const;
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import type { BUN_SQLITE_DatabaseSchemaType } from "@moduletrace/bun-sqlite/dist/types";
|
||||
// @ts-check
|
||||
|
||||
const schema: BUN_SQLITE_DatabaseSchemaType = {
|
||||
/**
|
||||
* @type {import("@moduletrace/nsqlite/dist/types").NSQLITE_DatabaseSchemaType}
|
||||
*/
|
||||
const schema = {
|
||||
dbName: "test-db",
|
||||
tables: [
|
||||
{
|
||||
@ -18,10 +21,22 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
|
||||
fieldName: "email",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "username",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "image",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "password",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "is_super_admin",
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -1,8 +1,8 @@
|
||||
export const BunSQLiteTables = [
|
||||
export const NSQLiteTables = [
|
||||
"users",
|
||||
] as const
|
||||
|
||||
export type BUN_SQLITE_TEST_DB_USERS = {
|
||||
export type NSQLITE_TEST_DB_USERS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
@ -18,7 +18,10 @@ export type BUN_SQLITE_TEST_DB_USERS = {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
image?: string;
|
||||
password?: string;
|
||||
is_super_admin?: number;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_TEST_DB_ALL_TYPEDEFS = BUN_SQLITE_TEST_DB_USERS
|
||||
export type NSQLITE_TEST_DB_ALL_TYPEDEFS = NSQLITE_TEST_DB_USERS
|
||||
10
src/exports/client-exports.ts
Normal file
10
src/exports/client-exports.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import dsqlClient from "@moduletrace/datasquirel/dist/client";
|
||||
import EJSON from "@moduletrace/datasquirel/dist/package-shared/utils/ejson";
|
||||
import slugify from "@moduletrace/datasquirel/dist/package-shared/utils/slugify";
|
||||
import numberfy from "@moduletrace/datasquirel/dist/package-shared/utils/numberfy";
|
||||
const _n = numberfy;
|
||||
const serializeQuery = dsqlClient.utils.serializeQuery;
|
||||
const deserializeQuery = dsqlClient.utils.deserializeQuery;
|
||||
|
||||
export default dsqlClient;
|
||||
export { EJSON, slugify, numberfy, _n, serializeQuery, deserializeQuery };
|
||||
74
src/functions/pages/admin/default-admin-props.ts
Normal file
74
src/functions/pages/admin/default-admin-props.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { EJSON } from "@/src/exports/client-exports";
|
||||
import { PagePropsType, URLQueryType, User } from "@/src/types";
|
||||
import parsePageUrl from "@/src/utils/parse-page-url";
|
||||
import userAuth from "@/src/utils/user-auth";
|
||||
import _ from "lodash";
|
||||
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
|
||||
|
||||
type PropsFnParams = {
|
||||
user: User;
|
||||
props?: PagePropsType;
|
||||
query?: URLQueryType;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
ctx: GetServerSidePropsContext;
|
||||
props?: PagePropsType;
|
||||
propsFn?: (
|
||||
params: PropsFnParams,
|
||||
) => Promise<PagePropsType | false | string>;
|
||||
};
|
||||
|
||||
export default async function defaultAdminProps({
|
||||
ctx,
|
||||
props,
|
||||
propsFn,
|
||||
}: Params): Promise<GetServerSidePropsResult<PagePropsType>> {
|
||||
const { req, query, res } = ctx;
|
||||
const { singleRes: user } = await userAuth({ req });
|
||||
|
||||
if (!user?.id) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/auth/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const propsFnProps = propsFn
|
||||
? await propsFn?.({ user, query, props })
|
||||
: undefined;
|
||||
|
||||
if (typeof propsFnProps == "boolean" && !propsFnProps) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/admin",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof propsFnProps == "string") {
|
||||
return {
|
||||
redirect: {
|
||||
destination: propsFnProps,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const finalAdminUrl = parsePageUrl(req.url, true);
|
||||
|
||||
const defaultPageProps: PagePropsType = {
|
||||
query,
|
||||
user,
|
||||
pageUrl: finalAdminUrl,
|
||||
};
|
||||
|
||||
let finalProps = _.merge(props, propsFnProps, defaultPageProps);
|
||||
|
||||
return {
|
||||
props: { ...finalProps },
|
||||
};
|
||||
}
|
||||
73
src/hooks/use-login-form.ts
Normal file
73
src/hooks/use-login-form.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import useStatus from "@/twui/components/hooks/useStatus";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { AlertObject, APIReqObject, LoginFormData } from "../types";
|
||||
import fetchApi from "@moduletrace/datasquirel/dist/client/fetch";
|
||||
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
import z, { ZodError } from "zod";
|
||||
import { EJSON } from "../exports/client-exports";
|
||||
|
||||
export default function useLoginForm() {
|
||||
const [loginData, setLoginData] = useState<LoginFormData>({});
|
||||
const [alert, setAlert] = useState<AlertObject>();
|
||||
const { loading, setLoading } = useStatus();
|
||||
|
||||
useEffect(() => {
|
||||
setAlert(undefined);
|
||||
}, [loginData]);
|
||||
|
||||
const submitLogin = useCallback(async () => {
|
||||
if (!loginData.email && !loginData.username) {
|
||||
setAlert({ text: `Please Enter a username or email` });
|
||||
return;
|
||||
}
|
||||
|
||||
const password_schema = z.string().min(6);
|
||||
const is_password = password_schema.safeParse(loginData.password || "");
|
||||
|
||||
if (!is_password.success) {
|
||||
const error_text =
|
||||
`Invalid Password: ` +
|
||||
(EJSON.parse(is_password.error.message) as ZodError[])?.[0]
|
||||
.message;
|
||||
|
||||
setAlert({
|
||||
text: error_text,
|
||||
field_name: "password",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetchApi<APIReqObject, APIResponseObject>(
|
||||
`/api/auth/login`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
...loginData,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
setAlert({ text: res.msg || `Login Failed` });
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [loginData]);
|
||||
|
||||
return {
|
||||
loginData,
|
||||
setLoginData,
|
||||
loading,
|
||||
setLoading,
|
||||
submitLogin,
|
||||
alert,
|
||||
setAlert,
|
||||
};
|
||||
}
|
||||
17
src/layouts/admin/(data)/links.ts
Normal file
17
src/layouts/admin/(data)/links.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { TWUI_LINK_LIST_LINK_OBJECT } from "@/twui/components/elements/LinkList";
|
||||
|
||||
export const AdminAsideLinks: (
|
||||
| TWUI_LINK_LIST_LINK_OBJECT
|
||||
| TWUI_LINK_LIST_LINK_OBJECT[]
|
||||
| undefined
|
||||
)[] = [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/admin",
|
||||
strict: true,
|
||||
},
|
||||
{
|
||||
title: "Deployments",
|
||||
url: "/admin/deployments",
|
||||
},
|
||||
];
|
||||
22
src/layouts/admin/header.tsx
Normal file
22
src/layouts/admin/header.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import Logo from "@/src/components/general/logo";
|
||||
import Row from "@/twui/components/layout/Row";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
type Props = PropsWithChildren & {};
|
||||
|
||||
export default function Header({ children }: Props) {
|
||||
return (
|
||||
<header className="col-span-6">
|
||||
<Row className="w-full grid grid-cols-6 grid-frame nested-grid-frame">
|
||||
<Row className="h-full items-stretch grid-cell col-span-1 w-full justify-between">
|
||||
<Row className="px-4">
|
||||
<Logo />
|
||||
</Row>
|
||||
{/* <Divider vertical className="-mr-[7px]" /> */}
|
||||
</Row>
|
||||
<Row className="grid-cell col-span-4"></Row>
|
||||
<Row className="grid-cell col-span-1"></Row>
|
||||
</Row>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
29
src/layouts/admin/index.tsx
Normal file
29
src/layouts/admin/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import LinkList from "@/twui/components/elements/LinkList";
|
||||
import Main from "@/twui/components/layout/Main";
|
||||
import Row from "@/twui/components/layout/Row";
|
||||
import Stack from "@/twui/components/layout/Stack";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { AdminAsideLinks } from "./(data)/links";
|
||||
import Header from "./header";
|
||||
|
||||
type Props = PropsWithChildren & {};
|
||||
|
||||
export default function Layout({ children }: Props) {
|
||||
return (
|
||||
<Main className="w-screen h-screen overflow-hidden p-4 lg:p-10">
|
||||
<div className="grid-frame grid-cols-6 w-full h-full grid-rows-[64px_1fr]">
|
||||
<Header />
|
||||
<Stack className="grid-cell col-span-1">
|
||||
<LinkList
|
||||
links={AdminAsideLinks}
|
||||
className="w-full flex-col"
|
||||
linkProps={{
|
||||
className: "turboci-admin-aside-link",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack className="grid-cell col-span-5">{children}</Stack>
|
||||
</div>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,12 @@
|
||||
import Logo from "@/src/components/general/logo";
|
||||
import Center from "@/twui/components/layout/Center";
|
||||
import Container from "@/twui/components/layout/Container";
|
||||
import Divider from "@/twui/components/layout/Divider";
|
||||
import Main from "@/twui/components/layout/Main";
|
||||
import Row from "@/twui/components/layout/Row";
|
||||
import Section from "@/twui/components/layout/Section";
|
||||
import Spacer from "@/twui/components/layout/Spacer";
|
||||
import Stack from "@/twui/components/layout/Stack";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
type Props = PropsWithChildren & {};
|
||||
@ -7,7 +14,26 @@ type Props = PropsWithChildren & {};
|
||||
export default function Layout({ children }: Props) {
|
||||
return (
|
||||
<Main className="w-screen h-screen overflow-hidden">
|
||||
<Center>{children}</Center>
|
||||
<Section className="w-full h-full">
|
||||
<Container className="grid-frame section-grid grid-cols-1 h-full">
|
||||
<Stack className="w-full justify-between h-full">
|
||||
<Stack className="gap-0">
|
||||
<Row>
|
||||
<Row className="p-6">
|
||||
<Logo />
|
||||
</Row>
|
||||
<Divider vertical />
|
||||
</Row>
|
||||
<Divider />
|
||||
</Stack>
|
||||
<Center className="p-10">{children}</Center>
|
||||
<Stack>
|
||||
<Divider />
|
||||
<Spacer className="h-20 w-full" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import "@/src/styles/globals.css";
|
||||
|
||||
import type { AppProps } from "next/app";
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
|
||||
18
src/pages/admin/index.tsx
Normal file
18
src/pages/admin/index.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import Main from "@/src/components/pages/home";
|
||||
import defaultAdminProps from "@/src/functions/pages/admin/default-admin-props";
|
||||
import Layout from "@/src/layouts/admin";
|
||||
import { GetServerSideProps } from "next";
|
||||
|
||||
export default function AdminDashboard() {
|
||||
return (
|
||||
<Layout>
|
||||
<Main />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
return await defaultAdminProps({
|
||||
ctx,
|
||||
});
|
||||
};
|
||||
37
src/pages/api/auth/login.ts
Normal file
37
src/pages/api/auth/login.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { APIReqObject } from "@/src/types";
|
||||
import loginUser from "@/src/utils/login-user";
|
||||
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<APIResponseObject>,
|
||||
) {
|
||||
try {
|
||||
if (req.method !== "POST") {
|
||||
return res.json({
|
||||
success: false,
|
||||
msg: `Wrong Method`,
|
||||
});
|
||||
}
|
||||
|
||||
const { email, username, password } = req.body as APIReqObject;
|
||||
|
||||
if (typeof password !== "string" || !password?.match(/./)) {
|
||||
throw new Error(`Password is required!`);
|
||||
}
|
||||
|
||||
const logged_in_user = await loginUser({
|
||||
res,
|
||||
email_or_username: email || username,
|
||||
password,
|
||||
});
|
||||
|
||||
return res.json(logged_in_user);
|
||||
} catch (error: any) {
|
||||
return res.json({
|
||||
success: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
103
src/pages/api/auth/signup.ts
Normal file
103
src/pages/api/auth/signup.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { NSQLITE_TEST_DB_USERS, NSQLiteTables } from "@/src/db/types";
|
||||
import { APIReqObject } from "@/src/types";
|
||||
import loginUser from "@/src/utils/login-user";
|
||||
import hashPassword from "@moduletrace/datasquirel/dist/package-shared/functions/dsql/hashPassword";
|
||||
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
import NSQLite from "@moduletrace/nsqlite";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<APIResponseObject>,
|
||||
) {
|
||||
try {
|
||||
if (req.method !== "POST") {
|
||||
return res.json({
|
||||
success: false,
|
||||
msg: `Wrong Method`,
|
||||
});
|
||||
}
|
||||
|
||||
const { new_user } = req.body as APIReqObject;
|
||||
|
||||
if (!new_user) {
|
||||
throw new Error(`No new User Object Passed!`);
|
||||
}
|
||||
|
||||
if (!new_user.password) {
|
||||
throw new Error(`New User Password is required.`);
|
||||
}
|
||||
|
||||
const existing_users_res = await NSQLite.select<
|
||||
NSQLITE_TEST_DB_USERS,
|
||||
(typeof NSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
});
|
||||
|
||||
if (existing_users_res.payload?.[0]?.id) {
|
||||
return res.json({
|
||||
success: false,
|
||||
msg: `Super Admin User already exists. Other Users can be created by this user.`,
|
||||
});
|
||||
}
|
||||
|
||||
const { first_name, email, last_name, password } = new_user;
|
||||
|
||||
const new_user_password = hashPassword({ password });
|
||||
|
||||
const new_user_insert_res = await NSQLite.insert<
|
||||
NSQLITE_TEST_DB_USERS,
|
||||
(typeof NSQLiteTables)[number]
|
||||
>({
|
||||
data: [
|
||||
{
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
password: new_user_password,
|
||||
is_super_admin: 1,
|
||||
},
|
||||
],
|
||||
table: "users",
|
||||
});
|
||||
|
||||
console.log("new_user_insert_res", new_user_insert_res);
|
||||
|
||||
const new_user_id = new_user_insert_res.postInsertReturn?.insertId;
|
||||
|
||||
if (!new_user_id) {
|
||||
throw new Error(`Couldn't create New User.`);
|
||||
}
|
||||
|
||||
const newly_inserted_user_res = await NSQLite.select<
|
||||
NSQLITE_TEST_DB_USERS,
|
||||
(typeof NSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
query: {
|
||||
query: {
|
||||
is_super_admin: { value: "1" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newly_inserted_user = newly_inserted_user_res.singleRes;
|
||||
|
||||
if (!newly_inserted_user?.id) {
|
||||
throw new Error(`Couldn't Find Newly inserted user.`);
|
||||
}
|
||||
|
||||
const logged_in_user = await loginUser({
|
||||
res,
|
||||
user_id: newly_inserted_user.id,
|
||||
});
|
||||
|
||||
return res.json(logged_in_user);
|
||||
} catch (error: any) {
|
||||
return res.json({
|
||||
success: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
import Main from "@/src/components/pages/auth/login";
|
||||
import Layout from "@/src/layouts/login";
|
||||
import userAuth from "@/src/utils/user-auth";
|
||||
import NSQLite from "@moduletrace/nsqlite";
|
||||
import { GetServerSideProps } from "next";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
@ -8,3 +11,31 @@ export default function LoginPage() {
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const users = await NSQLite.select({ table: "users" });
|
||||
|
||||
if (!users.payload?.[0]) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth/signup`,
|
||||
statusCode: 307,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { singleRes: user } = await userAuth({ req: ctx.req });
|
||||
|
||||
if (user?.logged_in_status) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/admin`,
|
||||
statusCode: 307,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
};
|
||||
|
||||
29
src/pages/auth/signup.tsx
Normal file
29
src/pages/auth/signup.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import Main from "@/src/components/pages/auth/signup";
|
||||
import Layout from "@/src/layouts/login";
|
||||
import NSQLite from "@moduletrace/nsqlite";
|
||||
import { GetServerSideProps } from "next";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<Main />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const users = await NSQLite.select({ table: "users" });
|
||||
|
||||
if (users.payload?.[0]) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth/login`,
|
||||
statusCode: 307,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
import Main from "@/src/components/pages/home";
|
||||
import Layout from "@/src/layouts/login";
|
||||
import { GetServerSideProps } from "next";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
@ -8,3 +9,12 @@ export default function Home() {
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async () => {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/admin",
|
||||
statusCode: 307,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
31
src/server/index.ts
Normal file
31
src/server/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { createServer } from "http";
|
||||
import next from "next";
|
||||
|
||||
const port = parseInt(process.env.PORT || "3000", 10);
|
||||
const dev = process.env.NODE_ENV !== "production";
|
||||
const app = next({ dev });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
app.prepare().then(() => {
|
||||
createServer((req, res) => {
|
||||
if (!req.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const full_href = `${process.env.NEXT_PUBLIC_HOST}${req.url}`;
|
||||
|
||||
const url = new URL(full_href);
|
||||
|
||||
if (url.pathname.match(/^\/api\//)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handle(req, res);
|
||||
}).listen(port);
|
||||
|
||||
console.log(
|
||||
`> Server listening at http://localhost:${port} as ${
|
||||
dev ? "development" : process.env.NODE_ENV
|
||||
}`,
|
||||
);
|
||||
});
|
||||
@ -1,13 +1,87 @@
|
||||
@import "../../twui/components/base.css";
|
||||
|
||||
@theme inline {
|
||||
--breakpoint-xs: 350px;
|
||||
--font-sans: var(--font-body);
|
||||
--font-display: var(--font-space-grotesk);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
|
||||
--color-background-dark: #05070b;
|
||||
--color-foreground-dark: #f4f7fb;
|
||||
--color-background-light: #ffffff;
|
||||
--color-foreground-light: #171717;
|
||||
--color-background-dark: #0c0e11;
|
||||
--color-foreground-dark: #ededed;
|
||||
|
||||
--color-price: #16946a;
|
||||
--color-price-dark: #77ceb1;
|
||||
--color-dark: #000000;
|
||||
|
||||
--color-primary: #3ecf8e;
|
||||
--color-primary-hover: #3ecf8e;
|
||||
--color-primary-outline: #3ecf8e;
|
||||
--color-primary-text: #000000;
|
||||
--color-primary-dark: #3ecf8e;
|
||||
--color-primary-dark-hover: #3ecf8e;
|
||||
--color-primary-dark-outline: #3ecf8e;
|
||||
--color-primary-dark-text: #000000;
|
||||
|
||||
--radius-default: 3px;
|
||||
}
|
||||
|
||||
.grid-frame {
|
||||
@apply grid bg-foreground-light/10 dark:bg-foreground-dark/10;
|
||||
@apply gap-px p-px;
|
||||
}
|
||||
|
||||
.grid-frame.nested-grid-frame {
|
||||
@apply grid bg-transparent! dark:bg-transparent!;
|
||||
@apply gap-px p-0! h-full;
|
||||
}
|
||||
|
||||
.grid-frame.nested-grid-frame .grid-cell {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
.grid-frame > .grid-cell {
|
||||
@apply bg-background-light dark:bg-background-dark;
|
||||
}
|
||||
|
||||
.grid-cell-content {
|
||||
@apply p-10;
|
||||
}
|
||||
|
||||
.twui-button,
|
||||
.twui-button .twui-button-content-wrapper {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
.twui-section {
|
||||
@apply p-0!;
|
||||
}
|
||||
|
||||
.twui-stack {
|
||||
@apply w-full items-stretch;
|
||||
}
|
||||
|
||||
.twui-button .twui-loading .text-gray {
|
||||
@apply text-primary!;
|
||||
}
|
||||
|
||||
.twui-button-primary .twui-loading .fill-primary {
|
||||
@apply text-primary-text;
|
||||
}
|
||||
|
||||
.twui-h1,
|
||||
.twui-h2,
|
||||
.twui-h3 {
|
||||
@apply m-0!;
|
||||
}
|
||||
|
||||
.turboci-admin-aside-link {
|
||||
@apply w-full border-foreground-light/10 dark:border-r-foreground-dark/10 py-4 px-6;
|
||||
@apply text-foreground-light;
|
||||
}
|
||||
|
||||
.turboci-admin-aside-link.active {
|
||||
@apply bg-foreground-light/5 dark:bg-foreground-dark/5;
|
||||
}
|
||||
|
||||
.turboci-admin-aside-link.active * {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
import { DATASQUIREL_LoggedInUser } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
|
||||
export type User = DATASQUIREL_LoggedInUser & {};
|
||||
|
||||
export const CloudProviders = [
|
||||
{
|
||||
title: "Hetzner",
|
||||
@ -164,3 +168,36 @@ export type ServiceScriptObject = {
|
||||
deployment_name: string;
|
||||
work_dir?: string;
|
||||
};
|
||||
|
||||
export type PagePropsType = {};
|
||||
export type URLQueryType = {};
|
||||
|
||||
export type APIReqObject = {
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
new_user?: TurboCISignupFormObject;
|
||||
};
|
||||
|
||||
export type LoginFormData = {
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export type AlertObject = {
|
||||
text?: string;
|
||||
field_name?: string;
|
||||
};
|
||||
|
||||
export type WebSocketData = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export type TurboCISignupFormObject = {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
confirmed_password?: string;
|
||||
};
|
||||
|
||||
93
src/utils/cookies-actions.ts
Normal file
93
src/utils/cookies-actions.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { CookieOptions } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
import dayjs, { type Dayjs } from "dayjs";
|
||||
import * as http from "http";
|
||||
import { NextApiResponse } from "next";
|
||||
|
||||
type FinalCookieOpts = Omit<CookieOptions, "expires"> & { expires?: Dayjs };
|
||||
type Cookie = { name: string; value: string; options: FinalCookieOpts };
|
||||
|
||||
export function setCookie(
|
||||
res: http.ServerResponse | NextApiResponse,
|
||||
cookies: Cookie[],
|
||||
): void {
|
||||
const AllCookieParts: string[][] = [];
|
||||
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i];
|
||||
const { name, options, value } = cookie;
|
||||
|
||||
const cookieParts: string[] = [
|
||||
`${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
|
||||
];
|
||||
|
||||
if (options.expires) {
|
||||
cookieParts.push(
|
||||
`Expires=${options.expires.toDate().toUTCString()}`,
|
||||
);
|
||||
}
|
||||
if (options.maxAge !== undefined) {
|
||||
cookieParts.push(`Max-Age=${options.maxAge}`);
|
||||
}
|
||||
if (options.path) {
|
||||
cookieParts.push(`Path=${options.path}`);
|
||||
}
|
||||
if (options.domain) {
|
||||
cookieParts.push(`Domain=${options.domain}`);
|
||||
}
|
||||
if (options.secure) {
|
||||
cookieParts.push("Secure");
|
||||
}
|
||||
if (options.httpOnly) {
|
||||
cookieParts.push("HttpOnly");
|
||||
}
|
||||
|
||||
AllCookieParts.push(cookieParts);
|
||||
}
|
||||
|
||||
const final_cookie_string = AllCookieParts.map((ck) => ck.join("; "));
|
||||
|
||||
res.setHeader("Set-Cookie", final_cookie_string);
|
||||
}
|
||||
|
||||
export function getCookie(
|
||||
req: http.IncomingMessage,
|
||||
name: string,
|
||||
): string | null {
|
||||
const cookieHeader = req.headers.cookie;
|
||||
if (!cookieHeader) return null;
|
||||
|
||||
const cookies = cookieHeader
|
||||
.split(";")
|
||||
.reduce((acc: { [key: string]: string }, cookie: string) => {
|
||||
const [key, val] = cookie.trim().split("=").map(decodeURIComponent);
|
||||
acc[key] = val;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return cookies[name] || null;
|
||||
}
|
||||
|
||||
export function updateCookie(
|
||||
res: http.ServerResponse,
|
||||
cookies: Cookie[],
|
||||
): void {
|
||||
setCookie(res, cookies);
|
||||
}
|
||||
|
||||
export function deleteCookie(
|
||||
res: http.ServerResponse,
|
||||
cookies: Cookie[],
|
||||
): void {
|
||||
setCookie(
|
||||
res,
|
||||
cookies.map((ck) => ({
|
||||
...ck,
|
||||
value: "",
|
||||
options: {
|
||||
...ck.options,
|
||||
expires: dayjs().subtract(1, "day"),
|
||||
maxAge: 0,
|
||||
},
|
||||
})),
|
||||
);
|
||||
}
|
||||
140
src/utils/login-user.ts
Normal file
140
src/utils/login-user.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { NextApiResponse } from "next";
|
||||
import { ServerResponse } from "http";
|
||||
import NSQLite from "@moduletrace/nsqlite";
|
||||
import { NSQLITE_TEST_DB_USERS, NSQLiteTables } from "../db/types";
|
||||
import { User } from "../types";
|
||||
import { AppData } from "../data/app-data";
|
||||
import { setCookie } from "./cookies-actions";
|
||||
import { EJSON } from "../exports/client-exports";
|
||||
import encrypt from "@moduletrace/datasquirel/dist/package-shared/functions/dsql/encrypt";
|
||||
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
import hashPassword from "@moduletrace/datasquirel/dist/package-shared/functions/dsql/hashPassword";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
type Params = {
|
||||
res: NextApiResponse | ServerResponse;
|
||||
user_id?: string | number;
|
||||
password?: string;
|
||||
email_or_username?: string;
|
||||
};
|
||||
|
||||
export default async function loginUser({
|
||||
res,
|
||||
user_id,
|
||||
password,
|
||||
email_or_username,
|
||||
}: Params): Promise<APIResponseObject> {
|
||||
let fetched_user: NSQLITE_TEST_DB_USERS | undefined;
|
||||
|
||||
if (user_id) {
|
||||
const user_res = await NSQLite.select<
|
||||
NSQLITE_TEST_DB_USERS,
|
||||
(typeof NSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
targetId: user_id,
|
||||
});
|
||||
|
||||
if (!user_res.singleRes?.id) {
|
||||
throw new Error(`Couldn't Find user for login`);
|
||||
}
|
||||
|
||||
fetched_user = user_res.singleRes;
|
||||
}
|
||||
|
||||
if (email_or_username) {
|
||||
const user_res = await NSQLite.select<
|
||||
NSQLITE_TEST_DB_USERS,
|
||||
(typeof NSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
query: {
|
||||
query: {
|
||||
email: {
|
||||
value: email_or_username,
|
||||
},
|
||||
username: {
|
||||
value: email_or_username,
|
||||
},
|
||||
},
|
||||
searchOperator: "OR",
|
||||
},
|
||||
});
|
||||
|
||||
if (!user_res.singleRes?.id) {
|
||||
throw new Error(`Couldn't Find user for login`);
|
||||
}
|
||||
|
||||
fetched_user = user_res.singleRes;
|
||||
}
|
||||
|
||||
if (!fetched_user) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `User Not Found!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (password) {
|
||||
const hashed_password = hashPassword({ password });
|
||||
|
||||
if (hashed_password !== fetched_user.password) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Invalid Password.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const csrf_k =
|
||||
Math.random().toString(36).substring(2) +
|
||||
"-" +
|
||||
Math.random().toString(36).substring(2);
|
||||
|
||||
const logged_in_user_payload: User = {
|
||||
first_name: fetched_user.first_name!,
|
||||
last_name: fetched_user.last_name!,
|
||||
date: now,
|
||||
email: fetched_user.email!,
|
||||
csrf_k,
|
||||
id: fetched_user.id!,
|
||||
logged_in_status: true,
|
||||
image: fetched_user.image,
|
||||
image_thumbnail: fetched_user.image,
|
||||
};
|
||||
|
||||
const payload_string = EJSON.stringify(logged_in_user_payload);
|
||||
const encrypted_payload = encrypt({ data: payload_string || "" });
|
||||
|
||||
const expiration_date = dayjs(Date.now()).add(7, "days");
|
||||
expiration_date.add(7, "days");
|
||||
|
||||
setCookie(res, [
|
||||
{
|
||||
name: AppData["AuthCookieName"],
|
||||
value: encrypted_payload || "",
|
||||
options: {
|
||||
secure: process.env.DOMAIN !== "localhost",
|
||||
path: "/",
|
||||
expires: expiration_date,
|
||||
domain: process.env.DOMAIN,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: AppData["AuthCSRFCookieName"],
|
||||
value: csrf_k,
|
||||
options: {
|
||||
path: "/",
|
||||
expires: expiration_date,
|
||||
domain: process.env.DOMAIN,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
singleRes: logged_in_user_payload,
|
||||
};
|
||||
}
|
||||
21
src/utils/parse-page-url.ts
Normal file
21
src/utils/parse-page-url.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export default function parsePageUrl(url?: string, admin?: boolean) {
|
||||
if (!url) return null;
|
||||
|
||||
let finalAdminUrlArray = url?.match(/_next/)
|
||||
? null
|
||||
: url
|
||||
?.split("?")[0]
|
||||
.split("#")[0]
|
||||
.split("/")
|
||||
.filter((item) => item !== "");
|
||||
|
||||
if (admin) {
|
||||
finalAdminUrlArray?.splice(1, 1);
|
||||
}
|
||||
|
||||
const finalAdminUrl = finalAdminUrlArray
|
||||
? "/" + finalAdminUrlArray?.join("/") || ""
|
||||
: null;
|
||||
|
||||
return finalAdminUrl;
|
||||
}
|
||||
@ -1,12 +1,62 @@
|
||||
import datasquirel from "@moduletrace/datasquirel";
|
||||
import { NextApiRequest } from "next";
|
||||
import { User } from "../types";
|
||||
import { IncomingMessage } from "http";
|
||||
import { AppData } from "../data/app-data";
|
||||
import { getCookie } from "./cookies-actions";
|
||||
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
import decrypt from "@moduletrace/datasquirel/dist/package-shared/functions/dsql/decrypt";
|
||||
import { EJSON } from "../exports/client-exports";
|
||||
|
||||
type Params = {
|
||||
req: NextApiRequest;
|
||||
req:
|
||||
| NextApiRequest
|
||||
| (IncomingMessage & { cookies: Partial<{ [key: string]: string }> });
|
||||
};
|
||||
|
||||
export default async function userAuth({ req }: Params) {
|
||||
const auth = datasquirel.user.auth.auth({ req });
|
||||
const user = auth.payload;
|
||||
return { user };
|
||||
export default async function userAuth({
|
||||
req,
|
||||
}: Params): Promise<APIResponseObject<User>> {
|
||||
try {
|
||||
const key = getCookie(req, AppData["AuthCookieName"]);
|
||||
|
||||
if (!key) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No ${AppData["AuthCookieName"]} found in request object.`,
|
||||
};
|
||||
}
|
||||
|
||||
const decrypted_key = decrypt({ encryptedString: key });
|
||||
const decrypted_object = EJSON.parse(decrypted_key) as User | undefined;
|
||||
|
||||
if (!decrypted_object?.id) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Invalid Auth Key`,
|
||||
};
|
||||
}
|
||||
|
||||
const csrf = getCookie(req, AppData["AuthCSRFCookieName"]);
|
||||
|
||||
if (!csrf) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No ${AppData["AuthCSRFCookieName"]} found in request object.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (csrf !== decrypted_object.csrf_k) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `CSRF mismatch`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
singleRes: decrypted_object,
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
35
src/websocket/index.ts
Normal file
35
src/websocket/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { WebSocketData } from "../types";
|
||||
import socketInit from "./socket-init";
|
||||
|
||||
const server = Bun.serve<WebSocketData>({
|
||||
async fetch(req, server) {
|
||||
const { user } = await socketInit({ req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return new Response("Unauthorized!");
|
||||
}
|
||||
|
||||
const success = server.upgrade(req, {
|
||||
data: { user },
|
||||
});
|
||||
|
||||
if (success) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new Response("Web Socket Connection Failed!");
|
||||
},
|
||||
websocket: {
|
||||
async message(ws, message) {
|
||||
if (typeof message == "string") {
|
||||
}
|
||||
},
|
||||
async open(ws) {},
|
||||
async close(ws, code, message) {},
|
||||
idleTimeout: 600,
|
||||
maxPayloadLength: 1024 * 1024 * 10,
|
||||
},
|
||||
port: process.env.WEB_SOCKET_PORT,
|
||||
});
|
||||
|
||||
console.log(`Websocket Listening on http://${server.hostname}:${server.port}`);
|
||||
39
src/websocket/socket-init.ts
Normal file
39
src/websocket/socket-init.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import datasquirel from "@moduletrace/datasquirel";
|
||||
import { User } from "../types";
|
||||
|
||||
type Param = {
|
||||
req: Request;
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
type Return = {
|
||||
user: User | null;
|
||||
};
|
||||
|
||||
export default async function socketInit({
|
||||
req,
|
||||
debug,
|
||||
}: Param): Promise<Return> {
|
||||
const cookieString = req.headers.get("Cookie") || undefined;
|
||||
|
||||
if (debug) {
|
||||
console.log("DEBUG:::socketInit:cookieString", cookieString);
|
||||
}
|
||||
|
||||
if (!cookieString)
|
||||
return {
|
||||
user: null,
|
||||
};
|
||||
|
||||
const user = datasquirel.user.auth.auth({
|
||||
cookieString,
|
||||
database: process.env.DSQL_DB_NAME || "",
|
||||
skipFileCheck: true,
|
||||
});
|
||||
|
||||
if (debug) {
|
||||
console.log("DEBUG:::socketInit:user", user);
|
||||
}
|
||||
|
||||
return { user: user.payload as User | null };
|
||||
}
|
||||
@ -1,29 +1,37 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
2
twui
2
twui
@ -1 +1 @@
|
||||
Subproject commit 89e673bb59809ba2be04a43ed8c9b5775c580237
|
||||
Subproject commit 9f8527fc4d851c1fecd6600bd60c490de998676f
|
||||
Loading…
Reference in New Issue
Block a user