サインイン・サインアップまで完成
This commit is contained in:
commit
da54ec6ee3
|
|
@ -0,0 +1,2 @@
|
|||
.env
|
||||
node_modules
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
*
|
||||
!/.gitignore
|
||||
!/peas.svg
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="512px"
|
||||
height="512px"
|
||||
viewBox="0 0 210 297"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
sodipodi:docname="peas.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.3"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="244.28571"
|
||||
inkscape:cy="228.92857"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1094"
|
||||
inkscape:window-x="-11"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<rect
|
||||
style="fill:#99ff55;stroke-width:2.22859"
|
||||
id="rect1"
|
||||
width="297"
|
||||
height="297"
|
||||
x="-43.5"
|
||||
y="-3.5527137e-15" />
|
||||
<g
|
||||
id="g2"
|
||||
transform="matrix(1.1469899,0,0,1.1469899,-26.767316,-13.251856)">
|
||||
<path
|
||||
style="fill:#119b20;fill-opacity:1;stroke:#147202;stroke-width:1.83106;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 35.7551,61.017585 c -8.783005,22.596126 -5.761437,69.762885 16.492646,91.222475 l 21.858215,23.82446 c 41.098899,35.55484 90.612599,67.5229 124.660009,21.58881"
|
||||
id="path9"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<g
|
||||
id="g1"
|
||||
transform="matrix(0.85872413,0.14665947,-0.14081857,0.89434245,34.95914,6.2378533)">
|
||||
<ellipse
|
||||
style="fill:#11bd20;fill-opacity:1;stroke:#147202;stroke-width:1.65397;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path2"
|
||||
cx="-78.743805"
|
||||
cy="102.15148"
|
||||
rx="22.052917"
|
||||
ry="22.052916"
|
||||
transform="rotate(-60.606551)" />
|
||||
<ellipse
|
||||
style="fill:#11bd20;fill-opacity:1;stroke:#147202;stroke-width:1.48433;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle2"
|
||||
cx="-81.856644"
|
||||
cy="158.49173"
|
||||
rx="19.791082"
|
||||
ry="19.79108"
|
||||
transform="rotate(-60.606551)" />
|
||||
<ellipse
|
||||
style="fill:#11bd20;fill-opacity:1;stroke:#147202;stroke-width:1.59035;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle4"
|
||||
cx="-81.383789"
|
||||
cy="218.60431"
|
||||
rx="21.204731"
|
||||
ry="21.204727"
|
||||
transform="rotate(-60.606551)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1,86 @@
|
|||
PeasAboutTitle: About Peas
|
||||
PeasAboutText: |
|
||||
Peas is a SNS designed for sharing profiles.<br>
|
||||
You can register and share your user profile.<br>
|
||||
You can freely create profile sections suitable for use at school or work and also change the privacy settings.<br>
|
||||
It also supports printing QR codes making sharing easy.
|
||||
ServerAboutTitle: About this server
|
||||
or: or
|
||||
Start: Get started now
|
||||
Signin: Sign In
|
||||
Signup: Sign Up
|
||||
email: Email
|
||||
password: Password
|
||||
username: Username
|
||||
help: Help
|
||||
idHelp: An alphanumeric ID consisting of 3 to 20 characters and beginning with @
|
||||
mailHelp: Use an email address that can be received
|
||||
passwordHelp: Use a password of at least 8 characters
|
||||
signinError: Sign in failed
|
||||
mailVerify: Mail Verify
|
||||
code: Code
|
||||
submit: Submit
|
||||
Home: Home
|
||||
codeCharacterCount: Please enter a 6-digit code
|
||||
emailVerificationSuccess: |
|
||||
Email verification successful.
|
||||
Please log in.
|
||||
connectionError: Connection Error
|
||||
accountCreateSuccess: |
|
||||
Your account has been successfully created.
|
||||
We have sent a confirmation email to the email address you registered.
|
||||
Please click the confirmation email to activate your account.
|
||||
sameUsername: That username is already taken
|
||||
sameEmail: This email address is already in use.
|
||||
termsAgreement: Please agree to the Terms of Service and Privacy Policy
|
||||
bodyRequired: Form contents have not been submitted
|
||||
passwordCharacterLimit: Please make your password at least 8 characters long.
|
||||
usernameRestriction: Please make your username an alphanumeric ID with at least 3 characters.
|
||||
userCreationFailed: Failed to create the user.
|
||||
terms: Terms of Service
|
||||
privacypolicy: Privacy Policy
|
||||
agreeRule: I agree to the <a href="/terms" class="link">Terms</a> of Use and <a href="/privacypolicy" class="link">Privacy Policy</a>
|
||||
userCount: User Count
|
||||
communityCount: Community Count
|
||||
loading: Loading
|
||||
topPeasCredit: This server uses Peas({0}) an open-source software.
|
||||
notFound: The page you are looking for could not be found
|
||||
notFoundAbout: The specified page was not found on this server.
|
||||
backtoHome: Return to home
|
||||
error: An error occurred
|
||||
mailverifyText: |
|
||||
Hello {0}.
|
||||
Email authentication is required for {1}.
|
||||
By authenticating your email address, you will be able to use the {1} features.
|
||||
Please visit {2}.
|
||||
serverName: Server name
|
||||
serverDescription: Server description
|
||||
serverIconURL: Server icon image URL
|
||||
Q_origin: Origin to use
|
||||
Q_port: Port to broadcast to
|
||||
Q_db_host: MariaDB host name
|
||||
Q_db_port: MariaDB port
|
||||
Q_db_user: MariaDB user
|
||||
Q_db_pass: MariaDB password
|
||||
Q_db_db: MariaDB database name
|
||||
Q_turnstile_is_enabled: Do you want to enable Cloudflare Turnstile?
|
||||
Q_turnstile_sitekey: Turnstile site key
|
||||
Q_turnstile_secret: Turnstile secret key
|
||||
Q_smtp_host: SMTP server hostname
|
||||
Q_smtp_port: SMTP server port
|
||||
Q_smtp_user: SMTP server user
|
||||
Q_smtp_pass: SMTP server password
|
||||
Q_smtp_secure: Do you use encryption for communications to the SMTP server?
|
||||
CLISETUP_success: ✔Setup complete
|
||||
CLI_config_file_write_success: The configuration file was written successfully.
|
||||
ERR_input_lack: Insufficient input
|
||||
ERR_turnstile_request_failed: An error occurred on the server while authenticating Turnstile
|
||||
ERR_turnstile_failed: Turnstile authentication failed
|
||||
ERR_username_duplicate: That username is already in use
|
||||
ERR_email_duplicate: That email is already in use
|
||||
ERR_account_create_failed: Account creation failed
|
||||
ERR_length_different: Invalid character count
|
||||
ERR_code_invalid: Incorrect code
|
||||
ERR_code_not_found: Code not found
|
||||
ERR_user_not_found: User not found
|
||||
ERR_password_invalid: Incorrect password
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
PeasAboutTitle: Peasについて
|
||||
PeasAboutText: |
|
||||
Peasはプロフィール共有を目的としたSNSです。<br>
|
||||
ユーザーのプロフィールを登録し、共有することが出来ます。<br>
|
||||
学校や職場で使用できるようなプロフィール欄も自由に作成でき、公開設定も変更できます。<br>
|
||||
QRコードの印刷にも対応していて共有にも最適です。
|
||||
ServerAboutTitle: このサーバーについて
|
||||
or: または
|
||||
Start: 今すぐ始める
|
||||
Signin: サインイン
|
||||
Signup: サインアップ
|
||||
email: メールアドレス
|
||||
password: パスワード
|
||||
username: ユーザー名
|
||||
help: ヘルプ
|
||||
idHelp: 3文字以上20文字以下で@から始まる英数字のID
|
||||
mailHelp: メールアドレスは受信できるものを使用してください
|
||||
passwordHelp: 8文字以上15文字以下のパスワードを使用してください
|
||||
signinError: サインインに失敗しました
|
||||
mailVerify: メール認証
|
||||
code: コード
|
||||
submit: 送信
|
||||
Home: ホーム
|
||||
codeCharacterCount: コードは6桁で入力してください
|
||||
emailVerificationSuccess: |
|
||||
メール認証に成功しました。
|
||||
ログインしてください。
|
||||
connectionError: 通信エラー
|
||||
accountCreateSuccess: |
|
||||
アカウントの作成に成功しました。
|
||||
ご登録いただいたメールアドレスに確認用メールを送信しました。
|
||||
確認メールをクリックしてアカウントを有効化してください。
|
||||
sameUsername: 同じユーザー名が既に使用されています
|
||||
sameEmail: 同じメールアドレスが既に使用されています
|
||||
termsAgreement: 利用規約とプライバシーポリシーに同意してください
|
||||
bodyRequired: フォーム内容が送信されていません
|
||||
passwordCharacterLimit: パスワードは8文字以上にしてください
|
||||
usernameRestriction: ユーザー名は3文字以上の英数字のIDにしてください
|
||||
userCreationFailed: ユーザーの作成に失敗しました
|
||||
terms: 利用規約
|
||||
privacypolicy: プライバシーポリシー
|
||||
agreeRule: <a href="/terms" class="link">利用規約</a>及び<a href="/privacypolicy" class="link">プライバシーポリシー</a>に同意する
|
||||
userCount: ユーザー数
|
||||
communityCount: 投稿数
|
||||
loading: 読み込み中
|
||||
topPeasCredit: このサーバーはオープンソースソフトウェアのPeas({0})を使用しています。
|
||||
notFound: お探しのページが見つかりませんでした
|
||||
notFoundAbout: このサーバー上に指定されたページが見つかりません。
|
||||
backtoHome: ホームに戻る
|
||||
error: エラーが発生しました
|
||||
mailverifyText: |
|
||||
{0}さんこんにちは。
|
||||
{1}ではメール認証が必要となっています。
|
||||
メール認証をすることで{1}の機能を利用することができます。
|
||||
{2} にアクセスしてください。
|
||||
serverName: サーバー名
|
||||
serverDescription: サーバー説明
|
||||
serverIconURL: サーバーアイコン画像のURL
|
||||
Q_origin: 使用するオリジン
|
||||
Q_port: 配信するポート
|
||||
Q_db_host: MariaDBのホスト名
|
||||
Q_db_port: MariaDBのポート
|
||||
Q_db_user: MariaDBのユーザー
|
||||
Q_db_pass: MariaDBのパスワード
|
||||
Q_db_db: MariaDBのデータベース名
|
||||
Q_turnstile_is_enabled: Cloudflare Turnstileを有効化しますか?
|
||||
Q_turnstile_sitekey: Turnstileのサイトキー
|
||||
Q_turnstile_secret: Turnstileのシークレットキー
|
||||
Q_smtp_host: SMTPサーバーのホスト名
|
||||
Q_smtp_port: SMTPサーバーのポート
|
||||
Q_smtp_user: SMTPサーバーのユーザー
|
||||
Q_smtp_pass: SMTPサーバーのパスワード
|
||||
Q_smtp_secure: SMTPサーバーへの通信に暗号化を使用しますか?
|
||||
CLISETUP_success: ✔ セットアップが完了しました
|
||||
CLI_config_file_write_success: 設定ファイルの書き込みに成功しました
|
||||
ERR_input_lack: 入力が不足しています
|
||||
ERR_turnstile_request_failed: Turnstileの認証中にサーバーでエラーが発生しました
|
||||
ERR_turnstile_failed: Turnstileの認証ができませんでした
|
||||
ERR_username_duplicate: そのユーザー名は既に使用されています
|
||||
ERR_email_duplicate: そのメールアドレスは既に使用されています
|
||||
ERR_account_create_failed: アカウントの作成に失敗しました
|
||||
ERR_length_different: 文字数が正しくありません
|
||||
ERR_code_invalid: コードが正しくありません
|
||||
ERR_code_not_found: コードが見つかりません
|
||||
ERR_user_not_found: ユーザーが見つかりません
|
||||
ERR_password_invalid: パスワードが異なります
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "peas",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Peas is a SNS designed for sharing profiles.",
|
||||
"scripts": {
|
||||
"build:locale": "tsc"
|
||||
},
|
||||
"keywords": [
|
||||
"sns",
|
||||
"profile"
|
||||
],
|
||||
"author": {
|
||||
"name": "Last2014",
|
||||
"email": "info@last2014.com",
|
||||
"url": "https://last2014.com"
|
||||
},
|
||||
"contributors": [],
|
||||
"license": "AGPL-3.0-later",
|
||||
"packageManager": "pnpm@10.17.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^7.8.6",
|
||||
"@types/node": "^24.5.2",
|
||||
"fs": "0.0.1-security",
|
||||
"mysql2": "^3.15.1",
|
||||
"typescript": "^5.9.2",
|
||||
"yaml": "^2.8.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# modules
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# dist
|
||||
/dist
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
!*
|
||||
/peas.config.ts
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
export default interface ConfigType {
|
||||
server: {
|
||||
port: number;
|
||||
origin: string;
|
||||
},
|
||||
database: {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
pass: string;
|
||||
db: string;
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import type ConfigType from "./config";
|
||||
|
||||
const Config: ConfigType = {
|
||||
server: {
|
||||
port: 3000,
|
||||
origin: "https://peas.example.com",
|
||||
},
|
||||
database: {
|
||||
host: "db.example.com",
|
||||
port: 3306,
|
||||
user: "peas",
|
||||
pass: "DatabasePassword",
|
||||
db: "peas",
|
||||
},
|
||||
}
|
||||
|
||||
export default Config;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "web",
|
||||
"type": "module",
|
||||
"main": "dist/src/main.js",
|
||||
"scripts": {
|
||||
"build": "pnpm run \"/^build:.*/\"",
|
||||
"build:ts": "tsc",
|
||||
"build:css": "tailwindcss -i ./tailwind.init.css -o ./public/tailwind.css -m",
|
||||
"start": "node .",
|
||||
"dev": "pnpm run dev:build:css && pnpm run dev:start",
|
||||
"dev:build:css": "pnpm run build:css",
|
||||
"dev:start": "tsx --no-cache src/main.ts -tsx-develop"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.3",
|
||||
"@tailwindcss/cli": "^4.1.13",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/nodemailer": "^7.0.2",
|
||||
"bcrypt": "^6.0.0",
|
||||
"daisyui": "^5.1.25",
|
||||
"fs": "0.0.1-security",
|
||||
"globals": "^16.4.0",
|
||||
"hono": "^4.9.8",
|
||||
"jiti": "^2.5.1",
|
||||
"mysql2": "^3.15.0",
|
||||
"nodemailer": "^7.0.6",
|
||||
"path": "^0.12.7",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"ua-parser-js": "^2.0.5",
|
||||
"url": "^0.11.4",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.17.1"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,5 @@
|
|||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- '@tailwindcss/oxide'
|
||||
- bcrypt
|
||||
- esbuild
|
||||
|
|
@ -0,0 +1 @@
|
|||
tailwind.css
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
const form = document.querySelector("form");
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
document.getElementsByName("code")[0].disable = "";
|
||||
document.getElementsByName("submit")[0].disable = "";
|
||||
|
||||
let error = "";
|
||||
|
||||
const infoReq = await fetch("/api/info", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!infoReq.ok) {
|
||||
error = "通信エラーが発生しました";
|
||||
}
|
||||
const infoRes = await infoReq.json();
|
||||
|
||||
const code = document.getElementsByName("code")[0].value;
|
||||
let turnstile;
|
||||
if (infoRes.botprotection.turnstile === true) {
|
||||
turnstile = document.getElementsByName("cf-turnstile-response")[0].value;
|
||||
}
|
||||
|
||||
const req = await fetch("/api/mailverify", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code: code,
|
||||
turnstile: turnstile ?? undefined,
|
||||
}),
|
||||
});
|
||||
if (!req.ok) {
|
||||
error = "通信エラーが発生しました";
|
||||
}
|
||||
const res = await req.json();
|
||||
if (res.success === true) {
|
||||
location.href = "/signin";
|
||||
} else {
|
||||
location.replace(`/mailverify?error=${res.error}&code=${document.getElementsByName("code")[0].value}`);
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
const form = document.querySelector("form");
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
document.getElementsByName("username")[0].disable = "";
|
||||
document.getElementsByName("password")[0].disable = "";
|
||||
document.getElementsByName("submit")[0].disable = "";
|
||||
|
||||
let error = "";
|
||||
|
||||
const infoReq = await fetch("/api/info", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!infoReq.ok) {
|
||||
error = "通信エラーが発生しました";
|
||||
}
|
||||
const infoRes = await infoReq.json();
|
||||
|
||||
const username = document.getElementsByName("username")[0].value;
|
||||
const password = document.getElementsByName("password")[0].value;
|
||||
let turnstile;
|
||||
if (infoRes.botprotection.turnstile === true) {
|
||||
turnstile = document.getElementsByName("cf-turnstile-response")[0].value;
|
||||
}
|
||||
|
||||
const req = await fetch("/api/signin", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
turnstile: turnstile ?? undefined,
|
||||
}),
|
||||
});
|
||||
if (!req.ok) {
|
||||
error = "通信エラーが発生しました";
|
||||
}
|
||||
const res = await req.json();
|
||||
if (res.success === true) {
|
||||
location.href = "/home";
|
||||
} else {
|
||||
location.replace(`/signin?error=${res.error}`);
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
const form = document.querySelector("form");
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
document.getElementsByName("username")[0].disable = "";
|
||||
document.getElementsByName("email")[0].disable = "";
|
||||
document.getElementsByName("password")[0].disable = "";
|
||||
document.getElementsByName("agreeRule")[0].disable = "";
|
||||
document.getElementsByName("submit")[0].disable = "";
|
||||
|
||||
let error = "";
|
||||
|
||||
const infoReq = await fetch("/api/info", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!infoReq.ok) {
|
||||
error = "通信エラーが発生しました";
|
||||
}
|
||||
const infoRes = await infoReq.json();
|
||||
|
||||
const username = document.getElementsByName("username")[0].value;
|
||||
const email = document.getElementsByName("email")[0].value;
|
||||
const password = document.getElementsByName("password")[0].value;
|
||||
let turnstile;
|
||||
if (infoRes.botprotection.turnstile === true) {
|
||||
turnstile = document.getElementsByName("cf-turnstile-response")[0].value;
|
||||
}
|
||||
|
||||
const req = await fetch("/api/signup", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
email: email,
|
||||
password: password,
|
||||
turnstile: turnstile ?? undefined,
|
||||
}),
|
||||
});
|
||||
if (!req.ok) {
|
||||
error = "通信エラーが発生しました";
|
||||
}
|
||||
const res = await req.json();
|
||||
if (res.success === true) {
|
||||
location.href = "/signin";
|
||||
} else {
|
||||
location.replace(`/signup?error=${res.error}`)
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { Locale } from "../lib/locale.js";
|
||||
import Icon from "./iconify.js";
|
||||
|
||||
const Help = (props: {
|
||||
text: string;
|
||||
lang: string;
|
||||
}) => {
|
||||
return (
|
||||
<div class={`
|
||||
dropdown
|
||||
dropdown-end
|
||||
m-0
|
||||
`}>
|
||||
<div
|
||||
tabindex={0}
|
||||
role="button"
|
||||
class={`
|
||||
btn
|
||||
btn-circle
|
||||
btn-ghost
|
||||
btn-xs
|
||||
text-info
|
||||
`}
|
||||
>
|
||||
<Icon icon="mdi:information" />
|
||||
</div>
|
||||
<div
|
||||
tabindex={0}
|
||||
class={`
|
||||
card
|
||||
card-sm
|
||||
dropdown-content
|
||||
bg-base-100
|
||||
rounded-box
|
||||
z-1
|
||||
w-64
|
||||
shadow-sm
|
||||
`}
|
||||
>
|
||||
<div
|
||||
tabindex={0}
|
||||
class="card-body"
|
||||
>
|
||||
<h2 class="card-title">
|
||||
{Locale({ text: "help" }, props.lang)}
|
||||
</h2>
|
||||
<p>
|
||||
{props.text}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Help;
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import type { JSX, DOMAttributes } from "hono/jsx";
|
||||
|
||||
const Icon = async (props: {
|
||||
icon: string;
|
||||
color?: string;
|
||||
} & DOMAttributes) => {
|
||||
const { icon, color, ...rest } = props;
|
||||
const iconColor = color === undefined ? "" : `?color=${color}`;
|
||||
const url = `https://api.iconify.design/${icon}.svg${iconColor}`;
|
||||
|
||||
const req = await fetch(url, {
|
||||
method: "GET"
|
||||
});
|
||||
if (!req.ok) {
|
||||
return (<></>);
|
||||
}
|
||||
const res = await req.text();
|
||||
|
||||
return (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{__html: res}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { html } from "hono/html";
|
||||
import type { HtmlEscapedString } from "hono/utils/html";
|
||||
|
||||
const Turnstile = (props: {
|
||||
sitekey: string;
|
||||
button: HtmlEscapedString | Promise<HtmlEscapedString>;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{html`<script
|
||||
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
|
||||
async
|
||||
defer
|
||||
></script>`}
|
||||
|
||||
<div id="turnstile-box" />
|
||||
|
||||
{html`<script>
|
||||
window.onload = function () {
|
||||
document.querySelector("${props.button}").disabled = true;
|
||||
|
||||
turnstile.render("#turnstile-box", {
|
||||
sitekey: "${props.sitekey}",
|
||||
callback: (token) => {
|
||||
if (token) {
|
||||
document.querySelector("${props.button}").disabled = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>`}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Turnstile;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import mysql from "mysql2/promise";
|
||||
import Config from "../../config/peas.config.js";
|
||||
|
||||
const option = (connectionLimit: number = 10) => {
|
||||
return {
|
||||
host: Config.database.host,
|
||||
port: Config.database.port,
|
||||
user: Config.database.user,
|
||||
password: Config.database.pass,
|
||||
database: Config.database.db,
|
||||
waitForConnections: true,
|
||||
connectionLimit: connectionLimit,
|
||||
multipleStatements: true,
|
||||
}
|
||||
};
|
||||
|
||||
const verify = async (option: {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
waitForConnections: boolean;
|
||||
connectionLimit?: number;
|
||||
}) => {
|
||||
option.connectionLimit = option.connectionLimit ?? 10;
|
||||
let connection;
|
||||
|
||||
try {
|
||||
connection = await mysql.createConnection(option);
|
||||
} catch (err) {
|
||||
console.error("Error: Could not connect to MariaDB");
|
||||
console.error(err);
|
||||
process.exit();
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { verify, option };
|
||||
|
||||
const pool = mysql.createPool(option());
|
||||
|
||||
export default pool;
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import * as fs from "fs";
|
||||
import { parse as yamlParse } from "yaml";
|
||||
|
||||
export const supportedLanguages = [
|
||||
"en",
|
||||
"ja",
|
||||
];
|
||||
|
||||
import { getDirname } from "./path.js";
|
||||
const dirname = getDirname(import.meta.url);
|
||||
|
||||
export function Check() {
|
||||
let error = false;
|
||||
for (let i = 0; i < supportedLanguages.length; i++) {
|
||||
if (!fs.existsSync(`${dirname}../../../../locales/${supportedLanguages[i]}.yaml`)) {
|
||||
error = true;
|
||||
console.error(`Error: ${supportedLanguages[i]} language pack not found`);
|
||||
}
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
export function Locale({
|
||||
text, data = [],
|
||||
variables = {}
|
||||
}: {
|
||||
text: string;
|
||||
data?: string[];
|
||||
} & {
|
||||
variables?: Record<string, string>;
|
||||
}, lang: string) {
|
||||
if (supportedLanguages.indexOf(lang) === -1) {
|
||||
return "__ERROR:LANG_NOT_SUPPORT";
|
||||
}
|
||||
|
||||
const yaml = fs.readFileSync(`${dirname}../../../../locales/${lang}.yaml`, "utf-8");
|
||||
const localeData = yamlParse(yaml);
|
||||
|
||||
let message = localeData[text];
|
||||
|
||||
data.forEach((value, index) => {
|
||||
message = message.replace(new RegExp('\\{' + index + '\\}', 'g'), value);
|
||||
});
|
||||
|
||||
if (variables) {
|
||||
Object.keys(variables).forEach(key => {
|
||||
message = message.replace(new RegExp('\\{' + key + '\\}', 'g'), variables[key]);
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import * as nodemailer from "nodemailer";
|
||||
import type SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
import pool from "./database.js";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
|
||||
export interface EmailMessage {
|
||||
to: string | string[];
|
||||
subject: string;
|
||||
text?: string;
|
||||
html?: string;
|
||||
}
|
||||
|
||||
export async function createTransporter() {
|
||||
const [host] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'smtp_host'"
|
||||
);
|
||||
const [port] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'smtp_port'"
|
||||
);
|
||||
const [secure] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'smtp_secure'"
|
||||
);
|
||||
const [user] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'smtp_user'"
|
||||
);
|
||||
const [password] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'smtp_password'"
|
||||
);
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: host[0].value,
|
||||
port: Number(port[0].value),
|
||||
secure: secure[0].value === "1" ? true : false,
|
||||
auth: {
|
||||
user: user[0].value,
|
||||
pass: password[0].value,
|
||||
},
|
||||
debug: process.argv.includes("-tsx-develop"),
|
||||
} as SMTPTransport.Options);
|
||||
|
||||
try {
|
||||
await transporter.verify();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return transporter;
|
||||
}
|
||||
|
||||
export async function sendMail(message: EmailMessage): Promise<void> {
|
||||
const [user] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'smtp_user'"
|
||||
);
|
||||
|
||||
try {
|
||||
const transporter = await createTransporter();
|
||||
await transporter.sendMail({
|
||||
from: user[0].value,
|
||||
to: Array.isArray(message.to) ? message.to.join(",") : message.to,
|
||||
subject: message.subject,
|
||||
text: message.text,
|
||||
html: message.html,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import pool from "./database.js";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
import { sendMail } from "./mailer.js";
|
||||
import Config from "../../config/peas.config.js";
|
||||
import type InfoAPI from "../../types/api/info.js";
|
||||
import { Locale } from "./locale.js";
|
||||
|
||||
async function createCode(maxAttempts = 10) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
const [existingCodes] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM mailverifiedcode WHERE code = ?",
|
||||
[code],
|
||||
);
|
||||
if (existingCodes.length === 0) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default async function MailVerifySend(user: string, email: string, lang: string) {
|
||||
const [userData] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM mailverifiedcode WHERE user = ?",
|
||||
[user],
|
||||
);
|
||||
|
||||
if (user === undefined) {
|
||||
return "user_not_found";
|
||||
}
|
||||
|
||||
const code = await createCode();
|
||||
|
||||
const [existingCodes] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM mailverifiedcode WHERE user = ?",
|
||||
[user],
|
||||
);
|
||||
|
||||
let row: RowDataPacket[] = [];
|
||||
|
||||
if (existingCodes.length !== 0) {
|
||||
[row] = await pool.execute<RowDataPacket[]>(
|
||||
"UPDATE `mailverifiedcode` SET `code` = ?, `time` = current_timestamp(3) WHERE `mailverifiedcode`.`user` = ?",
|
||||
[code, user],
|
||||
);
|
||||
} else {
|
||||
[row] = await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `mailverifiedcode` (`code`, `user`, `email`, `time`, `num`) VALUES (?, ?, ?, current_timestamp(3), NULL);",
|
||||
[code, user, email],
|
||||
);
|
||||
}
|
||||
|
||||
if ((row as RowDataPacket).affectedRows === 1) {
|
||||
const infoReq = await fetch(`${Config.server.origin}/api/info`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!infoReq.ok) {
|
||||
return "network_error";
|
||||
}
|
||||
const infoRes: InfoAPI = await infoReq.json();
|
||||
|
||||
await sendMail({
|
||||
to: email,
|
||||
subject: `【${infoRes.server.name}】メール認証`,
|
||||
text: Locale({ text: "mailverifyText", data: [
|
||||
user,
|
||||
infoRes.server.name,
|
||||
`${Config.server.origin}/mailverify?code=${code}`
|
||||
]}, lang),
|
||||
});
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return "save_failed";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
|
||||
const getDirname = (moduleUrl: string) => {
|
||||
const basicDirname = dirname(
|
||||
fileURLToPath(moduleUrl)
|
||||
) + "/";
|
||||
|
||||
if (process.argv.includes("-tsx-develop")) {
|
||||
return basicDirname;
|
||||
} else {
|
||||
return basicDirname + "../";
|
||||
}
|
||||
};
|
||||
|
||||
export { getDirname };
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default function toComponent(message: string) {
|
||||
return <span dangerouslySetInnerHTML={{__html: message}} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { randomBytes } from "crypto";
|
||||
import pool from "./database.js";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
import type scope from "../../types/api/scope";
|
||||
|
||||
export async function createAPIToken(
|
||||
user: string,
|
||||
scope: scope[],
|
||||
name: string,
|
||||
) {
|
||||
const token = randomBytes(32).toString("hex");
|
||||
|
||||
const [insert] = await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `token` (`user`, `scope`, `name`, `token`) VALUES (?, ?, ?, ?);",
|
||||
[user, scope.join(","), name, token],
|
||||
);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function getScopeAPIToken(token: string) {
|
||||
const [tokenData] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM `token` WHERE token = ?",
|
||||
[token]
|
||||
);
|
||||
|
||||
const scope: scope[] = tokenData[0].scope.split(",");
|
||||
|
||||
return scope;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import pool from "./database.js";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
|
||||
export async function isEnabled() {
|
||||
const [isEnabled] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'turnstile_isEnabled'"
|
||||
);
|
||||
|
||||
if (isEnabled[0].value === "1") {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getKeys() {
|
||||
if (!isEnabled()) {
|
||||
return [];
|
||||
}
|
||||
const [Sitekey] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'turnstile_sitekey'"
|
||||
);
|
||||
const [Secret] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'turnstile_secret'"
|
||||
);
|
||||
return [Sitekey[0].value, Secret[0].value];
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Checks
|
||||
/// Locale Data Check
|
||||
import { Check as LocaleDataCheck } from "./lib/locale.js";
|
||||
LocaleDataCheck();
|
||||
|
||||
/// Database Connect Check
|
||||
import { verify as DBCheck, option as DBOption } from "./lib/database.js";
|
||||
DBCheck(DBOption(0));
|
||||
|
||||
// Middleware
|
||||
/// Language
|
||||
import { languageDetector } from "hono/language"
|
||||
app.use(languageDetector({
|
||||
lookupCookie: "lang",
|
||||
caches: ["cookie"],
|
||||
cookieOptions: {
|
||||
path: "/",
|
||||
sameSite: "Lax",
|
||||
maxAge: 86400 * 365,
|
||||
},
|
||||
supportedLanguages: ["en", "ja"],
|
||||
fallbackLanguage: "ja",
|
||||
}))
|
||||
|
||||
// Routing
|
||||
/// API
|
||||
import API from "./routes/api/main.js";
|
||||
app.route("/api", API);
|
||||
|
||||
/// Static
|
||||
import { serveStatic } from "@hono/node-server/serve-static";
|
||||
import { getDirname } from "./lib/path.js";
|
||||
|
||||
//// Hono/Public
|
||||
app.use("*", serveStatic({
|
||||
root: `${getDirname(import.meta.url)}../public`,
|
||||
}));
|
||||
|
||||
//// Project Root/Files
|
||||
app.use("*", serveStatic({
|
||||
root: `${getDirname(import.meta.url)}../../../files`,
|
||||
}));
|
||||
|
||||
/// Pages
|
||||
import Pages from "./routes/pages/main.js";
|
||||
app.route("/", Pages);
|
||||
|
||||
// Server
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port: 3000
|
||||
}, (info) => {
|
||||
console.log(`Server is running on http://${info.address}:${info.port}`);
|
||||
});
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { Hono } from "hono";
|
||||
import pool from "../../lib/database.js";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
import { readFileSync } from "fs";
|
||||
import { getDirname } from "../../lib/path.js";
|
||||
import { getKeys as TurnstileKeys, isEnabled as Turnstile } from "../../lib/turnstile.js";
|
||||
|
||||
const infoAPI = new Hono();
|
||||
|
||||
infoAPI.post("/", async (c) => {
|
||||
const packageJson = JSON.parse(readFileSync(
|
||||
`${getDirname(import.meta.url)}/../../../../../package.json`,
|
||||
"utf-8"));
|
||||
const version = packageJson.version;
|
||||
const author = packageJson.author;
|
||||
const contributors = packageJson.contributors;
|
||||
|
||||
const [IconURL] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'server_icon'"
|
||||
);
|
||||
|
||||
const [serverName] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'server_name'"
|
||||
);
|
||||
|
||||
const [serverDescription] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT value FROM config WHERE name = 'server_description'"
|
||||
);
|
||||
|
||||
const [users] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT COUNT(*) AS count FROM users"
|
||||
);
|
||||
|
||||
const [communitys] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT COUNT(*) AS count FROM community"
|
||||
);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
|
||||
server: {
|
||||
icon: IconURL[0].value,
|
||||
name: serverName[0].value,
|
||||
description: serverDescription[0].value,
|
||||
},
|
||||
|
||||
botprotection: {
|
||||
turnstile: await Turnstile(),
|
||||
},
|
||||
|
||||
software: {
|
||||
version: version,
|
||||
name: "Peas",
|
||||
author: author,
|
||||
contributors: contributors,
|
||||
},
|
||||
|
||||
counts: {
|
||||
user: users[0].count,
|
||||
community: communitys[0].count,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default infoAPI;
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import { Hono } from "hono";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
import pool from "../../lib/database.js";
|
||||
import { getKeys as TurnstileKeys, isEnabled as Turnstile } from "../../lib/turnstile.js";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { getConnInfo } from "@hono/node-server/conninfo";
|
||||
|
||||
const Check = new Hono();
|
||||
|
||||
Check.post("/", async (c) => {
|
||||
const body = await c.req.json();
|
||||
|
||||
if (body.code === undefined) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "input_lack",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (
|
||||
await Turnstile() &&
|
||||
body.turnstile === undefined
|
||||
) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "input_lack",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (await Turnstile()) {
|
||||
const sendData = new FormData();
|
||||
const ip = getConnInfo(c).remote.address;
|
||||
if (ip !== undefined) {
|
||||
sendData.append("remoteip", ip);
|
||||
}
|
||||
sendData.append("secret", (await TurnstileKeys())[1]);
|
||||
sendData.append("response", body.turnstile);
|
||||
sendData.append("idempotency_key", uuid())
|
||||
|
||||
try {
|
||||
const req = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
body: sendData,
|
||||
});
|
||||
if (!req.ok) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "turnstile_request_failed",
|
||||
}, 500);
|
||||
}
|
||||
const res = await req.json();
|
||||
if (res.success === false) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "turnstile_failed",
|
||||
}, 400);
|
||||
}
|
||||
} catch (err) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "turnstile_request_failed",
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM mailverifiedcode WHERE code = ?",
|
||||
[body.code],
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "code_not_found",
|
||||
}, 400);
|
||||
} else {
|
||||
if (rows[0].code === body.code) {
|
||||
await pool.query<RowDataPacket[]>(
|
||||
"UPDATE `users` SET `mailverified` = 1 WHERE `users`.`id` = ?",
|
||||
[rows[0].user],
|
||||
);
|
||||
|
||||
await pool.query<RowDataPacket[]>(
|
||||
"DELETE FROM `mailverifiedcode` WHERE (`user` = ?)",
|
||||
[rows[0].user],
|
||||
);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
});
|
||||
} else {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "code_invalid",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default Check;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { Hono } from "hono";
|
||||
|
||||
const API = new Hono();
|
||||
|
||||
import infoAPI from "./info.js";
|
||||
API.route("/info", infoAPI);
|
||||
|
||||
import SignUpAPI from "./signup.js";
|
||||
API.route("/signup", SignUpAPI);
|
||||
|
||||
import SignInAPI from "./signin.js";
|
||||
API.route("/signin", SignInAPI);
|
||||
|
||||
import mailverifyAPI from "./mailverify.js";
|
||||
API.route("/mailverify", mailverifyAPI);
|
||||
|
||||
import NotFound from "./notfound.js";
|
||||
API.route("*", NotFound);
|
||||
|
||||
export default API;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Hono } from "hono";
|
||||
|
||||
const NotFound = new Hono();
|
||||
|
||||
NotFound.all("/", (c) => {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "not_found",
|
||||
}, 404);
|
||||
});
|
||||
|
||||
export default NotFound;
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import { Hono } from "hono";
|
||||
import pool from "../../lib/database.js";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
import { getKeys as TurnstileKeys, isEnabled as Turnstile } from "../../lib/turnstile.js";
|
||||
import { getConnInfo } from "@hono/node-server/conninfo";
|
||||
import { compareSync as passwordHashCheck } from "bcrypt";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import MailVerifySend from "../../lib/mailverify.js";
|
||||
import { createAPIToken } from "src/lib/token.js";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { setCookie } from "hono/cookie";
|
||||
|
||||
const SignInAPI = new Hono();
|
||||
|
||||
SignInAPI.post("/", async (c) => {
|
||||
const body = await c.req.json();
|
||||
if (
|
||||
body.username === undefined ||
|
||||
body.password === undefined
|
||||
) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "input_lack",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (
|
||||
body.username.length < 3 ||
|
||||
body.username.length > 15 ||
|
||||
body.password.length < 8 ||
|
||||
body.password.length > 15
|
||||
) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "length_different",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (
|
||||
await Turnstile() &&
|
||||
body.turnstile === undefined
|
||||
) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "input_lack",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (await Turnstile()) {
|
||||
const sendData = new FormData();
|
||||
const ip = getConnInfo(c).remote.address;
|
||||
if (ip !== undefined) {
|
||||
sendData.append("remoteip", ip);
|
||||
}
|
||||
sendData.append("secret", (await TurnstileKeys())[1]);
|
||||
sendData.append("response", body.turnstile);
|
||||
sendData.append("idempotency_key", uuid())
|
||||
|
||||
try {
|
||||
const req = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
body: sendData,
|
||||
});
|
||||
if (!req.ok) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "turnstile_request_failed",
|
||||
}, 500);
|
||||
}
|
||||
const res = await req.json();
|
||||
if (res.success === false) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "turnstile_failed",
|
||||
}, 400);
|
||||
}
|
||||
} catch (err) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "turnstile_request_failed",
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const [user] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
[body.username],
|
||||
);
|
||||
|
||||
if (user === undefined) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "user_not_found",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (passwordHashCheck(body.password, user[0].password)) {
|
||||
const token = await createAPIToken(
|
||||
body.username,
|
||||
["client"],
|
||||
"Peas Client"
|
||||
);
|
||||
|
||||
setCookie(c, "token", token, {
|
||||
path: "/",
|
||||
sameSite: "Lax",
|
||||
maxAge: 86400 * 365,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
token: token,
|
||||
});
|
||||
} else {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "password_invalid",
|
||||
}, 400);
|
||||
}
|
||||
});
|
||||
|
||||
export default SignInAPI;
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import { Hono } from "hono";
|
||||
import pool from "../../lib/database.js";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
import { getKeys as TurnstileKeys, isEnabled as Turnstile } from "../../lib/turnstile.js";
|
||||
import { getConnInfo } from "@hono/node-server/conninfo";
|
||||
import { hash as generatePasswordHash } from "bcrypt";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import MailVerifySend from "../../lib/mailverify.js";
|
||||
|
||||
const SignUpAPI = new Hono();
|
||||
|
||||
SignUpAPI.post("/", async (c) => {
|
||||
const body = await c.req.json();
|
||||
if (
|
||||
body.username === undefined ||
|
||||
body.email === undefined ||
|
||||
body.password === undefined
|
||||
) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "input_lack",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (
|
||||
body.username.length < 3 ||
|
||||
body.username.length > 15 ||
|
||||
body.password.length < 8 ||
|
||||
body.password.length > 15
|
||||
) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "length_different",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (
|
||||
await Turnstile() &&
|
||||
body.turnstile === undefined
|
||||
) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "input_lack",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (await Turnstile()) {
|
||||
const sendData = new FormData();
|
||||
const ip = getConnInfo(c).remote.address;
|
||||
if (ip !== undefined) {
|
||||
sendData.append("remoteip", ip);
|
||||
}
|
||||
sendData.append("secret", (await TurnstileKeys())[1]);
|
||||
sendData.append("response", body.turnstile);
|
||||
sendData.append("idempotency_key", uuid())
|
||||
|
||||
try {
|
||||
const req = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
body: sendData,
|
||||
});
|
||||
if (!req.ok) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "turnstile_request_failed",
|
||||
}, 500);
|
||||
}
|
||||
const res = await req.json();
|
||||
if (res.success === false) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "turnstile_failed",
|
||||
}, 400);
|
||||
}
|
||||
} catch (err) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "turnstile_request_failed",
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const [duplicateUsers] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
[body.username],
|
||||
);
|
||||
|
||||
const [duplicateEmails] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM users WHERE email = ?",
|
||||
[body.email],
|
||||
);
|
||||
|
||||
if (duplicateUsers.length > 0) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "username_duplicate",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (duplicateEmails.length > 0) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "email_duplicate",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
const passwordHash = await generatePasswordHash(body.password, 10);
|
||||
|
||||
const [result] = await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `users` (`id`, `password`, `email`, `mailverified`, `time`) VALUES (?, ?, ?, '0', current_timestamp(3))",
|
||||
[body.username, passwordHash, body.email],
|
||||
);
|
||||
|
||||
if ((result as RowDataPacket).affectedRows === 1) {
|
||||
const send = await MailVerifySend(body.username, body.email, c.get("language"));
|
||||
if (send === true) {
|
||||
return c.json({
|
||||
success: true,
|
||||
}, 201);
|
||||
} else {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: send,
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: "account_create_failed",
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
export default SignUpAPI;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { Hono } from "hono";
|
||||
import { Layout } from "./layout.js";
|
||||
import type InfoAPI from "../../../types/api/info";
|
||||
import Config from "../../../config/peas.config.js";
|
||||
import { getCookie } from "hono/cookie";
|
||||
import { Locale } from "../../lib/locale.js";
|
||||
|
||||
const Home = new Hono();
|
||||
|
||||
Home.get("/", async (c) => {
|
||||
if (getCookie(c, "token") === undefined) return c.redirect("/signin");
|
||||
|
||||
const InfoReq = await fetch(`${Config.server.origin}/api/info`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!InfoReq.ok) {
|
||||
return c.status(500);
|
||||
}
|
||||
|
||||
const InfoRes: InfoAPI = await InfoReq.json();
|
||||
|
||||
return c.html(
|
||||
<Layout
|
||||
title={`${Locale({ text: "Home" }, c.get("language"))} - ${InfoRes.server.name}`}
|
||||
noindex
|
||||
>
|
||||
|
||||
</Layout>
|
||||
);
|
||||
})
|
||||
|
||||
export default Home;
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import { Hono } from "hono";
|
||||
import { Layout } from "./layout.js";
|
||||
import type InfoAPI from "../../../types/api/info";
|
||||
import Config from "../../../config/peas.config.js";
|
||||
import Icon from "../../components/iconify.js";
|
||||
import { Locale } from "../../lib/locale.js";
|
||||
import toComponent from "../../lib/toComponent.js";
|
||||
import { getCookie } from "hono/cookie";
|
||||
import { getScopeAPIToken } from "../../lib/token.js";
|
||||
|
||||
const Index = new Hono();
|
||||
|
||||
Index.get("/", async (c) => {
|
||||
if (getCookie(c, "token") !== undefined) {
|
||||
const scope = await getScopeAPIToken(getCookie(c, "token") ?? "");
|
||||
if (scope !== undefined) return c.redirect("/home");
|
||||
}
|
||||
|
||||
const InfoReq = await fetch(`${Config.server.origin}/api/info`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!InfoReq.ok) {
|
||||
return c.status(500);
|
||||
}
|
||||
|
||||
const InfoRes: InfoAPI = await InfoReq.json();
|
||||
|
||||
return c.html(
|
||||
<Layout
|
||||
title={InfoRes.server.name}
|
||||
description={InfoRes.server.description}
|
||||
>
|
||||
<main class={`
|
||||
pt-5
|
||||
`}>
|
||||
<h1 class={`
|
||||
text-3xl
|
||||
font-bold
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
`}>
|
||||
<img
|
||||
src={InfoRes.server.icon}
|
||||
width={30}
|
||||
height={30}
|
||||
class={`
|
||||
rounded-[30%]
|
||||
align-middle
|
||||
`}
|
||||
/>
|
||||
{InfoRes.server.name}
|
||||
</h1>
|
||||
|
||||
<div class={`
|
||||
card
|
||||
card-body
|
||||
bg-base-100
|
||||
w-100
|
||||
shadow-sm
|
||||
m-auto
|
||||
mt-2
|
||||
`}>
|
||||
<h2 class="card-title">
|
||||
<Icon icon="ix:about-filled" />
|
||||
{Locale({ text: "PeasAboutTitle" }, c.get("language"))}
|
||||
</h2>
|
||||
{toComponent(Locale({ text: "PeasAboutText" }, c.get("language")))}
|
||||
</div>
|
||||
|
||||
<div class={`
|
||||
card
|
||||
card-body
|
||||
bg-base-100
|
||||
w-100
|
||||
shadow-sm
|
||||
m-auto
|
||||
mt-2
|
||||
`}>
|
||||
<h2 class="card-title">
|
||||
<Icon icon="ix:about-filled" />
|
||||
{Locale({ text: "ServerAboutTitle" }, c.get("language"))}
|
||||
</h2>
|
||||
{toComponent(InfoRes.server.description)}
|
||||
|
||||
<div className={`
|
||||
w-fit
|
||||
flex
|
||||
justify-center
|
||||
gap-[2rem]
|
||||
pr-[20px]
|
||||
pl-[20px]
|
||||
m-auto
|
||||
mt-[1rem]
|
||||
mb-[1rem]
|
||||
rounded-[5px]
|
||||
border-solid
|
||||
border-white
|
||||
border
|
||||
text-center
|
||||
`}>
|
||||
<div class={`
|
||||
w-fit
|
||||
m-0
|
||||
`}>
|
||||
{Locale({ text: "userCount" }, c.get("language"))}
|
||||
<br />
|
||||
{InfoRes.counts.user}
|
||||
</div>
|
||||
|
||||
<div class={`
|
||||
w-fit
|
||||
m-0
|
||||
`}>
|
||||
{Locale({ text: "communityCount" }, c.get("language"))}
|
||||
<br />
|
||||
{InfoRes.counts.community}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Locale({ text: "topPeasCredit", data: [InfoRes.software.version] }, c.get("language"))}
|
||||
</div>
|
||||
|
||||
<div class={`
|
||||
text-center
|
||||
mt-2
|
||||
`}>
|
||||
<a
|
||||
class="btn"
|
||||
href="/signup"
|
||||
>
|
||||
{Locale({ text: "Start" }, c.get("language"))}
|
||||
</a>
|
||||
|
||||
<p class={`
|
||||
mt-1
|
||||
mb-1
|
||||
`}>
|
||||
{Locale({ text: "or" }, c.get("language"))}
|
||||
</p>
|
||||
|
||||
<a
|
||||
class="btn"
|
||||
href="/signin"
|
||||
>
|
||||
{Locale({ text: "Signin" }, c.get("language"))}
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
});
|
||||
|
||||
export default Index;
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import type { FC } from "hono/jsx";
|
||||
import Config from "../../../config/peas.config.js";
|
||||
import type InfoAPI from "../../../types/api/info";
|
||||
|
||||
interface Option {
|
||||
title: string;
|
||||
description?: string;
|
||||
noindex?: boolean;
|
||||
noai?: boolean;
|
||||
noimageai?: boolean;
|
||||
children?: any;
|
||||
};
|
||||
|
||||
const Layout: FC<Option> = async (props: Option) => {
|
||||
const InfoReq = await fetch(`${Config.server.origin}/api/info`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!InfoReq.ok) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
const InfoRes: InfoAPI = await InfoReq.json();
|
||||
|
||||
const noai = props.noai ?? false;
|
||||
const noimageai = props.noimageai ?? false;
|
||||
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
{props.description && (
|
||||
<meta name="description" content={props.description} />
|
||||
)}
|
||||
{props.noindex && (
|
||||
<meta name="robots" content="none" />
|
||||
)}
|
||||
{props.noai && (
|
||||
<meta name="robots" content="noai" />
|
||||
)}
|
||||
{props.noimageai && (
|
||||
<meta name="robots" content="noimageai" />
|
||||
)}
|
||||
<title>{props.title}</title>
|
||||
<link rel="icon" href={InfoRes.server.icon} />
|
||||
<link rel="stylesheet" href="/tailwind.css" />
|
||||
</head>
|
||||
<body class={`
|
||||
bg-white
|
||||
text-black
|
||||
dark:bg-zinc-800
|
||||
dark:text-white
|
||||
w-full
|
||||
h-full
|
||||
`}>
|
||||
{props.children}
|
||||
</body>
|
||||
|
||||
{/*
|
||||
==Warning==
|
||||
This hidden tag is there to allow TailwindCSS and DaisyUI
|
||||
to detect classes that are only used for i18n,
|
||||
so please do not remove it.
|
||||
*/}
|
||||
<span class="hidden link"></span>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
export { Layout };
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import { Hono } from "hono";
|
||||
import Config from "../../../config/peas.config.js";
|
||||
import type InfoAPI from "../../../types/api/info";
|
||||
import { Layout } from "./layout.js";
|
||||
import { Locale } from "../../lib/locale.js";
|
||||
import Icon from "../../components/iconify.js";
|
||||
import { getKeys as getTurnstileKeys, isEnabled as TurnstileIsEnabled } from "../../lib/turnstile.js";
|
||||
import Turnstile from "../../components/turnstile.js";
|
||||
import { html } from "hono/html";
|
||||
import { getCookie } from "hono/cookie";
|
||||
import { getScopeAPIToken } from "../../lib/token.js";
|
||||
|
||||
const MailVerify = new Hono();
|
||||
|
||||
MailVerify.get("/", async (c) => {
|
||||
if (getCookie(c, "token") !== undefined) {
|
||||
const scope = await getScopeAPIToken(getCookie(c, "token") ?? "");
|
||||
if (scope !== undefined) return c.redirect("/home");
|
||||
}
|
||||
|
||||
const InfoReq = await fetch(`${Config.server.origin}/api/info`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!InfoReq.ok) {
|
||||
return c.status(500);
|
||||
}
|
||||
|
||||
const InfoRes: InfoAPI = await InfoReq.json();
|
||||
|
||||
let code = "";
|
||||
if (c.req.query("code") !== undefined) {
|
||||
code = c.req.query("code") ?? "";
|
||||
}
|
||||
|
||||
const TurnstileEnabled = await TurnstileIsEnabled();
|
||||
|
||||
let TurnstileKeys: Array<string> = [];
|
||||
if (TurnstileEnabled) {
|
||||
TurnstileKeys = await getTurnstileKeys();
|
||||
}
|
||||
|
||||
let error = false;
|
||||
let errorMsg = "";
|
||||
if (c.req.query("error") !== undefined) {
|
||||
error = true;
|
||||
errorMsg = Locale({ text: `ERR_${c.req.query("error")}` }, c.get("language"));
|
||||
}
|
||||
|
||||
return c.html(
|
||||
<Layout
|
||||
title={`${Locale({ text: "mailVerify" }, c.get("language"))} - ${InfoRes.server.name}`}
|
||||
noindex
|
||||
>
|
||||
<main class="text-center">
|
||||
{error && (
|
||||
<div
|
||||
role="alert"
|
||||
class="alert alert-error w-fit m-auto"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:alert-circle-outline"
|
||||
color="#ff0000"
|
||||
/>
|
||||
<span>{errorMsg}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 class={`
|
||||
text-3xl
|
||||
font-bold
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
`}>
|
||||
<Icon
|
||||
icon="material-symbols:mail-shield"
|
||||
class="w-4"
|
||||
/>
|
||||
{Locale({ text: "mailVerify" }, c.get("language"))}
|
||||
</h1>
|
||||
|
||||
<form
|
||||
class={`
|
||||
flex
|
||||
flex-col
|
||||
items-center
|
||||
justify-center
|
||||
`}
|
||||
id="form"
|
||||
>
|
||||
<div>
|
||||
<label class={`
|
||||
input
|
||||
w-70
|
||||
m-3
|
||||
`}>
|
||||
<span class="label">
|
||||
<Icon icon="material-symbols:barcode" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="123456"
|
||||
minlength={6}
|
||||
maxlength={6}
|
||||
value={code}
|
||||
name="code"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{TurnstileEnabled && (
|
||||
<Turnstile
|
||||
sitekey={TurnstileKeys[0]}
|
||||
button={html`[name='submit']`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
class={`
|
||||
btn
|
||||
btn-primary
|
||||
m-3
|
||||
`}
|
||||
type="submit"
|
||||
name="submit"
|
||||
disabled
|
||||
>
|
||||
<Icon icon="material-symbols:barcode" />
|
||||
{Locale({ text: "submit" }, c.get("language"))}
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script src="/js/mailverify.js" />
|
||||
</Layout>
|
||||
);
|
||||
});
|
||||
|
||||
export default MailVerify;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Hono } from "hono";
|
||||
|
||||
const Pages = new Hono();
|
||||
|
||||
import Index from "./index.js";
|
||||
Pages.route("/", Index);
|
||||
|
||||
import Home from "./home.js";
|
||||
Pages.route("/home", Home);
|
||||
|
||||
import SignUp from "./signup.js";
|
||||
Pages.route("/signup", SignUp);
|
||||
|
||||
import SignIn from "./signin.js";
|
||||
Pages.route("/signin", SignIn);
|
||||
|
||||
import MailVerify from "./mailverify.js";
|
||||
Pages.route("/mailverify", MailVerify);
|
||||
|
||||
import NotFound from "./notfound.js";
|
||||
Pages.route("*", NotFound);
|
||||
|
||||
export default Pages;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { Hono } from "hono";
|
||||
import Config from "../../../config/peas.config.js";
|
||||
import Icon from "../../components/iconify.js";
|
||||
import type InfoAPI from "../../../types/api/info.js";
|
||||
import { Layout } from "./layout.js";
|
||||
import { Locale } from "../../lib/locale.js";
|
||||
|
||||
const NotFound = new Hono();
|
||||
|
||||
NotFound.get("/", async (c) => {
|
||||
const InfoReq = await fetch(`${Config.server.origin}/api/info`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!InfoReq.ok) {
|
||||
return c.body("Internal Server Error", 500);
|
||||
}
|
||||
|
||||
const InfoRes: InfoAPI = await InfoReq.json();
|
||||
|
||||
c.status(404);
|
||||
return c.html(
|
||||
<Layout
|
||||
title={`${Locale({ text: "notFound" }, c.get("language"))} - ${InfoRes.server.name}`}
|
||||
noindex
|
||||
>
|
||||
<Icon icon="line-md:question-circle" />
|
||||
</Layout>
|
||||
);
|
||||
});
|
||||
|
||||
export default NotFound;
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { Hono } from "hono";
|
||||
import { Layout } from "./layout.js";
|
||||
import type InfoAPI from "../../../types/api/info";
|
||||
import Config from "../../../config/peas.config.js";
|
||||
import { Locale } from "../../lib/locale.js";
|
||||
import Icon from "../../components/iconify.js";
|
||||
import Help from "../../components/help.js";
|
||||
import Turnstile from "../../components/turnstile.js";
|
||||
import { getKeys as getTurnstileKeys, isEnabled as TurnstileIsEnabled } from "../../lib/turnstile.js";
|
||||
import { html } from "hono/html";
|
||||
import { getCookie } from "hono/cookie";
|
||||
import { getScopeAPIToken } from "../../lib/token.js";
|
||||
|
||||
const SignIn = new Hono();
|
||||
|
||||
SignIn.get("/", async (c) => {
|
||||
if (getCookie(c, "token") !== undefined) {
|
||||
const scope = await getScopeAPIToken(getCookie(c, "token") ?? "");
|
||||
if (scope !== undefined) return c.redirect("/home");
|
||||
}
|
||||
|
||||
const InfoReq = await fetch(`${Config.server.origin}/api/info`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!InfoReq.ok) {
|
||||
return c.status(500);
|
||||
}
|
||||
|
||||
const InfoRes: InfoAPI = await InfoReq.json();
|
||||
|
||||
const TurnstileEnabled = await TurnstileIsEnabled();
|
||||
|
||||
let TurnstileKeys: Array<string> = [];
|
||||
if (TurnstileEnabled) {
|
||||
TurnstileKeys = await getTurnstileKeys();
|
||||
}
|
||||
|
||||
let error = false;
|
||||
let errorMsg = "";
|
||||
if (c.req.query("error") !== undefined) {
|
||||
error = true;
|
||||
errorMsg = Locale({ text: `ERR_${c.req.query("error")}` }, c.get("language"));
|
||||
}
|
||||
|
||||
return c.html(
|
||||
<Layout
|
||||
title={`${Locale({ text: "Signin" }, c.get("language"))} - ${InfoRes.server.name}`}
|
||||
noindex
|
||||
>
|
||||
<main class="text-center">
|
||||
{error && (
|
||||
<div
|
||||
role="alert"
|
||||
class="alert alert-error w-fit m-auto"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:alert-circle-outline"
|
||||
color="#ff0000"
|
||||
/>
|
||||
<span>{errorMsg}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 class={`
|
||||
text-3xl
|
||||
font-bold
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
`}>
|
||||
<Icon
|
||||
icon="material-symbols:power-settings-new"
|
||||
class="w-4"
|
||||
/>
|
||||
{Locale({ text: "Signin" }, c.get("language"))}
|
||||
</h1>
|
||||
|
||||
<form
|
||||
class={`
|
||||
flex
|
||||
flex-col
|
||||
items-center
|
||||
justify-center
|
||||
`}
|
||||
id="form"
|
||||
>
|
||||
<div>
|
||||
<label class={`
|
||||
input
|
||||
w-70
|
||||
m-3
|
||||
`}>
|
||||
<span class="label">@</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={Locale({ text: "username" }, c.get("language"))}
|
||||
minlength={3}
|
||||
maxlength={20}
|
||||
name="username"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<Help
|
||||
text={Locale({ text: "idHelp" }, c.get("language"))}
|
||||
lang={c.get("language")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class={`
|
||||
input
|
||||
w-70
|
||||
m-3
|
||||
`}>
|
||||
<span class="label">
|
||||
<Icon
|
||||
icon="material-symbols:key-rounded"
|
||||
color="#A4A7A9"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={Locale({ text: "password" }, c.get("language"))}
|
||||
name="password"
|
||||
minlength={8}
|
||||
maxlength={15}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<Help
|
||||
text={Locale({ text: "passwordHelp" }, c.get("language"))}
|
||||
lang={c.get("language")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{TurnstileEnabled && (
|
||||
<Turnstile
|
||||
sitekey={TurnstileKeys[0]}
|
||||
button={html`[name='submit']`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
class={`
|
||||
btn
|
||||
btn-primary
|
||||
m-3
|
||||
`}
|
||||
type="submit"
|
||||
name="submit"
|
||||
disabled
|
||||
>
|
||||
<Icon icon="material-symbols:power-settings-new" />
|
||||
{Locale({ text: "Signin" }, c.get("language"))}
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script src="/js/signin.js" />
|
||||
</Layout>
|
||||
);
|
||||
});
|
||||
|
||||
export default SignIn;
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
import { Hono } from "hono";
|
||||
import { Layout } from "./layout.js";
|
||||
import type InfoAPI from "../../../types/api/info";
|
||||
import Config from "../../../config/peas.config.js";
|
||||
import { Locale } from "../../lib/locale.js";
|
||||
import Icon from "../../components/iconify.js";
|
||||
import Help from "../../components/help.js";
|
||||
import toComponent from "../../lib/toComponent.js";
|
||||
import Turnstile from "../../components/turnstile.js";
|
||||
import { getKeys as getTurnstileKeys, isEnabled as TurnstileIsEnabled } from "../../lib/turnstile.js";
|
||||
import { html } from "hono/html";
|
||||
import { getCookie } from "hono/cookie";
|
||||
import { getScopeAPIToken } from "../../lib/token.js";
|
||||
|
||||
const SignUp = new Hono();
|
||||
|
||||
SignUp.get("/", async (c) => {
|
||||
if (getCookie(c, "token") !== undefined) {
|
||||
const scope = await getScopeAPIToken(getCookie(c, "token") ?? "");
|
||||
if (scope !== undefined) return c.redirect("/home");
|
||||
}
|
||||
|
||||
const InfoReq = await fetch(`${Config.server.origin}/api/info`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!InfoReq.ok) {
|
||||
return c.status(500);
|
||||
}
|
||||
|
||||
const InfoRes: InfoAPI = await InfoReq.json();
|
||||
|
||||
const TurnstileEnabled = await TurnstileIsEnabled();
|
||||
|
||||
let TurnstileKeys: Array<string> = [];
|
||||
if (TurnstileEnabled) {
|
||||
TurnstileKeys = await getTurnstileKeys();
|
||||
}
|
||||
|
||||
let error = false;
|
||||
let errorMsg = "";
|
||||
if (c.req.query("error") !== undefined) {
|
||||
error = true;
|
||||
errorMsg = Locale({ text: `ERR_${c.req.query("error")}` }, c.get("language"));
|
||||
}
|
||||
|
||||
return c.html(
|
||||
<Layout
|
||||
title={`${Locale({ text: "Signup" }, c.get("language"))} - ${InfoRes.server.name}`}
|
||||
noindex
|
||||
>
|
||||
<main class="text-center">
|
||||
{error && (
|
||||
<div
|
||||
role="alert"
|
||||
class="alert alert-error w-fit m-auto"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:alert-circle-outline"
|
||||
color="#ff0000"
|
||||
/>
|
||||
<span>{errorMsg}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 class={`
|
||||
text-3xl
|
||||
font-bold
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
`}>
|
||||
<Icon
|
||||
icon="material-symbols:person-add-rounded"
|
||||
class="w-4"
|
||||
/>
|
||||
{Locale({ text: "Signup" }, c.get("language"))}
|
||||
</h1>
|
||||
|
||||
<form
|
||||
class={`
|
||||
flex
|
||||
flex-col
|
||||
items-center
|
||||
justify-center
|
||||
`}
|
||||
id="form"
|
||||
>
|
||||
<div>
|
||||
<label class={`
|
||||
input
|
||||
w-70
|
||||
m-3
|
||||
`}>
|
||||
<span class="label">@</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={Locale({ text: "username" }, c.get("language"))}
|
||||
name="username"
|
||||
minlength={3}
|
||||
maxlength={20}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<Help
|
||||
text={Locale({ text: "idHelp" }, c.get("language"))}
|
||||
lang={c.get("language")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class={`
|
||||
input
|
||||
w-70
|
||||
m-3
|
||||
`}>
|
||||
<span class="label">
|
||||
<Icon
|
||||
icon="material-symbols:mail"
|
||||
color="#A4A7A9"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="info@mail.example.com"
|
||||
name="email"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<Help
|
||||
text={Locale({ text: "mailHelp" }, c.get("language"))}
|
||||
lang={c.get("language")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class={`
|
||||
input
|
||||
w-70
|
||||
m-3
|
||||
`}>
|
||||
<span class="label">
|
||||
<Icon
|
||||
icon="material-symbols:key-rounded"
|
||||
color="#A4A7A9"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={Locale({ text: "password" }, c.get("language"))}
|
||||
name="password"
|
||||
minlength={8}
|
||||
maxlength={15}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<Help
|
||||
text={Locale({ text: "passwordHelp" }, c.get("language"))}
|
||||
lang={c.get("language")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="label mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
name="agreeRule"
|
||||
required
|
||||
/>
|
||||
{toComponent(Locale({ text: "agreeRule" }, c.get("language")))}
|
||||
</label>
|
||||
|
||||
{TurnstileEnabled && (
|
||||
<Turnstile
|
||||
sitekey={TurnstileKeys[0]}
|
||||
button={html`[name='submit']`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
class={`
|
||||
btn
|
||||
btn-primary
|
||||
m-3
|
||||
`}
|
||||
type="submit"
|
||||
name="submit"
|
||||
disabled
|
||||
>
|
||||
<Icon icon="material-symbols:person-add-rounded" />
|
||||
{Locale({ text: "Signup" }, c.get("language"))}
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script src="/js/signup.js" />
|
||||
</Layout>
|
||||
);
|
||||
});
|
||||
|
||||
export default SignUp;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}'
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "./dist",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "./",
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
],
|
||||
"removeComments": true,
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
interface developer {
|
||||
name: string;
|
||||
email: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default interface InfoAPI {
|
||||
success: true;
|
||||
|
||||
server: {
|
||||
icon: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
botprotection: {
|
||||
turnstile: boolean;
|
||||
};
|
||||
|
||||
software: {
|
||||
version: string;
|
||||
name: string;
|
||||
author: developer;
|
||||
contributors: developer[];
|
||||
};
|
||||
|
||||
counts: {
|
||||
user: number;
|
||||
community: number;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
type scope =
|
||||
"read:user" |
|
||||
"client";
|
||||
|
||||
export default scope;
|
||||
|
|
@ -0,0 +1,506 @@
|
|||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@inquirer/prompts':
|
||||
specifier: ^7.8.6
|
||||
version: 7.8.6(@types/node@24.5.2)
|
||||
'@types/node':
|
||||
specifier: ^24.5.2
|
||||
version: 24.5.2
|
||||
fs:
|
||||
specifier: 0.0.1-security
|
||||
version: 0.0.1-security
|
||||
mysql2:
|
||||
specifier: ^3.15.1
|
||||
version: 3.15.1
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
yaml:
|
||||
specifier: ^2.8.1
|
||||
version: 2.8.1
|
||||
|
||||
packages:
|
||||
|
||||
'@inquirer/ansi@1.0.0':
|
||||
resolution: {integrity: sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@inquirer/checkbox@4.2.4':
|
||||
resolution: {integrity: sha512-2n9Vgf4HSciFq8ttKXk+qy+GsyTXPV1An6QAwe/8bkbbqvG4VW1I/ZY1pNu2rf+h9bdzMLPbRSfcNxkHBy/Ydw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/confirm@5.1.18':
|
||||
resolution: {integrity: sha512-MilmWOzHa3Ks11tzvuAmFoAd/wRuaP3SwlT1IZhyMke31FKLxPiuDWcGXhU+PKveNOpAc4axzAgrgxuIJJRmLw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@10.2.2':
|
||||
resolution: {integrity: sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/editor@4.2.20':
|
||||
resolution: {integrity: sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/expand@4.0.20':
|
||||
resolution: {integrity: sha512-Dt9S+6qUg94fEvgn54F2Syf0Z3U8xmnBI9ATq2f5h9xt09fs2IJXSCIXyyVHwvggKWFXEY/7jATRo2K6Dkn6Ow==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/external-editor@1.0.2':
|
||||
resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/figures@1.0.13':
|
||||
resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@inquirer/input@4.2.4':
|
||||
resolution: {integrity: sha512-cwSGpLBMwpwcZZsc6s1gThm0J+it/KIJ+1qFL2euLmSKUMGumJ5TcbMgxEjMjNHRGadouIYbiIgruKoDZk7klw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/number@3.0.20':
|
||||
resolution: {integrity: sha512-bbooay64VD1Z6uMfNehED2A2YOPHSJnQLs9/4WNiV/EK+vXczf/R988itL2XLDGTgmhMF2KkiWZo+iEZmc4jqg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/password@4.0.20':
|
||||
resolution: {integrity: sha512-nxSaPV2cPvvoOmRygQR+h0B+Av73B01cqYLcr7NXcGXhbmsYfUb8fDdw2Us1bI2YsX+VvY7I7upgFYsyf8+Nug==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/prompts@7.8.6':
|
||||
resolution: {integrity: sha512-68JhkiojicX9SBUD8FE/pSKbOKtwoyaVj1kwqLfvjlVXZvOy3iaSWX4dCLsZyYx/5Ur07Fq+yuDNOen+5ce6ig==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/rawlist@4.1.8':
|
||||
resolution: {integrity: sha512-CQ2VkIASbgI2PxdzlkeeieLRmniaUU1Aoi5ggEdm6BIyqopE9GuDXdDOj9XiwOqK5qm72oI2i6J+Gnjaa26ejg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/search@3.1.3':
|
||||
resolution: {integrity: sha512-D5T6ioybJJH0IiSUK/JXcoRrrm8sXwzrVMjibuPs+AgxmogKslaafy1oxFiorNI4s3ElSkeQZbhYQgLqiL8h6Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/select@4.3.4':
|
||||
resolution: {integrity: sha512-Qp20nySRmfbuJBBsgPU7E/cL62Hf250vMZRzYDcBHty2zdD1kKCnoDFWRr0WO2ZzaXp3R7a4esaVGJUx0E6zvA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@3.0.8':
|
||||
resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@types/node@24.5.2':
|
||||
resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
aws-ssl-profiles@1.1.2:
|
||||
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
chardet@2.1.0:
|
||||
resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==}
|
||||
|
||||
cli-width@4.1.0:
|
||||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
fs@0.0.1-security:
|
||||
resolution: {integrity: sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==}
|
||||
|
||||
generate-function@2.3.1:
|
||||
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-property@1.0.2:
|
||||
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
|
||||
|
||||
long@5.3.2:
|
||||
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
||||
|
||||
lru-cache@7.18.3:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
lru.min@1.1.2:
|
||||
resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==}
|
||||
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
|
||||
|
||||
mute-stream@2.0.0:
|
||||
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
mysql2@3.15.1:
|
||||
resolution: {integrity: sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw==}
|
||||
engines: {node: '>= 8.0'}
|
||||
|
||||
named-placeholders@1.1.3:
|
||||
resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
seq-queue@0.0.5:
|
||||
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
sqlstring@2.3.3:
|
||||
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
typescript@5.9.2:
|
||||
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@7.12.0:
|
||||
resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
yaml@2.8.1:
|
||||
resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yoctocolors-cjs@2.1.3:
|
||||
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@inquirer/ansi@1.0.0': {}
|
||||
|
||||
'@inquirer/checkbox@4.2.4(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 1.0.0
|
||||
'@inquirer/core': 10.2.2(@types/node@24.5.2)
|
||||
'@inquirer/figures': 1.0.13
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
yoctocolors-cjs: 2.1.3
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/confirm@5.1.18(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.5.2)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/core@10.2.2(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 1.0.0
|
||||
'@inquirer/figures': 1.0.13
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
cli-width: 4.1.0
|
||||
mute-stream: 2.0.0
|
||||
signal-exit: 4.1.0
|
||||
wrap-ansi: 6.2.0
|
||||
yoctocolors-cjs: 2.1.3
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/editor@4.2.20(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.5.2)
|
||||
'@inquirer/external-editor': 1.0.2(@types/node@24.5.2)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/expand@4.0.20(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.5.2)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
yoctocolors-cjs: 2.1.3
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/external-editor@1.0.2(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
chardet: 2.1.0
|
||||
iconv-lite: 0.7.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/figures@1.0.13': {}
|
||||
|
||||
'@inquirer/input@4.2.4(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.5.2)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/number@3.0.20(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.5.2)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/password@4.0.20(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 1.0.0
|
||||
'@inquirer/core': 10.2.2(@types/node@24.5.2)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/prompts@7.8.6(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/checkbox': 4.2.4(@types/node@24.5.2)
|
||||
'@inquirer/confirm': 5.1.18(@types/node@24.5.2)
|
||||
'@inquirer/editor': 4.2.20(@types/node@24.5.2)
|
||||
'@inquirer/expand': 4.0.20(@types/node@24.5.2)
|
||||
'@inquirer/input': 4.2.4(@types/node@24.5.2)
|
||||
'@inquirer/number': 3.0.20(@types/node@24.5.2)
|
||||
'@inquirer/password': 4.0.20(@types/node@24.5.2)
|
||||
'@inquirer/rawlist': 4.1.8(@types/node@24.5.2)
|
||||
'@inquirer/search': 3.1.3(@types/node@24.5.2)
|
||||
'@inquirer/select': 4.3.4(@types/node@24.5.2)
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/rawlist@4.1.8(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.5.2)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
yoctocolors-cjs: 2.1.3
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/search@3.1.3(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.5.2)
|
||||
'@inquirer/figures': 1.0.13
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
yoctocolors-cjs: 2.1.3
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/select@4.3.4(@types/node@24.5.2)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 1.0.0
|
||||
'@inquirer/core': 10.2.2(@types/node@24.5.2)
|
||||
'@inquirer/figures': 1.0.13
|
||||
'@inquirer/type': 3.0.8(@types/node@24.5.2)
|
||||
yoctocolors-cjs: 2.1.3
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@inquirer/type@3.0.8(@types/node@24.5.2)':
|
||||
optionalDependencies:
|
||||
'@types/node': 24.5.2
|
||||
|
||||
'@types/node@24.5.2':
|
||||
dependencies:
|
||||
undici-types: 7.12.0
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
aws-ssl-profiles@1.1.2: {}
|
||||
|
||||
chardet@2.1.0: {}
|
||||
|
||||
cli-width@4.1.0: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
denque@2.1.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
fs@0.0.1-security: {}
|
||||
|
||||
generate-function@2.3.1:
|
||||
dependencies:
|
||||
is-property: 1.0.2
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-property@1.0.2: {}
|
||||
|
||||
long@5.3.2: {}
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
lru.min@1.1.2: {}
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
|
||||
mysql2@3.15.1:
|
||||
dependencies:
|
||||
aws-ssl-profiles: 1.1.2
|
||||
denque: 2.1.0
|
||||
generate-function: 2.3.1
|
||||
iconv-lite: 0.7.0
|
||||
long: 5.3.2
|
||||
lru.min: 1.1.2
|
||||
named-placeholders: 1.1.3
|
||||
seq-queue: 0.0.5
|
||||
sqlstring: 2.3.3
|
||||
|
||||
named-placeholders@1.1.3:
|
||||
dependencies:
|
||||
lru-cache: 7.18.3
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
seq-queue@0.0.5: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
sqlstring@2.3.3: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
typescript@5.9.2: {}
|
||||
|
||||
undici-types@7.12.0: {}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
yaml@2.8.1: {}
|
||||
|
||||
yoctocolors-cjs@2.1.3: {}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
-- MariaDB dump 10.19 Distrib 10.4.32-MariaDB, for Win64 (AMD64)
|
||||
--
|
||||
-- Host: 192.168.3.7 Database: peas
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 10.11.11-MariaDB-0+deb12u1
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `community`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `community`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `community` (
|
||||
`user` text NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`time` datetime(3) NOT NULL DEFAULT current_timestamp(3),
|
||||
`num` int(11) NOT NULL AUTO_INCREMENT,
|
||||
PRIMARY KEY (`num`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `config`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `config`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `config` (
|
||||
`num` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`value` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`num`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `mailverifiedcode`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `mailverifiedcode`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `mailverifiedcode` (
|
||||
`code` text NOT NULL,
|
||||
`user` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`time` datetime(3) NOT NULL DEFAULT current_timestamp(3),
|
||||
`num` int(11) NOT NULL AUTO_INCREMENT,
|
||||
PRIMARY KEY (`num`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `token`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `token`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `token` (
|
||||
`num` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user` varchar(20) NOT NULL,
|
||||
`scope` text NOT NULL,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`token` varchar(64) NOT NULL,
|
||||
PRIMARY KEY (`num`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `users`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `users` (
|
||||
`id` text NOT NULL,
|
||||
`password` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`mailverified` int(1) NOT NULL DEFAULT 0,
|
||||
`time` datetime(3) NOT NULL DEFAULT current_timestamp(3),
|
||||
`num` int(11) NOT NULL AUTO_INCREMENT,
|
||||
PRIMARY KEY (`num`),
|
||||
KEY `id` (`id`(768))
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2025-10-01 19:45:29
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
import { input, number, select, password, editor, confirm } from "@inquirer/prompts";
|
||||
import { Locale } from "../packages/web/src/lib/locale.js";
|
||||
import { getDirname } from "../packages/web/src/lib/path.js";
|
||||
import { verify as DBVerify } from "../packages/web/src/lib/database.js";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import pool from "../packages/web/src/lib/database.js";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
|
||||
async function wizard() {
|
||||
try {
|
||||
const lang = await select({
|
||||
message: "🌐",
|
||||
choices: [
|
||||
{
|
||||
name: "日本語",
|
||||
value: "ja",
|
||||
},
|
||||
{
|
||||
name: "English",
|
||||
value: "en",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const origin = await input({
|
||||
message: Locale({ text: "Q_origin" }, lang),
|
||||
default: "http://localhost:3000",
|
||||
required: true,
|
||||
});
|
||||
const port = await number({
|
||||
message: Locale({ text: "Q_port" }, lang),
|
||||
min: 0,
|
||||
max: 65535,
|
||||
default: 3000,
|
||||
required: true,
|
||||
});
|
||||
|
||||
let db: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
db?: string;
|
||||
} = {};
|
||||
db.host = await input({
|
||||
message: Locale({ text: "Q_db_host" }, lang),
|
||||
default: "db.example.com",
|
||||
required: true,
|
||||
});
|
||||
db.port = await number({
|
||||
message: Locale({ text: "Q_db_port" }, lang),
|
||||
min: 0,
|
||||
max: 65535,
|
||||
default: 3306,
|
||||
required: true,
|
||||
});
|
||||
db.user = await input({
|
||||
message: Locale({ text: "Q_db_user" }, lang),
|
||||
default: "root",
|
||||
required: true,
|
||||
});
|
||||
db.pass = await password({
|
||||
message: Locale({ text: "Q_db_pass" }, lang),
|
||||
mask: "*",
|
||||
});
|
||||
db.db = await input({
|
||||
message: Locale({ text: "Q_db_db" }, lang),
|
||||
default: "peas",
|
||||
required: true,
|
||||
});
|
||||
|
||||
await DBVerify({
|
||||
host: db.host,
|
||||
port: db.port,
|
||||
user: db.user,
|
||||
password: db.pass,
|
||||
database: db.db,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 0,
|
||||
});
|
||||
|
||||
writeFileSync(`${getDirname(import.meta.url)}../packages/web/config/peas.config.ts`,
|
||||
`import type ConfigType from "./config";
|
||||
|
||||
const Config: ConfigType = {
|
||||
server: {
|
||||
port: ${String(port)},
|
||||
origin: "${origin}",
|
||||
},
|
||||
database: {
|
||||
host: "${db.host}",
|
||||
port: ${String(db.port)},
|
||||
user: "${db.user}",
|
||||
pass: "${db.pass}",
|
||||
db: "${db.db}",
|
||||
},
|
||||
}
|
||||
|
||||
export default Config;`, "utf-8");
|
||||
|
||||
console.log(Locale({ text: "CLI_config_file_write_success" }, lang));
|
||||
|
||||
const sql = readFileSync(`${getDirname(import.meta.url)}peas.sql`, "utf-8");
|
||||
await pool.query<RowDataPacket[]>(sql);
|
||||
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('server_name', ?)",
|
||||
[await input({
|
||||
message: Locale({ text: "serverName" }, lang),
|
||||
default: "Peas",
|
||||
required: true,
|
||||
})]
|
||||
);
|
||||
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('server_description', ?)",
|
||||
[await editor({
|
||||
message: Locale({ text: "serverDescription" }, lang),
|
||||
default: "Peas server",
|
||||
})]
|
||||
);
|
||||
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('server_icon', ?)",
|
||||
[await input({
|
||||
message: Locale({ text: "serverIconURL" }, lang),
|
||||
default: `${origin}/peas.svg`,
|
||||
required: true,
|
||||
})]
|
||||
);
|
||||
|
||||
const turnstileIsEnabled = await confirm({
|
||||
message: Locale({ text: "Q_turnstile_is_enabled" }, lang),
|
||||
default: true,
|
||||
});
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('turnstile_isEnabled', ?)",
|
||||
[turnstileIsEnabled ? "1" : "0"]
|
||||
);
|
||||
if (turnstileIsEnabled) {
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('turnstile_sitekey', ?)",
|
||||
[await input({
|
||||
message: Locale({ text: "Q_turnstile_sitekey" }, lang),
|
||||
default: "1x00000000000000000000AA",
|
||||
required: true,
|
||||
})]
|
||||
);
|
||||
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('turnstile_secret', ?)",
|
||||
[await password({
|
||||
message: Locale({ text: "Q_turnstile_secret" }, lang),
|
||||
mask: "*",
|
||||
})]
|
||||
);
|
||||
}
|
||||
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('smtp_host', ?)",
|
||||
[await input({
|
||||
message: Locale({ text: "Q_smtp_host" }, lang),
|
||||
default: "smtp.mail.example.com",
|
||||
required: true,
|
||||
})]
|
||||
);
|
||||
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('smtp_port', ?)",
|
||||
[String(await number({
|
||||
message: Locale({ text: "Q_smtp_port" }, lang),
|
||||
default: 465,
|
||||
min: 0,
|
||||
max: 65535,
|
||||
required: true,
|
||||
}))]
|
||||
);
|
||||
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('smtp_user', ?)",
|
||||
[await input({
|
||||
message: Locale({ text: "Q_smtp_user" }, lang),
|
||||
default: "peas@mail.example.com",
|
||||
required: true,
|
||||
})]
|
||||
);
|
||||
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('smtp_password', ?)",
|
||||
[await password({
|
||||
message: Locale({ text: "Q_smtp_pass" }, lang),
|
||||
mask: "*",
|
||||
})]
|
||||
);
|
||||
|
||||
await pool.execute<RowDataPacket[]>(
|
||||
"INSERT INTO `config` (`name`, `value`) VALUES ('smtp_secure', ?)",
|
||||
[await confirm({
|
||||
message: Locale({ text: "Q_smtp_secure" }, lang),
|
||||
default: true,
|
||||
}) ? "1": "0"]
|
||||
);
|
||||
|
||||
console.log(Locale({ text: "CLISETUP_success" }, lang));
|
||||
process.exit();
|
||||
} catch (err: any) {
|
||||
if (err.name === 'ExitPromptError') {
|
||||
process.exit();
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wizard();
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "./",
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
],
|
||||
"removeComments": true,
|
||||
},
|
||||
"include": [
|
||||
"scripts/**/*"
|
||||
],
|
||||
}
|
||||
Loading…
Reference in New Issue