configファイルに移行・多言語化完全対応・manifest系対応・PWA対応・ロード表示・アカウント初期化処理完成・mailsend.tsの型を修正

This commit is contained in:
Last2014 2025-07-13 01:06:42 +09:00
parent 8d7a968c75
commit ebacf6e1af
30 changed files with 987 additions and 554 deletions

View File

@ -1,13 +0,0 @@
// DATABASE(MySQL/MariaDB)
DB_HOST=
DB_PORT=
DB_USER=
DB_PASSWORD=
DB_DATABASE=
// MAIL(SMTP)
SMTP_HOST=
SMTP_PORT=
SMTP_SECURE=
SMTP_USER=
SMTP_PASSWORD=

7
.gitignore vendored
View File

@ -9,6 +9,7 @@
!.yarn/plugins !.yarn/plugins
!.yarn/releases !.yarn/releases
!.yarn/versions !.yarn/versions
/package-lock.json
# testing # testing
/coverage /coverage
@ -31,8 +32,7 @@ yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env .env*
.env.local
# vercel # vercel
.vercel .vercel
@ -45,3 +45,6 @@ next-env.d.ts
/.vscode/ /.vscode/
/.trae/ /.trae/
/.vs/ /.vs/
# config
/peas.config.ts

View File

@ -1,10 +1,9 @@
<center>
# Peas # Peas
<img width="140" style="border-radius: 100%" src="./src/asset/peas.svg"> <img width="140" src="./src/asset/peas.svg">
## Technologys ## Technologys
<img width="30" src="./src/asset/simpleicons/html5.svg"> <img width="30" src="./src/asset/simpleicons/html5.svg">
<img width="30" src="./src/asset/simpleicons/css.svg"> <img width="30" src="./src/asset/simpleicons/css.svg">
<img width="30" src="./src/asset/simpleicons/js.svg"> <img width="30" src="./src/asset/simpleicons/js.svg">
@ -20,11 +19,12 @@
<img width="30" src="./src/asset/simpleicons/svg.svg"> <img width="30" src="./src/asset/simpleicons/svg.svg">
## About Peas ## About Peas
Peas is a SNS designed for sharing profiles. Peas is a SNS designed for sharing profiles.
You can register and share your user profile. You can register and share your user profile.
You can freely create profile sections suitable for use at school or work, and also change the privacy settings. You can freely create profile sections suitable for use at school or work, and also change the privacy settings.
It also supports printing QR codes, making sharing easy. It also supports printing QR codes, making sharing easy.
## LICENSE ## LICENSE
AGPL-v3.0 AGPL-v3.0
</center>

29
examples/peas.config.ts Normal file
View File

@ -0,0 +1,29 @@
import { PeasConfigType } from "@/lib/peas.config";
const PeasConfig: PeasConfigType = {
// Server Config
serverName: "Peas Server",
serverDescription: "Peas SNS",
serverHost: "peas.example.com",
// SSL Config
ssl: {
ssl: false,
},
// SMTP Config
smtpHost: "mail.example.com",
smtpPort: 587,
smtpSecure: false,
smtpUser: "info@mail.example.com",
smtpPassword: "SMTPMailPassword",
// Database Config
databaseHost: "db.example.com",
databasePort: 3306,
databaseUser: "peas",
databasePassword: "DatabasePassword",
databaseDB: "peas",
};
export default PeasConfig;

108
package-lock.json generated
View File

@ -1,14 +1,15 @@
{ {
"name": "peas", "name": "peas",
"version": "1.0", "version": "nonerelease",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "peas", "name": "peas",
"version": "1.0", "version": "nonerelease",
"license": "ISC", "license": "AGPL-3.0-later",
"dependencies": { "dependencies": {
"@iconify/react": "^6.0.0",
"@types/dotenv": "^6.1.1", "@types/dotenv": "^6.1.1",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
@ -19,7 +20,7 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"mysql2": "^3.14.1", "mysql2": "^3.14.1",
"next": "^15.3.2", "next": "^15.3.4",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@ -289,6 +290,27 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@iconify/react": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@iconify/react/-/react-6.0.0.tgz",
"integrity": "sha512-eqNscABVZS8eCpZLU/L5F5UokMS9mnCf56iS1nM9YYHdH8ZxqZL9zyjSwW60IOQFsXZkilbBiv+1paMXBhSQnw==",
"license": "MIT",
"dependencies": {
"@iconify/types": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/cyberalien"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
"license": "MIT"
},
"node_modules/@img/sharp-darwin-arm64": { "node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.2", "version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz",
@ -724,9 +746,9 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "15.3.2", "version": "15.3.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.4.tgz",
"integrity": "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==", "integrity": "sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
@ -740,9 +762,9 @@
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "15.3.2", "version": "15.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.4.tgz",
"integrity": "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==", "integrity": "sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -756,9 +778,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "15.3.2", "version": "15.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.4.tgz",
"integrity": "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==", "integrity": "sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -772,9 +794,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.3.2", "version": "15.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.4.tgz",
"integrity": "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==", "integrity": "sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -788,9 +810,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "15.3.2", "version": "15.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.4.tgz",
"integrity": "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==", "integrity": "sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -804,9 +826,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "15.3.2", "version": "15.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.4.tgz",
"integrity": "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==", "integrity": "sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -820,9 +842,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "15.3.2", "version": "15.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.4.tgz",
"integrity": "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==", "integrity": "sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -836,9 +858,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.3.2", "version": "15.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.4.tgz",
"integrity": "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==", "integrity": "sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -852,9 +874,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.2", "version": "15.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.4.tgz",
"integrity": "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==", "integrity": "sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4349,12 +4371,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/next": { "node_modules/next": {
"version": "15.3.2", "version": "15.3.4",
"resolved": "https://registry.npmjs.org/next/-/next-15.3.2.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.3.4.tgz",
"integrity": "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==", "integrity": "sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/env": "15.3.2", "@next/env": "15.3.4",
"@swc/counter": "0.1.3", "@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
"busboy": "1.6.0", "busboy": "1.6.0",
@ -4369,14 +4391,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0" "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "15.3.2", "@next/swc-darwin-arm64": "15.3.4",
"@next/swc-darwin-x64": "15.3.2", "@next/swc-darwin-x64": "15.3.4",
"@next/swc-linux-arm64-gnu": "15.3.2", "@next/swc-linux-arm64-gnu": "15.3.4",
"@next/swc-linux-arm64-musl": "15.3.2", "@next/swc-linux-arm64-musl": "15.3.4",
"@next/swc-linux-x64-gnu": "15.3.2", "@next/swc-linux-x64-gnu": "15.3.4",
"@next/swc-linux-x64-musl": "15.3.2", "@next/swc-linux-x64-musl": "15.3.4",
"@next/swc-win32-arm64-msvc": "15.3.2", "@next/swc-win32-arm64-msvc": "15.3.4",
"@next/swc-win32-x64-msvc": "15.3.2", "@next/swc-win32-x64-msvc": "15.3.4",
"sharp": "^0.34.1" "sharp": "^0.34.1"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "peas", "name": "peas",
"version": "1.0", "version": "nonerelease",
"license": "AGPL-3.0-later", "license": "AGPL-3.0-later",
"author": { "author": {
"name": "Last2014", "name": "Last2014",
@ -15,6 +15,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@iconify/react": "^6.0.0",
"@types/dotenv": "^6.1.1", "@types/dotenv": "^6.1.1",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
@ -25,7 +26,7 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"mysql2": "^3.14.1", "mysql2": "^3.14.1",
"next": "^15.3.2", "next": "^15.3.4",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

View File

@ -1,27 +1,33 @@
import pool from '@/lib/database'; import pool from "@/lib/database";
import type { RowDataPacket } from 'mysql2'; import type { RowDataPacket } from "mysql2";
import { NextResponse, NextRequest } from 'next/server'; import { NextResponse, NextRequest } from "next/server";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const body = await request.json(); const body = await request.json();
const { code } = body; const { code } = body;
const [rows] = await pool.query<RowDataPacket[]>( const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM mailverifiedcode WHERE code = ?', "SELECT * FROM mailverifiedcode WHERE code = ?",
[code] [code],
); );
if (rows.length === 0) { if (rows.length === 0) {
return NextResponse.json({status: "error", message: 'Code not found' }, { status: 401 }); return NextResponse.json(
}else{ { status: "error", message: "Code not found" },
if(rows[0].code === code){ { status: 401 },
);
} else {
if (rows[0].code === code) {
await pool.query<RowDataPacket[]>( await pool.query<RowDataPacket[]>(
'UPDATE `users` SET `mailverified` = 1 WHERE `users`.`id` = ?', "UPDATE `users` SET `mailverified` = 1 WHERE `users`.`id` = ?",
[rows[0].user] [rows[0].user],
); );
return NextResponse.json({status: "success", message: 'Code verified' }, { status: 200 }); return NextResponse.json(
{ status: "success", message: "Code verified" },
{ status: 200 },
);
} }
} }
} }

View File

@ -1,16 +1,18 @@
import pool from '@/lib/database'; import pool from "@/lib/database";
import type { RowDataPacket } from 'mysql2'; import type { RowDataPacket } from "mysql2";
import { sendMail } from '@/lib/mailsend'; import { sendMail } from "@/lib/mailsend";
import PeasConfig from "@/../peas.config";
import { NextResponse, NextRequest } from 'next/server'; import { NextResponse, NextRequest } from "next/server";
// コード生成 // コード生成
async function createCode(maxAttempts = 10) { async function createCode(maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) { for (let i = 0; i < maxAttempts; i++) {
const code = Math.floor(100000 + Math.random() * 900000).toString(); const code = Math.floor(100000 + Math.random() * 900000).toString();
const [existingCodes] = await pool.execute<RowDataPacket[]>( const [existingCodes] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM mailverifiedcode WHERE code = ?",[code] "SELECT * FROM mailverifiedcode WHERE code = ?",
[code],
); );
if (existingCodes.length === 0) { if (existingCodes.length === 0) {
return code; return code;
@ -28,44 +30,49 @@ export async function POST(request: NextRequest) {
// コードが存在する場合 // コードが存在する場合
const [existingCodes] = await pool.execute<RowDataPacket[]>( const [existingCodes] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM mailverifiedcode WHERE user = ?",[user] "SELECT * FROM mailverifiedcode WHERE user = ?",
[user],
); );
let row: RowDataPacket[] = []; let row: RowDataPacket[] = [];
if(existingCodes.length !== 0){ if (existingCodes.length !== 0) {
[row] = await pool.execute<RowDataPacket[]>( [row] = await pool.execute<RowDataPacket[]>(
"UPDATE `mailverifiedcode` SET `code` = ?, `time` = current_timestamp(3) WHERE `mailverifiedcode`.`user` = ?", "UPDATE `mailverifiedcode` SET `code` = ?, `time` = current_timestamp(3) WHERE `mailverifiedcode`.`user` = ?",
[code, user] [code, user],
) );
}else{ } else {
// コードが存在しない場合 // コードが存在しない場合
[row] = await pool.execute<RowDataPacket[]>( [row] = await pool.execute<RowDataPacket[]>(
"INSERT INTO `mailverifiedcode` (`code`, `user`, `email`, `time`, `num`) VALUES (?, ?, ?, current_timestamp(3), NULL);", "INSERT INTO `mailverifiedcode` (`code`, `user`, `email`, `time`, `num`) VALUES (?, ?, ?, current_timestamp(3), NULL);",
[code, user, email] [code, user, email],
) );
} }
// メール送信 // メール送信
if ((row as RowDataPacket).affectedRows === 1) { if ((row as RowDataPacket).affectedRows === 1) {
await sendMail({ await sendMail({
to: email, to: email,
subject: "【Peas】メール認証", subject: `${PeasConfig.serverName}】メール認証`,
text: `${user}さんこんにちは。\n text: `
Peasではメール認証が必要となっています\n ${user}\n
Peasの機能を利用することができます\n ${PeasConfig.serverName}\n
${request.nextUrl.origin}/mailverify?code=${code} ` ${PeasConfig.serverName}\n
}) ${request.nextUrl.origin}/mailverify?code=${code} `,
});
return NextResponse.json({ return NextResponse.json({
status: "success", status: "success",
code: code, code: code,
}) });
}else{ } else {
// コード保存失敗 // コード保存失敗
return NextResponse.json({ return NextResponse.json(
{
status: "error", status: "error",
error: "Failed to Save Code" error: "Failed to Save Code",
}, { status: 400 }); },
{ status: 400 },
);
} }
} }

View File

@ -1,47 +1,57 @@
import pool from '@/lib/database'; import pool from "@/lib/database";
import type { RowDataPacket } from 'mysql2'; import type { RowDataPacket } from "mysql2";
import bcrypt from 'bcrypt'; import bcrypt from "bcrypt";
import { NextResponse, NextRequest } from 'next/server'; import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers' import { cookies } from "next/headers";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
// body取得 // body取得
const body = await request.json(); const body = await request.json();
const { email, password } = body; const { email, password } = body;
// ユーザー取得 // ユーザー取得
const [existingUsers] = await pool.execute<RowDataPacket[]>( const [existingUsers] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM users WHERE email = ?", [email] "SELECT * FROM users WHERE email = ?",
[email],
); );
// ユーザーが存在しない場合 // ユーザーが存在しない場合
if (existingUsers.length === 0) { if (existingUsers.length === 0) {
return NextResponse.json({ return NextResponse.json(
{
status: "error", status: "error",
error: "User not found" error: "User not found",
}, { status: 404 }); },
{ status: 404 },
);
} }
const user = existingUsers[0]; const user = existingUsers[0];
const passwordMatch = await bcrypt.compare(password, user.password); const passwordMatch = await bcrypt.compare(password, user.password);
// パスワード確認 // パスワード確認
if (!passwordMatch) { if (!passwordMatch) {
return NextResponse.json({ return NextResponse.json(
{
status: "error", status: "error",
error: "Incorrect password" error: "Incorrect password",
}, { status: 401 }); },
{ status: 401 },
);
} else { } else {
// 成功 // 成功
const sessionCookie = await cookies(); const sessionCookie = await cookies();
sessionCookie.set('user', user.id); sessionCookie.set("user", user.id);
sessionCookie.set('password', password); sessionCookie.set("password", password);
return NextResponse.json({ return NextResponse.json(
{
status: "success", status: "success",
message: "Login successful" message: "Login successful",
}, { status: 200 }); },
{ status: 200 },
);
} }
} }

View File

@ -1,87 +1,114 @@
import pool from '@/lib/database'; import pool from "@/lib/database";
import type { RowDataPacket } from 'mysql2'; import type { RowDataPacket } from "mysql2";
import bcrypt from 'bcrypt'; import bcrypt from "bcrypt";
import { NextResponse, NextRequest } from 'next/server'; import { NextResponse, NextRequest } from "next/server";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const body = await request.json(); const body = await request.json();
const { email, password, id, rule } = body; const { email, password, id, rule } = body;
try{ try {
if(!rule || rule === false){ if (!rule || rule === false) {
return NextResponse.json({ return NextResponse.json(
{
status: "error", status: "error",
error: "Please read and accept the terms and conditions" error: "Please read and accept the terms and conditions",
}, { status: 400 }); },
{ status: 400 },
);
} }
}catch(e){ } catch (e) {
console.log(e); console.log(e);
} }
// bodyが空になっていないか // bodyが空になっていないか
if(!email || !password || !id || !rule){ if (!email || !password || !id || !rule) {
return NextResponse.json({ return NextResponse.json(
{
status: "error", status: "error",
error: "Body Required" error: "Body Required",
}, { status: 400 }); },
{ status: 400 },
);
} }
// パスワードが八文字以上か // パスワードが8文字以上か
if(password.length < 8){ if (password.length < 8) {
return NextResponse.json({ return NextResponse.json(
{
status: "error", status: "error",
error: "Password must be at least 8 characters long" error: "Password must be at least 8 characters long",
}, { status: 400 }); },
{ status: 400 },
);
} }
// ユーザー名が英数字3文字以上か // ユーザー名が英数字3文字以上か
if(!/^[a-zA-Z0-9]{3,}$/.test(id)){ if (!/^[a-zA-Z0-9]{3,}$/.test(id)) {
return NextResponse.json({ return NextResponse.json(
{
status: "error", status: "error",
error: "Username must be at least 3 alphanumeric characters" error: "Username must be at least 3 alphanumeric characters",
}, { status: 400 }); },
{ status: 400 },
);
} }
const [existingUsers] = await pool.execute<RowDataPacket[]>( const [existingUsers] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM users WHERE id = ?",[id] "SELECT * FROM users WHERE id = ?",
[id],
); );
if(existingUsers.length === 0){ if (existingUsers.length === 0) {
const passwordHash = await bcrypt.hash(password, 10); const passwordHash = await bcrypt.hash(password, 10);
const [result] = await pool.execute<RowDataPacket[]>( const [result] = await pool.execute<RowDataPacket[]>(
"INSERT INTO `users` (`id`, `password`, `email`, `mailverified`, `time`, `num`) VALUES (?, ?, ?, '0', current_timestamp(3), NULL)", "INSERT INTO `users` (`id`, `password`, `email`, `mailverified`, `time`, `num`) VALUES (?, ?, ?, '0', current_timestamp(3), NULL)",
[id, passwordHash, email] [id, passwordHash, email],
); );
if ((result as RowDataPacket).affectedRows === 1) { if ((result as RowDataPacket).affectedRows === 1) {
await fetch(request.nextUrl.protocol + request.nextUrl.host + "/api/mailverified/send", { await fetch(
request.nextUrl.protocol +
request.nextUrl.host +
"/api/mailverified/send",
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
email: email, email: email,
user: id, user: id,
}) }),
}); },
);
return NextResponse.json({ return NextResponse.json(
{
status: "success", status: "success",
message: "User created successfully" message: "User created successfully",
}, { status: 201 }); },
{ status: 201 },
);
} else { } else {
return NextResponse.json({ return NextResponse.json(
{
status: "error", status: "error",
error: "Failed to create user" error: "Failed to create user",
}, { status: 500 }); },
{ status: 500 },
);
} }
}else{ } else {
return NextResponse.json({ return NextResponse.json(
{
status: "error", status: "error",
error: "User already exists" error: "User already exists",
}, { status: 400 }); },
{ status: 400 },
);
} }
} }

2
src/app/bulma.css vendored
View File

@ -1 +1 @@
@import 'bulma/css/bulma.css'; @import "bulma/css/bulma.min.css";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -2,9 +2,11 @@ import type { Metadata } from "next";
import "./bulma.css"; import "./bulma.css";
import "./global.css"; import "./global.css";
import PeasConfig from "@/../peas.config";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Peas", title: PeasConfig.serverName,
description: "Peas SNS", description: PeasConfig.serverDescription,
}; };
export default function RootLayout({ export default function RootLayout({
@ -14,13 +16,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html> <html>
<body> <body>{children}</body>
<header className="header">
<span className="title">Peas</span>
</header>
{children}
</body>
</html> </html>
); );
} }

View File

@ -1,28 +1,38 @@
"use client"; "use client";
import { useState } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { Icon } from "@iconify/react";
import { MessageLang } from "@/lang/component/client";
import { MessageLangVar } from "@/lang/component/variable";
export default function MailVerify() { export default function MailVerify() {
const [loading, setLoading] = useState(false);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const URLCode = searchParams.get("code"); const URLCode = searchParams.get("code");
let code: string = ""; let code: string = "";
if(URLCode){ if (URLCode !== null) {
code = URLCode; code = URLCode;
} }
const verify = async (e: React.FormEvent<HTMLFormElement>) => { const verify = async (e: React.FormEvent<HTMLFormElement>) => {
if(code === ""){ e.preventDefault();
setLoading(true);
if (code === "") {
code = e.currentTarget.code.value; code = e.currentTarget.code.value;
} }
if (code.length !== 6) { if (code.length !== 6) {
alert("コードは6桁で入力してください。"); alert(MessageLangVar({text: "codeCharacterCount"}));
return; return;
} }
const res = await fetch(`${location.origin}/api/mailverify/check`, { try {
const res = await fetch(`${location.origin}/api/mailverified/check`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -31,23 +41,50 @@ export default function MailVerify() {
code, code,
}), }),
}); });
const data = await res.text(); const data = await res.text();
const result = JSON.parse(data); const result = JSON.parse(data);
if (result.status === "success") { if (result.status === "success") {
alert("メール認証に成功しました。\nログインしてください。"); alert(MessageLangVar({ text: "emailVerificationSuccess" }));
window.location.href = "/login"; window.location.href = "/signin";
}
} catch (err) {
console.error(err)
} finally {
setLoading(false);
} }
}; };
return ( return (
<> <>
<h1 className="title"></h1> <h1 className="title has-text-centered"><MessageLang text="mailVerify" /></h1>
<form onSubmit={verify}> {loading && (
<label className="label"></label> <div style={{ marginBottom: "1rem" }}>
<input type="text" name="code" defaultValue={code} className="input" placeholder="123456" style={{width: "10rem"}} maxLength={6} /> <Icon icon="svg-spinners:6-dots-scale" width={32} height={32} />
</div>
)}
<form onSubmit={verify} className="has-text-centered">
<label className="label"><MessageLang text="code" />:</label>
<input
type="text"
name="code"
defaultValue={code}
className="input"
placeholder="123456"
style={{
width: "10rem",
marginBottom: "10px"
}}
maxLength={6}
/>
<br />
<button className="button" type="submit">
<MessageLang text="submit" />
</button>
</form> </form>
</> </>
) );
} }

20
src/app/manifest.ts Normal file
View File

@ -0,0 +1,20 @@
import type { MetadataRoute } from "next";
import PeasConfig from "@/../peas.config";
export default function manifest(): MetadataRoute.Manifest {
return {
name: PeasConfig.serverName,
description: PeasConfig.serverDescription,
start_url: "/",
display: "standalone",
background_color: "#fff",
theme_color: "#fff",
icons: [
{
src: "/favicon.ico",
sizes: "any",
type: "image/x-icon",
},
],
};
}

View File

@ -1,26 +1,54 @@
"use client"; "use client";
import "./style.css" import "./style.css";
import Link from "next/link"; import Link from "next/link";
import { Icon } from "@iconify/react";
import PeasConfig from "@/../peas.config";
import { MessageLang } from "@/lang/component/client"; import { MessageLang } from "@/lang/component/client";
export default function Home() { export default function Home() {
return ( return (
<> <>
<h1 className="has-text-centered title">{PeasConfig.serverName}</h1>
<div className="message about"> <div className="message about">
<h1 className="message-header"><MessageLang text="PeasAboutTitle" /></h1> <h1 className="message-header">
<Icon icon="ix:about-filled" />
<MessageLang className="peasAboutTitle" text="ServerAboutTitle" />
</h1>
<p className="message-body">{PeasConfig.serverDescription}</p>
</div>
<div className="message about">
<h1 className="message-header">
<Icon icon="ix:about-filled" />
<MessageLang className="peasAboutTitle" text="PeasAboutTitle" />
</h1>
<p className="message-body"> <p className="message-body">
<MessageLang text="PeasAboutText" /> <MessageLang text="PeasAboutText" />
</p> </p>
</div> </div>
<div style={{width: "15rem"}}> <div className="has-text-centered">
<Link href="/signup" className="card card-footer card-footer-item signup"><MessageLang text="Start" /></Link> <Link
<p className="has-text-centered"><MessageLang text="or" /></p> href="/signup"
<Link href="/signin" className="card card-footer card-footer-item signin"><MessageLang text="Signin" /></Link> className="card card-footer card-footer-item signup"
>
<MessageLang text="Start" />
</Link>
<p className="has-text-centered">
<MessageLang text="or" />
</p>
<Link
href="/signin"
className="card card-footer card-footer-item signin"
>
<MessageLang text="Signin" />
</Link>
</div> </div>
</> </>
); );

20
src/app/robots.ts Normal file
View File

@ -0,0 +1,20 @@
import type { MetadataRoute } from "next";
import PeasConfig from "../../peas.config";
let protocol: string;
if (PeasConfig.ssl.ssl) {
protocol = "https";
} else {
protocol = "http";
}
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: `${protocol}://${PeasConfig.serverHost}/sitemap.xml`,
};
}

View File

@ -1,13 +1,20 @@
"use client"; "use client";
import "./style.css"; import "./style.css";
import { useState } from "react";
import { MessageLang } from "@/lang/component/client"; import { MessageLang } from "@/lang/component/client";
import { Icon } from "@iconify/react";
export default function SignIn() { export default function SignIn() {
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setLoading(true);
try {
const res = await fetch("/api/signin", { const res = await fetch("/api/signin", {
method: "POST", method: "POST",
headers: { headers: {
@ -23,24 +30,64 @@ export default function SignIn() {
if (json.status === "success") { if (json.status === "success") {
window.location.href = "/"; window.location.href = "/";
} else {
setError(true);
window.location.replace("/signin?error=true");
} }
} catch (err) {
console.log(err);
setError(true);
window.location.replace("/signin?error=true");
} finally {
setLoading(false);
} }
};
useState(() => {
const locationURL = new URL(window.location.href);
if (locationURL.searchParams.get("error")) {
setError(true);
}
});
return ( return (
<form onSubmit={FormSubmit}> <form onSubmit={FormSubmit} className="has-text-centered">
<h1 className="title"><MessageLang text="Signin" /></h1> {error && (
<div className="message is-danger">
<div className="message-body">
<Icon icon="iconoir:warning-circle-solid" />
<MessageLang text="signinError" />
</div>
</div>
)}
<label className="label"><MessageLang text="email" /></label> <h1 className="title">
<MessageLang text="Signin" />
</h1>
{loading && (
<div style={{ marginBottom: "1rem" }}>
<Icon icon="svg-spinners:6-dots-scale" width={32} height={32} />
</div>
)}
<label className="label">
<MessageLang text="email" />
</label>
<input type="email" name="email" className="input" required /> <input type="email" name="email" className="input" required />
<br /> <br />
<label className="label"><MessageLang text="password" /></label> <label className="label">
<MessageLang text="password" />
</label>
<input type="password" name="password" className="input" required /> <input type="password" name="password" className="input" required />
<br /> <br />
<button type="submit" className="button"><MessageLang text="Signin" /></button> <button type="submit" className="button">
<MessageLang text="Signin" />
</button>
</form> </form>
) );
} }

View File

@ -1,7 +1,7 @@
.input{ .input {
width: 20rem; width: 20rem;
} }
.button{ .button {
margin-top: 5px; margin-top: 5px;
} }

View File

@ -1,15 +1,22 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react";
import { Icon } from "@iconify/react";
import "./style.css"; import "./style.css";
import { MessageLang } from "@/lang/component/client"; import { MessageLang } from "@/lang/component/client";
import { MessageLangVar } from "@/lang/component/variable";
export default function SignUp() { export default function SignUp() {
const [loading, setLoading] = useState(false);
const FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setLoading(true);
try {
const res = await fetch("/api/signup", { const res = await fetch("/api/signup", {
method: "POST", method: "POST",
headers: { headers: {
@ -22,56 +29,146 @@ export default function SignUp() {
rule: e.currentTarget.ruleCheck.checked, rule: e.currentTarget.ruleCheck.checked,
}), }),
}); });
const data = await res.text(); const data = await res.text();
const json = JSON.parse(data); const json = JSON.parse(data);
if (json.status === "success") { if (json.status === "success") {
alert("アカウントの作成に成功しました。\nご登録いただいたメールアドレスに確認用メールを送信しました。\n確認メールをクリックしてアカウントを有効化してください。") alert(MessageLangVar({ text: "accountCreateSuccess" }));
window.location.href = "/"; window.location.href = "/";
}else{ } else {
if(json.error === "User already exists"){ if (json.error === "User already exists") {
alert("同じユーザー名が既に使用されています") alert(MessageLangVar({ text: "sameUsername" }));
}else if(json.error === "Please read and accept the terms and conditions"){ } else if (
alert("利用規約とプライバシーポリシーに同意してください") json.error === "Please read and accept the terms and conditions"
}else if(json.error === "Body Required"){ ) {
alert("Bodyは必須です") alert(MessageLangVar({ text: "termsAgreement" }));
}else if(json.error === "Password must be at least 8 characters long"){ } else if (json.error === "Body Required") {
alert("パスワードは8文字以上にしてください") alert(MessageLangVar({ text: "bodyRequired" }));
}else if(json.error === "Username must be at least 3 alphanumeric characters"){ } else if (
alert("ユーザー名は3文字以上の英数字のIDにしてください") json.error === "Password must be at least 8 characters long"
}else if(json.error === "Failed to create user"){ ) {
alert("ユーザーの作成に失敗しました") alert(MessageLangVar({ text: "passwordCharacterLimit" }));
} else if (
json.error === "Username must be at least 3 alphanumeric characters"
) {
alert(MessageLangVar({ text: "usernameRestriction" }));
} else if (json.error === "Failed to create user") {
alert(MessageLangVar({ text: "userCreationFailed" }));
} }
} }
} catch (err) {
alert(`${MessageLangVar({ text: "connectionError" })}: ${err}`);
} finally {
setLoading(false);
} }
};
return ( return (
<form onSubmit={FormSubmit}> <div
<h1 className="title"><MessageLang text="Signup" /></h1> className="has-text-centered"
style={{ maxWidth: "400px", margin: "2rem auto" }}
>
<form onSubmit={FormSubmit} style={{ width: "100%" }}>
<h1 className="title">
<MessageLang text="Signup" />
</h1>
<label className="label"><MessageLang text="username" /></label> {loading && (
<div className="field" style={{ position: 'relative' }}> <div style={{ marginBottom: "1rem" }}>
<span className="input-out">@</span> <Icon icon="svg-spinners:6-dots-scale" width={32} height={32} />
<input type="text" name="username" placeholder="username" className="input" style={{ paddingLeft: '25px' }} required />
</div> </div>
<p className="help"><MessageLang text="idHelp" /></p> )}
<label className="label"><MessageLang text="email" /></label> <label className="label">
<input className="input" type="email" name="email" placeholder="info@example.com" required /> <MessageLang text="username" />
<p className="help"><MessageLang text="mailHelp" /></p> </label>
<div className="field" style={{ position: "relative", width: "100%" }}>
<span
className="input-out"
style={{
position: "absolute",
left: "10px",
top: "50%",
transform: "translateY(-50%)",
pointerEvents: "none",
}}
>
@
</span>
<input
type="text"
name="username"
placeholder="username"
className="input"
style={{ paddingLeft: "30px", width: "100%" }}
required
disabled={loading}
/>
</div>
<p className="help">
<MessageLang text="idHelp" />
</p>
<label className="label"><MessageLang text="password" /></label> <label className="label">
<input type="password" name="password" className="input" placeholder="password" required /> <MessageLang text="email" />
<p className="help"><MessageLang text="passwordHelp" /></p> </label>
<input
className="input"
type="email"
name="email"
placeholder="info@example.com"
style={{ width: "100%" }}
required
disabled={loading}
/>
<p className="help">
<MessageLang text="mailHelp" />
</p>
<span style={{margin: "5px"}} /> <label className="label">
<MessageLang text="password" />
</label>
<input
type="password"
name="password"
className="input"
placeholder="password"
style={{ width: "100%" }}
required
disabled={loading}
/>
<p className="help">
<MessageLang text="passwordHelp" />
</p>
<label className="label checkbox"> <span style={{ margin: "5px" }} />
<input type="checkbox" name="ruleCheck" required />
<Link href="/terms"></Link><Link href="/privacypolicy"></Link> <label className="label checkbox" style={{ justifyContent: "center" }}>
<input type="checkbox" name="ruleCheck" required disabled={loading} />
<MessageLang text="agree" />
{": "}
<Link href="/terms">
<MessageLang text="terms" />
</Link>
<MessageLang text="and" />
<Link href="/privacypolicy">
<MessageLang text="privacypolicy" />
</Link>
</label> </label>
<button type="submit" className="button"></button> <button
type="submit"
className="button is-primary"
style={{ width: "100%" }}
id="signup"
disabled={loading}
>
<MessageLang text="Signup" />
</button>
</form> </form>
) </div>
);
} }

View File

@ -1,12 +1,12 @@
.input{ .input {
width: 20rem; width: 20rem;
} }
.button{ .button {
margin-top: 5px; margin-top: 5px;
} }
.input-out{ .input-out {
position: absolute; position: absolute;
left: 10px; left: 10px;
top: 50%; top: 50%;
@ -18,6 +18,6 @@
font-weight: normal; font-weight: normal;
} }
.inputout-email{ .inputout-email {
z-index: 1; z-index: 1;
} }

5
src/app/sitemap.ts Normal file
View File

@ -0,0 +1,5 @@
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [];
}

View File

@ -1,7 +1,16 @@
.about{ .about {
margin: 10px; margin: 0 auto;
margin-top: 10px;
max-width: 40rem;
} }
.signup,.signin{ .signup,
.signin {
margin: 10px; margin: 10px;
display: inline-block;
}
.peasAboutTitle {
position: absolute;
margin-left: 16px;
} }

View File

@ -1,27 +1,33 @@
'use client'; "use client";
import { useEffect, useState } from "react";
import Cookies from "js-cookie";
import { messages } from "../messages";
import { MessageLangProps, SupportedLanguage, isValidLanguage } from "../types";
import { useEffect, useState } from 'react'; interface MessageLangPropsWithStyle
import Cookies from 'js-cookie'; extends MessageLangProps,
import { messages } from '../messages'; React.HTMLAttributes<HTMLSpanElement> {}
import { MessageLangProps, SupportedLanguage, isValidLanguage } from '../types';
export function MessageLang({ text, data = [] }: MessageLangProps) { export function MessageLang({
const [lang, setLang] = useState<SupportedLanguage>('ja'); text,
data = [],
...htmlProps
}: MessageLangPropsWithStyle) {
const [lang, setLang] = useState<SupportedLanguage>("ja");
useEffect(() => { useEffect(() => {
const browserLang = navigator.language.split('-')[0]; const browserLang = navigator.language.split("-")[0];
const savedLang = Cookies.get('lang') || browserLang || 'ja'; const savedLang = Cookies.get("lang") || browserLang || "ja";
if (isValidLanguage(savedLang)) { if (isValidLanguage(savedLang)) {
setLang(savedLang); setLang(savedLang);
Cookies.set('lang', savedLang); Cookies.set("lang", savedLang);
} }
}, []); }, []);
let message = messages[lang][text]; let message = messages[lang][text];
data.forEach((value, index) => { data.forEach((value, index) => {
message = message.replace(new RegExp('\\{' + index + '\\}', 'g'), value); message = message.replace(new RegExp("\\{" + index + "\\}", "g"), value);
}); });
return <span dangerouslySetInnerHTML={{__html: message}} />; return <span dangerouslySetInnerHTML={{ __html: message }} {...htmlProps} />;
} }

View File

@ -1,7 +1,12 @@
import { messages } from '../messages'; import { messages } from '../messages';
import { MessageLangProps, SupportedLanguage } from '../types'; import { MessageLangProps, SupportedLanguage } from '../types';
export function MessageLangVar({ text, data = [], variables = {} }: MessageLangProps & { variables?: Record<string, string>; }, langFunc?: string) { export function MessageLangVar({
text, data = [],
variables = {}
}: MessageLangProps & {
variables?: Record<string, string>;
}, langFunc?: string) {
const lang: SupportedLanguage = (langFunc as SupportedLanguage) || 'ja'; const lang: SupportedLanguage = (langFunc as SupportedLanguage) || 'ja';
let message = messages[lang][text]; let message = messages[lang][text];

View File

@ -7,7 +7,10 @@ export const messages = {
使<br /> 使<br />
QRコードの印刷にも対応していて共有にも最適です QRコードの印刷にも対応していて共有にも最適です
`, `,
ServerAboutTitle: "このサーバーについて",
or: "または", or: "または",
and: "と",
agree: "同意",
Start: "今すぐ始める", Start: "今すぐ始める",
Signin: "サインイン", Signin: "サインイン",
Signup: "サインアップ", Signup: "サインアップ",
@ -16,7 +19,30 @@ export const messages = {
username: "ユーザー名", username: "ユーザー名",
idHelp: "@から始まる英数字のID(3文字以上)", idHelp: "@から始まる英数字のID(3文字以上)",
mailHelp: "メールアドレスは受信できるものを使用してください", mailHelp: "メールアドレスは受信できるものを使用してください",
passwordHelp: "8文字以上のパスワードを使用してください" passwordHelp: "8文字以上のパスワードを使用してください",
signinError: "サインインに失敗しました",
mailVerify: "メール認証",
code: "コード",
submit: "送信",
codeCharacterCount: "コードは6桁で入力してください",
emailVerificationSuccess: `
`,
connectionError: "通信エラー",
accountCreateSuccess: `
`,
sameUsername: "同じユーザー名が既に使用されています",
termsAgreement: "利用規約とプライバシーポリシーに同意してください",
bodyRequired: "フォーム内容が送信されていません",
passwordCharacterLimit: "パスワードは8文字以上にしてください",
usernameRestriction: "ユーザー名は3文字以上の英数字のIDにしてください",
userCreationFailed: "ユーザーの作成に失敗しました",
terms: "利用規約",
privacypolicy: "プライバシーポリシー",
}, },
en: { en: {
PeasAboutTitle: "About Peas", PeasAboutTitle: "About Peas",
@ -26,15 +52,44 @@ export const messages = {
You can freely create profile sections suitable for use at school or work, and also change the privacy settings.<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. It also supports printing QR codes, making sharing easy.
`, `,
ServerAboutTitle: "About this server",
or: "or", or: "or",
Start: "Get Started Now", and: "and",
agree: "Agree",
Start: "Get started now",
Signin: "Sign In", Signin: "Sign In",
Signup: "Sign Up", Signup: "Sign Up",
email: "Email", email: "Email",
password: "Password", password: "Password",
username: "Username", username: "Username",
idHelp: "ID starting with @ and consisting of English numbers (at least 3 characters)", idHelp:
"ID starting with @ and consisting of English numbers (at least 3 characters)",
mailHelp: "Use an email address that can be received", mailHelp: "Use an email address that can be received",
passwordHelp: "Use a password of at least 8 characters" passwordHelp: "Use a password of at least 8 characters",
} signinError: "Sign in failed",
mailVerify: "Mail Verify",
code: "Code",
submit: "Submit",
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",
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",
},
}; };

View File

@ -1,11 +1,12 @@
import mysql from 'mysql2/promise'; import mysql from "mysql2/promise";
import PeasConfig from "@/../peas.config";
const pool = mysql.createPool({ const pool = mysql.createPool({
host: process.env.DB_HOST, host: PeasConfig.databaseHost,
port: Number(process.env.DB_PORT), port: PeasConfig.databasePort,
user: process.env.DB_USER, user: PeasConfig.databaseUser,
password: process.env.DB_PASSWORD, password: PeasConfig.databasePassword,
database: process.env.DB_DATABASE, database: PeasConfig.databaseDB,
waitForConnections: true, waitForConnections: true,
connectionLimit: 10, connectionLimit: 10,
}); });

View File

@ -1,41 +1,32 @@
import * as nodemailer from 'nodemailer'; import * as nodemailer from "nodemailer";
import * as dotenv from 'dotenv'; import type SMTPTransport from "nodemailer/lib/smtp-transport";
import PeasConfig from "@/../peas.config";
dotenv.config();
export interface EmailMessage { export interface EmailMessage {
to: string | string[], to: string | string[];
subject: string, subject: string;
text?: string, text?: string;
html?: string, html?: string;
} }
export async function createTransporter() { export async function createTransporter() {
// 環境変数の値をログ出力して確認
console.log('SMTP設定:', {
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: process.env.SMTP_SECURE,
user: process.env.SMTP_USER
});
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, host: PeasConfig.smtpHost,
port: Number(process.env.SMTP_PORT), port: PeasConfig.smtpPort,
secure: process.env.SMTP_SECURE === 'true', secure: PeasConfig.smtpSecure,
auth: { auth: {
user: process.env.SMTP_USER, user: PeasConfig.smtpUser,
pass: process.env.SMTP_PASSWORD, pass: PeasConfig.smtpPassword,
}, },
debug: process.env.DEBUG === 'true', debug: process.env.DEBUG === "true",
}); } as SMTPTransport.Options);
// 接続テスト // 接続テスト
try { try {
await transporter.verify(); await transporter.verify();
console.log('SMTPサーバーに接続できました'); console.log("SMTPサーバーに接続できました");
} catch (error) { } catch (error) {
console.error('SMTP接続テストに失敗:', error); console.error("SMTP接続テストに失敗:", error);
throw error; throw error;
} }
@ -43,18 +34,18 @@ export async function createTransporter() {
} }
export async function sendMail(message: EmailMessage): Promise<void> { export async function sendMail(message: EmailMessage): Promise<void> {
try{ try {
const transporter = await createTransporter(); const transporter = await createTransporter();
await transporter.sendMail({ await transporter.sendMail({
from: process.env.SMTP_USER, from: PeasConfig.smtpUser,
to: Array.isArray(message.to) ? message.to.join(',') : message.to, to: Array.isArray(message.to) ? message.to.join(",") : message.to,
subject: message.subject, subject: message.subject,
text: message.text, text: message.text,
html: message.html html: message.html,
}); });
console.log('メール送信成功'); console.log("メール送信成功");
}catch (error){ } catch (error) {
console.error('メール送信に失敗しました:', error); console.error("メール送信に失敗しました:", error);
throw error; throw error;
} }
} }

24
src/lib/peas.config.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
interface SSLConfig {
ssl: boolean;
}
export interface PeasConfigType {
// Server Config
serverName: string;
serverDescription: string;
serverHost: string;
// SSL Config
ssl: SSLConfig;
// SMTP Config
smtpHost: string;
smtpPort: number;
smtpSecure: boolean;
smtpUser: string;
smtpPassword: string;
// Database Config
databaseHost: string;
databasePort: number;
databaseUser: string;
databasePassword: string;
databaseDB: string;
}