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=

9
.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
@ -44,4 +44,7 @@ next-env.d.ts
# editor # editor
/.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.
You can register and share your user profile. Peas is a SNS designed for sharing profiles.
You can freely create profile sections suitable for use at school or work, and also change the privacy settings. 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.
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) {
return NextResponse.json(
{ status: "error", message: "Code not found" },
{ status: 401 },
); );
} else {
if (rows[0].code === code) {
await pool.query<RowDataPacket[]>(
"UPDATE `users` SET `mailverified` = 1 WHERE `users`.`id` = ?",
[rows[0].user],
);
if (rows.length === 0) { return NextResponse.json(
return NextResponse.json({status: "error", message: 'Code not found' }, { status: 401 }); { status: "success", message: "Code verified" },
}else{ { status: 200 },
if(rows[0].code === code){ );
await pool.query<RowDataPacket[]>(
'UPDATE `users` SET `mailverified` = 1 WHERE `users`.`id` = ?',
[rows[0].user]
);
return NextResponse.json({status: "success", message: 'Code verified' }, { status: 200 });
}
} }
} }
}

View File

@ -1,71 +1,78 @@
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) { );
return code; if (existingCodes.length === 0) {
} return code;
} }
throw new Error("Failed to generate unique code after maximum attempts"); }
throw new Error("Failed to generate unique code after maximum attempts");
} }
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, user } = body; const { email, user } = body;
const code = await createCode(); const code = await createCode();
// コードが存在する場合 // コードが存在する場合
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[] = [];
if (existingCodes.length !== 0) {
[row] = await pool.execute<RowDataPacket[]>(
"UPDATE `mailverifiedcode` SET `code` = ?, `time` = current_timestamp(3) WHERE `mailverifiedcode`.`user` = ?",
[code, user],
); );
} else {
let row: RowDataPacket[] = []; // コードが存在しない場合
[row] = await pool.execute<RowDataPacket[]>(
"INSERT INTO `mailverifiedcode` (`code`, `user`, `email`, `time`, `num`) VALUES (?, ?, ?, current_timestamp(3), NULL);",
[code, user, email],
);
}
if(existingCodes.length !== 0){ // メール送信
[row] = await pool.execute<RowDataPacket[]>( if ((row as RowDataPacket).affectedRows === 1) {
"UPDATE `mailverifiedcode` SET `code` = ?, `time` = current_timestamp(3) WHERE `mailverifiedcode`.`user` = ?", await sendMail({
[code, user] to: email,
) subject: `${PeasConfig.serverName}】メール認証`,
}else{ text: `
// コードが存在しない場合 ${user}\n
[row] = await pool.execute<RowDataPacket[]>( ${PeasConfig.serverName}\n
"INSERT INTO `mailverifiedcode` (`code`, `user`, `email`, `time`, `num`) VALUES (?, ?, ?, current_timestamp(3), NULL);", ${PeasConfig.serverName}\n
[code, user, email] ${request.nextUrl.origin}/mailverify?code=${code} `,
) });
}
// メール送信 return NextResponse.json({
if ((row as RowDataPacket).affectedRows === 1) { status: "success",
await sendMail({ code: code,
to: email, });
subject: "【Peas】メール認証", } else {
text: `${user}さんこんにちは。\n // コード保存失敗
Peasではメール認証が必要となっています\n return NextResponse.json(
Peasの機能を利用することができます\n {
${request.nextUrl.origin}/mailverify?code=${code} ` status: "error",
}) error: "Failed to Save Code",
},
return NextResponse.json({ { status: 400 },
status: "success", );
code: code, }
}) }
}else{
// コード保存失敗
return NextResponse.json({
status: "error",
error: "Failed to Save Code"
}, { 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) {
return NextResponse.json(
{
status: "error",
error: "User not found",
},
{ status: 404 },
); );
}
// ユーザーが存在しない場合 const user = existingUsers[0];
if (existingUsers.length === 0) { const passwordMatch = await bcrypt.compare(password, user.password);
return NextResponse.json({
status: "error",
error: "User not found"
}, { status: 404 });
}
const user = existingUsers[0]; // パスワード確認
const passwordMatch = await bcrypt.compare(password, user.password); if (!passwordMatch) {
return NextResponse.json(
{
status: "error",
error: "Incorrect password",
},
{ status: 401 },
);
} else {
// 成功
const sessionCookie = await cookies();
sessionCookie.set("user", user.id);
sessionCookie.set("password", password);
// パスワード確認 return NextResponse.json(
if (!passwordMatch) { {
return NextResponse.json({ status: "success",
status: "error", message: "Login successful",
error: "Incorrect password" },
}, { status: 401 }); { status: 200 },
} else { );
// 成功 }
const sessionCookie = await cookies(); }
sessionCookie.set('user', user.id);
sessionCookie.set('password', password);
return NextResponse.json({
status: "success",
message: "Login successful"
}, { 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", {
error: "Please read and accept the terms and conditions" status: "error",
}, { status: 400 }); error: "Please read and accept the terms and conditions",
} },
}catch(e){ { status: 400 },
console.log(e); );
} }
} catch (e) {
console.log(e);
}
// bodyが空になっていないか // bodyが空になっていないか
if(!email || !password || !id || !rule){ if (!email || !password || !id || !rule) {
return NextResponse.json({ return NextResponse.json(
status: "error", {
error: "Body Required" status: "error",
}, { status: 400 }); error: "Body Required",
} },
{ status: 400 },
);
}
// パスワードが八文字以上か // パスワードが8文字以上か
if(password.length < 8){ if (password.length < 8) {
return NextResponse.json({ return NextResponse.json(
status: "error", {
error: "Password must be at least 8 characters long" status: "error",
}, { status: 400 }); error: "Password must be at least 8 characters long",
} },
{ 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", {
error: "Username must be at least 3 alphanumeric characters" status: "error",
}, { status: 400 }); error: "Username must be at least 3 alphanumeric characters",
} },
{ 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) {
const passwordHash = await bcrypt.hash(password, 10);
const [result] = await pool.execute<RowDataPacket[]>(
"INSERT INTO `users` (`id`, `password`, `email`, `mailverified`, `time`, `num`) VALUES (?, ?, ?, '0', current_timestamp(3), NULL)",
[id, passwordHash, email],
); );
if(existingUsers.length === 0){ if ((result as RowDataPacket).affectedRows === 1) {
const passwordHash = await bcrypt.hash(password, 10); await fetch(
request.nextUrl.protocol +
request.nextUrl.host +
"/api/mailverified/send",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email,
user: id,
}),
},
);
const [result] = await pool.execute<RowDataPacket[]>( return NextResponse.json(
"INSERT INTO `users` (`id`, `password`, `email`, `mailverified`, `time`, `num`) VALUES (?, ?, ?, '0', current_timestamp(3), NULL)", {
[id, passwordHash, email] status: "success",
); message: "User created successfully",
},
if ((result as RowDataPacket).affectedRows === 1) { { status: 201 },
await fetch(request.nextUrl.protocol + request.nextUrl.host + "/api/mailverified/send", { );
method: "POST", } else {
headers: { return NextResponse.json(
"Content-Type": "application/json" {
}, status: "error",
body: JSON.stringify({ error: "Failed to create user",
email: email, },
user: id, { status: 500 },
}) );
});
return NextResponse.json({
status: "success",
message: "User created successfully"
}, { status: 201 });
} else {
return NextResponse.json({
status: "error",
error: "Failed to create user"
}, { status: 500 });
}
}else{
return NextResponse.json({
status: "error",
error: "User already exists"
}, { status: 400 });
} }
} else {
return NextResponse.json(
{
status: "error",
error: "User already exists",
},
{ 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,53 +1,90 @@
"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 searchParams = useSearchParams(); const [loading, setLoading] = useState(false);
const URLCode = searchParams.get("code");
let code: string = ""; const searchParams = useSearchParams();
const URLCode = searchParams.get("code");
if(URLCode){ let code: string = "";
code = URLCode;
if (URLCode !== null) {
code = URLCode;
}
const verify = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
if (code === "") {
code = e.currentTarget.code.value;
} }
const verify = async (e: React.FormEvent<HTMLFormElement>) => { if (code.length !== 6) {
if(code === ""){ alert(MessageLangVar({text: "codeCharacterCount"}));
code = e.currentTarget.code.value; return;
} }
if (code.length !== 6) { try {
alert("コードは6桁で入力してください。"); const res = await fetch(`${location.origin}/api/mailverified/check`, {
return; method: "POST",
} headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
}),
});
const res = await fetch(`${location.origin}/api/mailverify/check`, { const data = await res.text();
method: "POST", const result = JSON.parse(data);
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
}),
});
const data = await res.text();
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} />
</form> </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>
</>
);
}

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,46 +1,93 @@
"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 FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const [error, setError] = useState(false);
e.preventDefault(); const [loading, setLoading] = useState(false);
const res = await fetch("/api/signin", { const FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
method: "POST", e.preventDefault();
headers: { setLoading(true);
"Content-Type": "application/json",
},
body: JSON.stringify({
email: e.currentTarget.email.value,
password: e.currentTarget.password.value,
}),
});
const data = await res.text();
const json = JSON.parse(data);
if (json.status === "success") { try {
window.location.href = "/"; const res = await fetch("/api/signin", {
} method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: e.currentTarget.email.value,
password: e.currentTarget.password.value,
}),
});
const data = await res.text();
const json = JSON.parse(data);
if (json.status === "success") {
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);
} }
};
return ( useState(() => {
<form onSubmit={FormSubmit}> const locationURL = new URL(window.location.href);
<h1 className="title"><MessageLang text="Signin" /></h1> if (locationURL.searchParams.get("error")) {
setError(true);
}
});
<label className="label"><MessageLang text="email" /></label> return (
<input type="email" name="email" className="input" required /> <form onSubmit={FormSubmit} className="has-text-centered">
{error && (
<div className="message is-danger">
<div className="message-body">
<Icon icon="iconoir:warning-circle-solid" />
<MessageLang text="signinError" />
</div>
</div>
)}
<br /> <h1 className="title">
<MessageLang text="Signin" />
</h1>
<label className="label"><MessageLang text="password" /></label> {loading && (
<input type="password" name="password" className="input" required /> <div style={{ marginBottom: "1rem" }}>
<Icon icon="svg-spinners:6-dots-scale" width={32} height={32} />
</div>
)}
<br /> <label className="label">
<MessageLang text="email" />
</label>
<input type="email" name="email" className="input" required />
<button type="submit" className="button"><MessageLang text="Signin" /></button> <br />
</form>
) <label className="label">
} <MessageLang text="password" />
</label>
<input type="password" name="password" className="input" required />
<br />
<button type="submit" className="button">
<MessageLang text="Signin" />
</button>
</form>
);
}

View File

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

View File

@ -1,77 +1,174 @@
"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 FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const [loading, setLoading] = useState(false);
e.preventDefault();
const res = await fetch("/api/signup", { const FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
method: "POST", e.preventDefault();
headers: { setLoading(true);
"Content-Type": "application/json",
},
body: JSON.stringify({
email: e.currentTarget.email.value,
password: e.currentTarget.password.value,
id: e.currentTarget.username.value,
rule: e.currentTarget.ruleCheck.checked,
}),
});
const data = await res.text();
const json = JSON.parse(data);
if (json.status === "success") { try {
alert("アカウントの作成に成功しました。\nご登録いただいたメールアドレスに確認用メールを送信しました。\n確認メールをクリックしてアカウントを有効化してください。") const res = await fetch("/api/signup", {
window.location.href = "/"; method: "POST",
}else{ headers: {
if(json.error === "User already exists"){ "Content-Type": "application/json",
alert("同じユーザー名が既に使用されています") },
}else if(json.error === "Please read and accept the terms and conditions"){ body: JSON.stringify({
alert("利用規約とプライバシーポリシーに同意してください") email: e.currentTarget.email.value,
}else if(json.error === "Body Required"){ password: e.currentTarget.password.value,
alert("Bodyは必須です") id: e.currentTarget.username.value,
}else if(json.error === "Password must be at least 8 characters long"){ rule: e.currentTarget.ruleCheck.checked,
alert("パスワードは8文字以上にしてください") }),
}else if(json.error === "Username must be at least 3 alphanumeric characters"){ });
alert("ユーザー名は3文字以上の英数字のIDにしてください")
}else if(json.error === "Failed to create user"){ const data = await res.text();
alert("ユーザーの作成に失敗しました") const json = JSON.parse(data);
}
if (json.status === "success") {
alert(MessageLangVar({ text: "accountCreateSuccess" }));
window.location.href = "/";
} else {
if (json.error === "User already exists") {
alert(MessageLangVar({ text: "sameUsername" }));
} else if (
json.error === "Please read and accept the terms and conditions"
) {
alert(MessageLangVar({ text: "termsAgreement" }));
} else if (json.error === "Body Required") {
alert(MessageLangVar({ text: "bodyRequired" }));
} else if (
json.error === "Password must be at least 8 characters long"
) {
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>
<button type="submit" className="button"></button> <label className="label checkbox" style={{ justifyContent: "center" }}>
</form> <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>
<button
type="submit"
className="button is-primary"
style={{ width: "100%" }}
id="signup"
disabled={loading}
>
<MessageLang text="Signup" />
</button>
</form>
</div>
);
}

View File

@ -1,16 +1,16 @@
.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%;
transform: translateY(-50%); transform: translateY(-50%);
pointer-events: none; pointer-events: none;
z-index: 1; z-index: 1;
background-color: transparent; background-color: transparent;
@ -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) => {
message = message.replace(new RegExp("\\{" + index + "\\}", "g"), value);
});
data.forEach((value, index) => { return <span dangerouslySetInnerHTML={{ __html: message }} {...htmlProps} />;
message = message.replace(new RegExp('\\{' + index + '\\}', 'g'), value); }
});
return <span dangerouslySetInnerHTML={{__html: message}} />;
}

View File

@ -3,15 +3,15 @@ import { messages } from '../messages';
import { MessageLangProps, isValidLanguage } from '../types'; import { MessageLangProps, isValidLanguage } from '../types';
export async function MessageLang({ text, data = [] }: MessageLangProps) { export async function MessageLang({ text, data = [] }: MessageLangProps) {
const cookieStore = await cookies(); const cookieStore = await cookies();
const savedLang = cookieStore.get('lang')?.value || 'ja'; const savedLang = cookieStore.get('lang')?.value || 'ja';
const lang = isValidLanguage(savedLang) ? savedLang : 'ja'; const lang = isValidLanguage(savedLang) ? savedLang : 'ja';
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}} />;
} }

View File

@ -1,20 +1,25 @@
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({
const lang: SupportedLanguage = (langFunc as SupportedLanguage) || 'ja'; text, data = [],
variables = {}
}: MessageLangProps & {
variables?: Record<string, string>;
}, langFunc?: string) {
const lang: SupportedLanguage = (langFunc as SupportedLanguage) || 'ja';
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);
});
if (variables) {
Object.keys(variables).forEach(key => {
message = message.replace(new RegExp('\\{' + key + '\\}', 'g'), variables[key]);
}); });
}
if (variables) { return message;
Object.keys(variables).forEach(key => {
message = message.replace(new RegExp('\\{' + key + '\\}', 'g'), variables[key]);
});
}
return message;
} }

View File

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

View File

@ -1,60 +1,51 @@
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() {
// 環境変数の値をログ出力して確認 const transporter = nodemailer.createTransport({
console.log('SMTP設定:', { host: PeasConfig.smtpHost,
host: process.env.SMTP_HOST, port: PeasConfig.smtpPort,
port: process.env.SMTP_PORT, secure: PeasConfig.smtpSecure,
secure: process.env.SMTP_SECURE, auth: {
user: process.env.SMTP_USER user: PeasConfig.smtpUser,
}); pass: PeasConfig.smtpPassword,
},
debug: process.env.DEBUG === "true",
} as SMTPTransport.Options);
const transporter = nodemailer.createTransport({ // 接続テスト
host: process.env.SMTP_HOST, try {
port: Number(process.env.SMTP_PORT), await transporter.verify();
secure: process.env.SMTP_SECURE === 'true', console.log("SMTPサーバーに接続できました");
auth: { } catch (error) {
user: process.env.SMTP_USER, console.error("SMTP接続テストに失敗:", error);
pass: process.env.SMTP_PASSWORD, throw error;
}, }
debug: process.env.DEBUG === 'true',
});
// 接続テスト return transporter;
try {
await transporter.verify();
console.log('SMTPサーバーに接続できました');
} catch (error) {
console.error('SMTP接続テストに失敗:', error);
throw error;
}
return transporter;
} }
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;
}