mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
feat: 更新了部分依赖包的开发状态标记
This commit is contained in:
48
app/components/auth/Login.tsx
Normal file
48
app/components/auth/Login.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from '@remix-run/react';
|
||||
|
||||
export function Login() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as { token: string };
|
||||
localStorage.setItem('token', data.token);
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
// 处理错误
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="邮箱"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="密码"
|
||||
required
|
||||
/>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
54
app/components/auth/Register.tsx
Normal file
54
app/components/auth/Register.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from '@remix-run/react';
|
||||
|
||||
export function Register() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email, password }),
|
||||
});
|
||||
if (response.ok) {
|
||||
navigate('/login');
|
||||
} else {
|
||||
// 处理错误
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="用户名"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="邮箱"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="密码"
|
||||
required
|
||||
/>
|
||||
<button type="submit">注册</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
25
app/hooks/useAuth.ts
Normal file
25
app/hooks/useAuth.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from '@remix-run/react';
|
||||
|
||||
export function useAuth() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
setIsAuthenticated(!!token);
|
||||
}, []);
|
||||
|
||||
const login = (token: string) => {
|
||||
localStorage.setItem('token', token);
|
||||
setIsAuthenticated(true);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
setIsAuthenticated(false);
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return { isAuthenticated, login, logout };
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
|
||||
15
app/routes/api.auth.login.ts
Normal file
15
app/routes/api.auth.login.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
import { type ActionFunction } from '@remix-run/node';
|
||||
import { verifyLogin, createToken } from '~/utils/auth.server';
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const { email, password } = (await request.json()) as { email: string; password: string };
|
||||
|
||||
const user = await verifyLogin(email, password);
|
||||
if (!user) {
|
||||
return json({ success: false, error: 'Invalid credentials' }, { status: 400 });
|
||||
}
|
||||
|
||||
const token = createToken(user._id.toString());
|
||||
return json({ success: true, token });
|
||||
};
|
||||
20
app/routes/api.auth.register.ts
Normal file
20
app/routes/api.auth.register.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { type ActionFunction, json } from '@remix-run/cloudflare';
|
||||
import { db } from '~/utils/db.server';
|
||||
import { hashPassword } from '~/utils/auth.server';
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const { username, email, password } = (await request.json()) as { username: string; email: string; password: string };
|
||||
|
||||
try {
|
||||
const hashedPassword = await hashPassword(password);
|
||||
const [userId] = await db('users').insert({
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword
|
||||
});
|
||||
return json({ success: true, userId });
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
return json({ success: false, error: 'Registration failed' }, { status: 400 });
|
||||
}
|
||||
};
|
||||
5
app/routes/login.tsx
Normal file
5
app/routes/login.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Login } from '~/components/auth/Login';
|
||||
|
||||
export default function LoginPage() {
|
||||
return <Login />;
|
||||
}
|
||||
5
app/routes/register.tsx
Normal file
5
app/routes/register.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Register } from '~/components/auth/Register';
|
||||
|
||||
export default function RegisterPage() {
|
||||
return <Register />;
|
||||
}
|
||||
17
app/utils/api.ts
Normal file
17
app/utils/api.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export async function authenticatedFetch(url: string, options: RequestInit = {}) {
|
||||
const token = localStorage.getItem('token');
|
||||
const headers = {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
};
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401) {
|
||||
// Token is invalid or expired
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
32
app/utils/auth.server.ts
Normal file
32
app/utils/auth.server.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { db } from './db.server';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||
|
||||
export async function hashPassword(password: string) {
|
||||
return bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
export async function verifyLogin(email: string, password: string) {
|
||||
const user = await db('users').where({ email }).first();
|
||||
if (!user) return null;
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
if (!isValid) return null;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export function createToken(userId: string) {
|
||||
return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '30d' });
|
||||
}
|
||||
|
||||
export function verifyToken(token: string) {
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
return decoded as { userId: string };
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
37
app/utils/db.server.ts
Normal file
37
app/utils/db.server.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import knex from 'knex';
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
let db: Knex;
|
||||
|
||||
declare global {
|
||||
var __db: Knex | undefined;
|
||||
}
|
||||
|
||||
// 这个检查可以防止在开发模式下多次创建连接
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
db = getDb();
|
||||
} else {
|
||||
if (!global.__db) {
|
||||
global.__db = getDb();
|
||||
}
|
||||
db = global.__db;
|
||||
}
|
||||
|
||||
function getDb() {
|
||||
return knex({
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
port: Number(process.env.DB_PORT) || 3306,
|
||||
},
|
||||
pool: {
|
||||
min: 2,
|
||||
max: 10
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { db };
|
||||
Reference in New Issue
Block a user