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

View File

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

View File

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

View File

@ -1,27 +1,33 @@
import pool from '@/lib/database';
import type { RowDataPacket } from 'mysql2';
import pool from "@/lib/database";
import type { RowDataPacket } from "mysql2";
import { NextResponse, NextRequest } from 'next/server';
import { NextResponse, NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
const { code } = body;
const body = await request.json();
const { code } = body;
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM mailverifiedcode WHERE code = ?',
[code]
const [rows] = await pool.query<RowDataPacket[]>(
"SELECT * FROM mailverifiedcode WHERE 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({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]
);
return NextResponse.json({status: "success", message: 'Code verified' }, { status: 200 });
}
return NextResponse.json(
{ status: "success", message: "Code verified" },
{ status: 200 },
);
}
}
}

View File

@ -1,71 +1,78 @@
import pool from '@/lib/database';
import type { RowDataPacket } from 'mysql2';
import pool from "@/lib/database";
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) {
for (let i = 0; i < maxAttempts; i++) {
const code = Math.floor(100000 + Math.random() * 900000).toString();
const [existingCodes] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM mailverifiedcode WHERE code = ?",[code]
);
if (existingCodes.length === 0) {
return code;
}
for (let i = 0; i < maxAttempts; i++) {
const code = Math.floor(100000 + Math.random() * 900000).toString();
const [existingCodes] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM mailverifiedcode WHERE code = ?",
[code],
);
if (existingCodes.length === 0) {
return code;
}
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) {
// body取得
const body = await request.json();
const { email, user } = body;
// body取得
const body = await request.json();
const { email, user } = body;
const code = await createCode();
const code = await createCode();
// コードが存在する場合
const [existingCodes] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM mailverifiedcode WHERE user = ?",[user]
// コードが存在する場合
const [existingCodes] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM mailverifiedcode WHERE user = ?",
[user],
);
let row: RowDataPacket[] = [];
if (existingCodes.length !== 0) {
[row] = await pool.execute<RowDataPacket[]>(
"UPDATE `mailverifiedcode` SET `code` = ?, `time` = current_timestamp(3) WHERE `mailverifiedcode`.`user` = ?",
[code, user],
);
} else {
// コードが存在しない場合
[row] = await pool.execute<RowDataPacket[]>(
"INSERT INTO `mailverifiedcode` (`code`, `user`, `email`, `time`, `num`) VALUES (?, ?, ?, current_timestamp(3), NULL);",
[code, user, email],
);
}
let row: RowDataPacket[] = [];
// メール送信
if ((row as RowDataPacket).affectedRows === 1) {
await sendMail({
to: email,
subject: `${PeasConfig.serverName}】メール認証`,
text: `
${user}\n
${PeasConfig.serverName}\n
${PeasConfig.serverName}\n
${request.nextUrl.origin}/mailverify?code=${code} `,
});
if(existingCodes.length !== 0){
[row] = await pool.execute<RowDataPacket[]>(
"UPDATE `mailverifiedcode` SET `code` = ?, `time` = current_timestamp(3) WHERE `mailverifiedcode`.`user` = ?",
[code, user]
)
}else{
// コードが存在しない場合
[row] = await pool.execute<RowDataPacket[]>(
"INSERT INTO `mailverifiedcode` (`code`, `user`, `email`, `time`, `num`) VALUES (?, ?, ?, current_timestamp(3), NULL);",
[code, user, email]
)
}
// メール送信
if ((row as RowDataPacket).affectedRows === 1) {
await sendMail({
to: email,
subject: "【Peas】メール認証",
text: `${user}さんこんにちは。\n
Peasではメール認証が必要となっています\n
Peasの機能を利用することができます\n
${request.nextUrl.origin}/mailverify?code=${code} `
})
return NextResponse.json({
status: "success",
code: code,
})
}else{
// コード保存失敗
return NextResponse.json({
status: "error",
error: "Failed to Save Code"
}, { status: 400 });
}
return NextResponse.json({
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 type { RowDataPacket } from 'mysql2';
import pool from "@/lib/database";
import type { RowDataPacket } from "mysql2";
import bcrypt from 'bcrypt';
import bcrypt from "bcrypt";
import { NextResponse, NextRequest } from 'next/server';
import { cookies } from 'next/headers'
import { NextResponse, NextRequest } from "next/server";
import { cookies } from "next/headers";
export async function POST(request: NextRequest) {
// body取得
const body = await request.json();
const { email, password } = body;
// body取得
const body = await request.json();
const { email, password } = body;
// ユーザー取得
const [existingUsers] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM users WHERE email = ?", [email]
// ユーザー取得
const [existingUsers] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM users WHERE email = ?",
[email],
);
// ユーザーが存在しない場合
if (existingUsers.length === 0) {
return NextResponse.json(
{
status: "error",
error: "User not found",
},
{ status: 404 },
);
}
// ユーザーが存在しない場合
if (existingUsers.length === 0) {
return NextResponse.json({
status: "error",
error: "User not found"
}, { status: 404 });
}
const user = existingUsers[0];
const passwordMatch = await bcrypt.compare(password, user.password);
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);
// パスワード確認
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({
status: "success",
message: "Login successful"
}, { status: 200 });
}
return NextResponse.json(
{
status: "success",
message: "Login successful",
},
{ status: 200 },
);
}
}

View File

@ -1,87 +1,114 @@
import pool from '@/lib/database';
import type { RowDataPacket } from 'mysql2';
import pool from "@/lib/database";
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) {
const body = await request.json();
const { email, password, id, rule } = body;
const body = await request.json();
const { email, password, id, rule } = body;
try{
if(!rule || rule === false){
return NextResponse.json({
status: "error",
error: "Please read and accept the terms and conditions"
}, { status: 400 });
}
}catch(e){
console.log(e);
try {
if (!rule || rule === false) {
return NextResponse.json(
{
status: "error",
error: "Please read and accept the terms and conditions",
},
{ status: 400 },
);
}
} catch (e) {
console.log(e);
}
// bodyが空になっていないか
if(!email || !password || !id || !rule){
return NextResponse.json({
status: "error",
error: "Body Required"
}, { status: 400 });
}
// bodyが空になっていないか
if (!email || !password || !id || !rule) {
return NextResponse.json(
{
status: "error",
error: "Body Required",
},
{ status: 400 },
);
}
// パスワードが八文字以上か
if(password.length < 8){
return NextResponse.json({
status: "error",
error: "Password must be at least 8 characters long"
}, { status: 400 });
}
// パスワードが8文字以上か
if (password.length < 8) {
return NextResponse.json(
{
status: "error",
error: "Password must be at least 8 characters long",
},
{ status: 400 },
);
}
// ユーザー名が英数字3文字以上か
if(!/^[a-zA-Z0-9]{3,}$/.test(id)){
return NextResponse.json({
status: "error",
error: "Username must be at least 3 alphanumeric characters"
}, { status: 400 });
}
// ユーザー名が英数字3文字以上か
if (!/^[a-zA-Z0-9]{3,}$/.test(id)) {
return NextResponse.json(
{
status: "error",
error: "Username must be at least 3 alphanumeric characters",
},
{ status: 400 },
);
}
const [existingUsers] = await pool.execute<RowDataPacket[]>(
"SELECT * FROM users WHERE id = ?",[id]
const [existingUsers] = await pool.execute<RowDataPacket[]>(
"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){
const passwordHash = await bcrypt.hash(password, 10);
if ((result as RowDataPacket).affectedRows === 1) {
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[]>(
"INSERT INTO `users` (`id`, `password`, `email`, `mailverified`, `time`, `num`) VALUES (?, ?, ?, '0', current_timestamp(3), NULL)",
[id, passwordHash, email]
);
if ((result as RowDataPacket).affectedRows === 1) {
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,
})
});
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 });
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 },
);
}
}

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

View File

@ -1,53 +1,90 @@
"use client";
import { useState } from "react";
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() {
const searchParams = useSearchParams();
const URLCode = searchParams.get("code");
const [loading, setLoading] = useState(false);
let code: string = "";
const searchParams = useSearchParams();
const URLCode = searchParams.get("code");
if(URLCode){
code = URLCode;
let code: string = "";
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 === ""){
code = e.currentTarget.code.value;
}
if (code.length !== 6) {
alert(MessageLangVar({text: "codeCharacterCount"}));
return;
}
if (code.length !== 6) {
alert("コードは6桁で入力してください。");
return;
}
try {
const res = await fetch(`${location.origin}/api/mailverified/check`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
}),
});
const res = await fetch(`${location.origin}/api/mailverify/check`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
}),
});
const data = await res.text();
const result = JSON.parse(data);
const data = await res.text();
const result = JSON.parse(data);
if (result.status === "success") {
alert("メール認証に成功しました。\nログインしてください。");
window.location.href = "/login";
}
};
if (result.status === "success") {
alert(MessageLangVar({ text: "emailVerificationSuccess" }));
window.location.href = "/signin";
}
} catch (err) {
console.error(err)
} finally {
setLoading(false);
}
};
return (
<>
<h1 className="title"></h1>
return (
<>
<h1 className="title has-text-centered"><MessageLang text="mailVerify" /></h1>
<form onSubmit={verify}>
<label className="label"></label>
<input type="text" name="code" defaultValue={code} className="input" placeholder="123456" style={{width: "10rem"}} maxLength={6} />
</form>
</>
)
{loading && (
<div style={{ marginBottom: "1rem" }}>
<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>
</>
);
}

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";
import "./style.css"
import "./style.css";
import Link from "next/link";
import { Icon } from "@iconify/react";
import PeasConfig from "@/../peas.config";
import { MessageLang } from "@/lang/component/client";
export default function Home() {
return (
<>
<h1 className="has-text-centered title">{PeasConfig.serverName}</h1>
<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">
<MessageLang text="PeasAboutText" />
</p>
</div>
<div style={{width: "15rem"}}>
<Link href="/signup" 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 className="has-text-centered">
<Link
href="/signup"
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>
</>
);

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";
import "./style.css";
import { useState } from "react";
import { MessageLang } from "@/lang/component/client";
import { Icon } from "@iconify/react";
export default function SignIn() {
const FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
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);
const FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
if (json.status === "success") {
window.location.href = "/";
}
try {
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 (
<form onSubmit={FormSubmit}>
<h1 className="title"><MessageLang text="Signin" /></h1>
useState(() => {
const locationURL = new URL(window.location.href);
if (locationURL.searchParams.get("error")) {
setError(true);
}
});
<label className="label"><MessageLang text="email" /></label>
<input type="email" name="email" className="input" required />
return (
<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>
<input type="password" name="password" className="input" required />
{loading && (
<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>
</form>
)
<br />
<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;
}
.button{
.button {
margin-top: 5px;
}

View File

@ -1,77 +1,174 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { Icon } from "@iconify/react";
import "./style.css";
import { MessageLang } from "@/lang/component/client";
import { MessageLangVar } from "@/lang/component/variable";
export default function SignUp() {
const FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const [loading, setLoading] = useState(false);
const res = await fetch("/api/signup", {
method: "POST",
headers: {
"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);
const FormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
if (json.status === "success") {
alert("アカウントの作成に成功しました。\nご登録いただいたメールアドレスに確認用メールを送信しました。\n確認メールをクリックしてアカウントを有効化してください。")
window.location.href = "/";
}else{
if(json.error === "User already exists"){
alert("同じユーザー名が既に使用されています")
}else if(json.error === "Please read and accept the terms and conditions"){
alert("利用規約とプライバシーポリシーに同意してください")
}else if(json.error === "Body Required"){
alert("Bodyは必須です")
}else if(json.error === "Password must be at least 8 characters long"){
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"){
alert("ユーザーの作成に失敗しました")
}
try {
const res = await fetch("/api/signup", {
method: "POST",
headers: {
"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") {
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 (
<form onSubmit={FormSubmit}>
<h1 className="title"><MessageLang text="Signup" /></h1>
return (
<div
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>
<div className="field" style={{ position: 'relative' }}>
<span className="input-out">@</span>
<input type="text" name="username" placeholder="username" className="input" style={{ paddingLeft: '25px' }} required />
</div>
<p className="help"><MessageLang text="idHelp" /></p>
{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 className="input" type="email" name="email" placeholder="info@example.com" required />
<p className="help"><MessageLang text="mailHelp" /></p>
<label className="label">
<MessageLang text="username" />
</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>
<input type="password" name="password" className="input" placeholder="password" required />
<p className="help"><MessageLang text="passwordHelp" /></p>
<label className="label">
<MessageLang text="email" />
</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">
<input type="checkbox" name="ruleCheck" required />
<Link href="/terms"></Link><Link href="/privacypolicy"></Link>
</label>
<span style={{ margin: "5px" }} />
<button type="submit" className="button"></button>
</form>
)
<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>
<button
type="submit"
className="button is-primary"
style={{ width: "100%" }}
id="signup"
disabled={loading}
>
<MessageLang text="Signup" />
</button>
</form>
</div>
);
}

View File

@ -1,12 +1,12 @@
.input{
.input {
width: 20rem;
}
.button{
.button {
margin-top: 5px;
}
.input-out{
.input-out {
position: absolute;
left: 10px;
top: 50%;
@ -18,6 +18,6 @@
font-weight: normal;
}
.inputout-email{
.inputout-email {
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{
margin: 10px;
.about {
margin: 0 auto;
margin-top: 10px;
max-width: 40rem;
}
.signup,.signin{
.signup,
.signin {
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';
import Cookies from 'js-cookie';
import { messages } from '../messages';
import { MessageLangProps, SupportedLanguage, isValidLanguage } from '../types';
interface MessageLangPropsWithStyle
extends MessageLangProps,
React.HTMLAttributes<HTMLSpanElement> {}
export function MessageLang({ text, data = [] }: MessageLangProps) {
const [lang, setLang] = useState<SupportedLanguage>('ja');
export function MessageLang({
text,
data = [],
...htmlProps
}: MessageLangPropsWithStyle) {
const [lang, setLang] = useState<SupportedLanguage>("ja");
useEffect(() => {
const browserLang = navigator.language.split('-')[0];
const savedLang = Cookies.get('lang') || browserLang || 'ja';
if (isValidLanguage(savedLang)) {
setLang(savedLang);
Cookies.set('lang', savedLang);
}
}, []);
useEffect(() => {
const browserLang = navigator.language.split("-")[0];
const savedLang = Cookies.get("lang") || browserLang || "ja";
if (isValidLanguage(savedLang)) {
setLang(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) => {
message = message.replace(new RegExp('\\{' + index + '\\}', 'g'), value);
});
return <span dangerouslySetInnerHTML={{__html: message}} />;
return <span dangerouslySetInnerHTML={{ __html: message }} {...htmlProps} />;
}

View File

@ -3,15 +3,15 @@ import { messages } from '../messages';
import { MessageLangProps, isValidLanguage } from '../types';
export async function MessageLang({ text, data = [] }: MessageLangProps) {
const cookieStore = await cookies();
const savedLang = cookieStore.get('lang')?.value || 'ja';
const lang = isValidLanguage(savedLang) ? savedLang : 'ja';
const cookieStore = await cookies();
const savedLang = cookieStore.get('lang')?.value || 'ja';
const lang = isValidLanguage(savedLang) ? savedLang : 'ja';
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) => {
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 { MessageLangProps, SupportedLanguage } from '../types';
export function MessageLangVar({ text, data = [], variables = {} }: MessageLangProps & { variables?: Record<string, string>; }, langFunc?: string) {
const lang: SupportedLanguage = (langFunc as SupportedLanguage) || 'ja';
export function MessageLangVar({
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) => {
message = message.replace(new RegExp('\\{' + index + '\\}', 'g'), value);
data.forEach((value, index) => {
message = message.replace(new RegExp('\\{' + index + '\\}', 'g'), value);
});
if (variables) {
Object.keys(variables).forEach(key => {
message = message.replace(new RegExp('\\{' + key + '\\}', 'g'), variables[key]);
});
}
if (variables) {
Object.keys(variables).forEach(key => {
message = message.replace(new RegExp('\\{' + key + '\\}', 'g'), variables[key]);
});
}
return message;
return message;
}

View File

@ -1,40 +1,95 @@
export const messages = {
ja: {
PeasAboutTitle: "Peasについて",
PeasAboutText: `
Peasはプロフィール共有を目的としたSNSです<br />
<br />
使<br />
QRコードの印刷にも対応していて共有にも最適です
`,
or: "または",
Start: "今すぐ始める",
Signin: "サインイン",
Signup: "サインアップ",
email: "メールアドレス",
password: "パスワード",
username: "ユーザー名",
idHelp: "@から始まる英数字のID(3文字以上)",
mailHelp: "メールアドレスは受信できるものを使用してください",
passwordHelp: "8文字以上のパスワードを使用してください"
},
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.
`,
or: "or",
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"
}
ja: {
PeasAboutTitle: "Peasについて",
PeasAboutText: `
Peasはプロフィール共有を目的としたSNSです<br />
<br />
使<br />
QRコードの印刷にも対応していて共有にも最適です
`,
ServerAboutTitle: "このサーバーについて",
or: "または",
and: "と",
agree: "同意",
Start: "今すぐ始める",
Signin: "サインイン",
Signup: "サインアップ",
email: "メールアドレス",
password: "パスワード",
username: "ユーザー名",
idHelp: "@から始まる英数字のID(3文字以上)",
mailHelp: "メールアドレスは受信できるものを使用してください",
passwordHelp: "8文字以上のパスワードを使用してください",
signinError: "サインインに失敗しました",
mailVerify: "メール認証",
code: "コード",
submit: "送信",
codeCharacterCount: "コードは6桁で入力してください",
emailVerificationSuccess: `
`,
connectionError: "通信エラー",
accountCreateSuccess: `
`,
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({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
waitForConnections: true,
connectionLimit: 10,
host: PeasConfig.databaseHost,
port: PeasConfig.databasePort,
user: PeasConfig.databaseUser,
password: PeasConfig.databasePassword,
database: PeasConfig.databaseDB,
waitForConnections: true,
connectionLimit: 10,
});
export default pool;

View File

@ -1,60 +1,51 @@
import * as nodemailer from 'nodemailer';
import * as dotenv from 'dotenv';
dotenv.config();
import * as nodemailer from "nodemailer";
import type SMTPTransport from "nodemailer/lib/smtp-transport";
import PeasConfig from "@/../peas.config";
export interface EmailMessage {
to: string | string[],
subject: string,
text?: string,
html?: string,
to: string | string[];
subject: string;
text?: string;
html?: string;
}
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({
host: PeasConfig.smtpHost,
port: PeasConfig.smtpPort,
secure: PeasConfig.smtpSecure,
auth: {
user: PeasConfig.smtpUser,
pass: PeasConfig.smtpPassword,
},
debug: process.env.DEBUG === "true",
} as SMTPTransport.Options);
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
debug: process.env.DEBUG === 'true',
});
// 接続テスト
try {
await transporter.verify();
console.log("SMTPサーバーに接続できました");
} catch (error) {
console.error("SMTP接続テストに失敗:", error);
throw error;
}
// 接続テスト
try {
await transporter.verify();
console.log('SMTPサーバーに接続できました');
} catch (error) {
console.error('SMTP接続テストに失敗:', error);
throw error;
}
return transporter;
return transporter;
}
export async function sendMail(message: EmailMessage): Promise<void> {
try{
const transporter = await createTransporter();
await transporter.sendMail({
from: process.env.SMTP_USER,
to: Array.isArray(message.to) ? message.to.join(',') : message.to,
subject: message.subject,
text: message.text,
html: message.html
});
console.log('メール送信成功');
}catch (error){
console.error('メール送信に失敗しました:', error);
throw error;
}
try {
const transporter = await createTransporter();
await transporter.sendMail({
from: PeasConfig.smtpUser,
to: Array.isArray(message.to) ? message.to.join(",") : message.to,
subject: message.subject,
text: message.text,
html: message.html,
});
console.log("メール送信成功");
} catch (error) {
console.error("メール送信に失敗しました:", 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;
}