feat: 更新了部分依赖包的开发状态标记

This commit is contained in:
zyh
2024-10-22 02:46:31 +00:00
parent 663bc4c3b0
commit cd5b6243a5
14 changed files with 366 additions and 46 deletions

View 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>
);
}

View 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
View 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 };
}

View File

@@ -77,7 +77,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
</>
);
}
export default function App() {
return <Outlet />;
}

View 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 });
};

View 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
View File

@@ -0,0 +1,5 @@
import { Login } from '~/components/auth/Login';
export default function LoginPage() {
return <Login />;
}

5
app/routes/register.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { Register } from '~/components/auth/Register';
export default function RegisterPage() {
return <Register />;
}

17
app/utils/api.ts Normal file
View 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
View 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
View 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 };