diff --git a/README.md b/README.md index 0039e5c..f17ca69 100644 --- a/README.md +++ b/README.md @@ -42,26 +42,17 @@ EveEve(Everyone Translate Everything)は、インターネットに公開さ cd web bun i ``` -3. googleログインの設定をする必要があります。 - https://console.cloud.google.com/apis - 設定方法は以下のページを参考にしてください - https://developers.google.com/identity/sign-in/web/sign-in?hl=ja - https://zenn.dev/yoiyoicho/articles/c44a80e4bb4515#google%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E5%85%AC%E5%BC%8F%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88%E3%82%92%E8%AA%AD%E3%82%80 - - 承認済みのリダイレクトURIには - http://localhost:5173/api/auth/callback/google - を設定してください - - クライアントIDとクライアントシークレットを取得してください 3. 環境変数ファイルを作成し、必要な値を設定します: ``` cp .env.example .env ``` - `.env` ファイルを開き、以下の変数を適切な値に設定してください: + 以下のコマンドを実行してください + ``` + openssl rand -base64 32 + ``` + このコマンドで生成された文字列を`.env`ファイルの`SESSION_SECRET`に設定してください: - SESSION_SECRET - - GOOGLE_CLIENT_ID - - GOOGLE_CLIENT_SECRET 4. dockerを起動します: ``` @@ -71,12 +62,19 @@ EveEve(Everyone Translate Everything)は、インターネットに公開さ ``` bunx prisma migrate dev ``` -6. 起動します: +6. seedを実行します: + ``` + bun run seed + ``` +7. 起動します: ``` bun run dev ``` -6. ブラウザで `http://localhost:5173` にアクセスして、eveeve を使用開始します。 +6. ブラウザで `http://localhost:5173` にアクセスして、eveeve を使用開始します: +7. ローカル開発環境では、認証プロセスが簡略化されています: + - `http://localhost:5173/auth/login` にアクセスして、dev@example.comとdevpasswordでログインしてください。 + 注意: この簡易認証は開発環境でのみ機能し、本番環境では無効になります。本番環境では通常のGoogle認証フローが使用されます。 ## 貢献方法 翻訳、プログラミング、デザイン、ドキュメンテーションなど、あらゆる形の貢献を歓迎します。現在特に以下の分野での貢献を求めています: diff --git a/web/app/routes/auth.login.tsx b/web/app/routes/auth.login.tsx index 27db384..87f47a7 100644 --- a/web/app/routes/auth.login.tsx +++ b/web/app/routes/auth.login.tsx @@ -1,9 +1,29 @@ +import { getFormProps, getInputProps, useForm } from "@conform-to/react"; +import { parseWithZod } from "@conform-to/zod"; +import { getZodConstraint } from "@conform-to/zod"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Form, useActionData } from "@remix-run/react"; +import { z } from "zod"; +import { Button } from "~/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Separator } from "~/components/ui/separator"; import { GoogleForm } from "../components/GoogleForm"; import { authenticator } from "../utils/auth.server"; +const loginSchema = z.object({ + email: z.string().email("有効なメールアドレスを入力してください"), + password: z.string().min(4, "パスワードは4文字以上である必要があります"), +}); + export const loader = async ({ request }: LoaderFunctionArgs) => { const safeUser = await authenticator.isAuthenticated(request, { successRedirect: "/", @@ -12,13 +32,44 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { }; export const action = async ({ request }: ActionFunctionArgs) => { - return authenticator.authenticate("google", request, { - successRedirect: "/", - failureRedirect: "/auth/login", - }); + const formData = await request.clone().formData(); + const intent = String(formData.get("intent")); + const submission = parseWithZod(formData, { schema: loginSchema }); + try { + switch (intent) { + case "SignIn": + if (submission.status !== "success") { + return submission.reply(); + } + return authenticator.authenticate("user-pass", request, { + successRedirect: "/", + }); + case "SignInWithGoogle": + return authenticator.authenticate("google", request, { + successRedirect: "/", + failureRedirect: "/auth/login", + }); + default: + return submission.reply({ formErrors: ["Invalid action"] }); + } + } catch (error) { + return submission.reply({ formErrors: [error as string] }); + } }; const LoginPage = () => { + const lastResult = useActionData(); + const [form, { email, password }] = useForm({ + id: "login-form", + lastResult, + constraint: getZodConstraint(loginSchema), + shouldValidate: "onBlur", + shouldRevalidate: "onInput", + onValidate({ formData }) { + return parseWithZod(formData, { schema: loginSchema }); + }, + }); + return (
@@ -26,8 +77,43 @@ const LoginPage = () => { Login - +
+ {form.errors && ( +

invaild

+ )} +
+
+ + + {email.errors && ( +

{email.errors}

+ )} +
+
+ + + {password.errors && ( +

{password.errors}

+ )} +
+ +
+
+ + +
+ Or continue with +
+ +
); diff --git a/web/app/types.ts b/web/app/types.ts index 7cecda0..c2885ff 100644 --- a/web/app/types.ts +++ b/web/app/types.ts @@ -1,3 +1,3 @@ import type { User } from "@prisma/client"; -export type SafeUser = Omit; +export type SafeUser = Omit; diff --git a/web/app/utils/auth.server.ts b/web/app/utils/auth.server.ts index e07a504..2068c1d 100644 --- a/web/app/utils/auth.server.ts +++ b/web/app/utils/auth.server.ts @@ -1,4 +1,6 @@ -import { Authenticator } from "remix-auth"; +import bcrypt from "bcryptjs"; +import { Authenticator, AuthorizationError } from "remix-auth"; +import { FormStrategy } from "remix-auth-form"; import { GoogleStrategy } from "remix-auth-google"; import type { SafeUser } from "../types"; import { prisma } from "./prisma"; @@ -12,6 +14,37 @@ if (!SESSION_SECRET) { const authenticator = new Authenticator(sessionStorage); +const formStrategy = new FormStrategy(async ({ form }) => { + const email = form.get("email"); + const password = form.get("password"); + + if (!(email && password)) { + throw new Error("Invalid Request"); + } + + const user = await prisma.user.findUnique({ + where: { email: String(email) }, + }); + console.log(user); + if (!user) { + throw new AuthorizationError("User not found"); + } + + if (!user.password) { + throw new AuthorizationError("User has no password set."); + } + const passwordsMatch = await bcrypt.compare(String(password), user.password); + + if (!passwordsMatch) { + throw new AuthorizationError("Invalid password"); + } + + const { password: _, geminiApiKey: __, openAIApiKey: ___, claudeApiKey: ____, ...safeUser } = user; + return safeUser; +}); + +authenticator.use(formStrategy, "user-pass"); + const googleStrategy = new GoogleStrategy( { clientID: process.env.GOOGLE_CLIENT_ID || "", @@ -23,7 +56,7 @@ const googleStrategy = new GoogleStrategy( where: { email: profile.emails[0].value }, }); if (user) { - const { geminiApiKey, ...safeUser } = user; + const { password, geminiApiKey, openAIApiKey, claudeApiKey, ...safeUser } = user; return safeUser as SafeUser; } try { diff --git a/web/package.json b/web/package.json index c0dafe2..8992b47 100644 --- a/web/package.json +++ b/web/package.json @@ -9,7 +9,11 @@ "dev": "remix vite:dev", "start": "remix-serve ./build/server/index.js", "typecheck": "tsc", - "check": "bunx @biomejs/biome check --write ." + "check": "bunx @biomejs/biome check --write .", + "seed": "NODE_ENV=development prisma db seed" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" }, "dependencies": { "@conform-to/react": "^1.1.5", diff --git a/web/prisma/migrations/20240721105034_/migration.sql b/web/prisma/migrations/20240721105034_/migration.sql new file mode 100644 index 0000000..58df144 --- /dev/null +++ b/web/prisma/migrations/20240721105034_/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "password" TEXT; diff --git a/web/prisma/migrations/20240721115647_/migration.sql b/web/prisma/migrations/20240721115647_/migration.sql new file mode 100644 index 0000000..8f029d3 --- /dev/null +++ b/web/prisma/migrations/20240721115647_/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `password` on the `users` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "users" DROP COLUMN "password"; diff --git a/web/prisma/migrations/20240721123812_/migration.sql b/web/prisma/migrations/20240721123812_/migration.sql new file mode 100644 index 0000000..58df144 --- /dev/null +++ b/web/prisma/migrations/20240721123812_/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "password" TEXT; diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma index 7dc1763..f0f9e54 100644 --- a/web/prisma/schema.prisma +++ b/web/prisma/schema.prisma @@ -10,6 +10,7 @@ datasource db { model User { id Int @id @default(autoincrement()) email String @unique + password String? name String image String plan String @default("free") diff --git a/web/prisma/seed.ts b/web/prisma/seed.ts new file mode 100644 index 0000000..821e0e0 --- /dev/null +++ b/web/prisma/seed.ts @@ -0,0 +1,49 @@ +import { PrismaClient } from '@prisma/client' +import bcrypt from 'bcryptjs' + +const prisma = new PrismaClient() + +async function seed() { + if (process.env.NODE_ENV !== 'development' && !process.env.ALLOW_SEEDING) { + console.log('Seeding is only allowed in development environment') + return + } + + const email = 'dev@example.com' + + // 既存のユーザーをチェック + const existingUser = await prisma.user.findUnique({ + where: { email }, + }) + + if (existingUser) { + console.log(`A user with email ${email} already exists`) + return + } + + const hashedPassword = await bcrypt.hash('devpassword', 10) + + const user = await prisma.user.create({ + data: { + email, + name: 'Dev User', + password: hashedPassword, + image: '', + provider: 'password', + plan: 'free', + totalPoints: 0, + isAI: false, + }, + }) + + console.log(`Created dev user with email: ${user.email}`) +} + +seed() + .catch((e) => { + console.error(e) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) \ No newline at end of file