layout v0.0.1

This commit is contained in:
isekaijoucyo 2025-02-08 08:33:13 +08:00
parent f7a295ba08
commit a03c5229f3
13 changed files with 415 additions and 87 deletions

8
db_scripts/test.py Normal file
View file

@ -0,0 +1,8 @@
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
backup_dir = os.getenv('BACKUP_DIR', '/default/backup/path')
print(backup_dir)

View file

@ -5,19 +5,30 @@ from bson.objectid import ObjectId
from bson.json_util import dumps
from datetime import datetime
from bson import errors as bson_errors
import os
import uuid
import jwt
from dotenv import load_dotenv
# Load environment variables before initializing app
load_dotenv()
app = Flask(__name__)
cors = CORS(app, resources={
r"/api/*": {
"origins": ["http://localhost:3000", "http://localhost:3001"],
"methods": ["GET", "POST", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type"]
"allow_headers": ["Content-Type", "Authorization"] # Allow Authorization header
}
})
app.config['CORS_HEADERS'] = 'Content-Type'
# Load JWT secret key from environment variable
SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'default_secret_key')
TOKEN_LV1 = os.getenv('JWT_TOKEN_LV1', 'default_token_lv1')
TOKEN_LV2 = os.getenv('JWT_TOKEN_LV2', 'default_token_lv2')
client = MongoClient('localhost', 27017)
db = client.flask_db
@ -26,6 +37,20 @@ todos = db.todos
# 确保UUID字段的唯一性
todos.create_index('uuid', unique=True)
def check_permission(token, required_level):
print(f"SECRET: {SECRET_KEY}")
print(f"TOKEN1: {TOKEN_LV1}")
print(f"TOKEN2: {TOKEN_LV2}")
print(f"TOKENIN: {token}")
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
print(f"PAYLOAD: {payload}")
return payload.get('level', 0) >= required_level
except jwt.ExpiredSignatureError:
return False
except jwt.InvalidTokenError:
return False
@app.route('/api/todos', methods=['GET'])
@cross_origin()
def get_todos():
@ -35,6 +60,10 @@ def get_todos():
@app.route('/api/todos', methods=['POST'])
@cross_origin()
def create_todo():
token = request.headers.get('Authorization', '').split(' ')[-1]
if not check_permission(token, 1):
return jsonify({'error': 'Unauthorized'}), 403
content = request.json.get('content')
degree = request.json.get('degree')
timestamp = datetime.now()
@ -50,6 +79,10 @@ def create_todo():
@app.route('/api/todos/<uuid>', methods=['DELETE'])
@cross_origin()
def delete_todo(uuid):
token = request.headers.get('Authorization', '').split(' ')[-1]
if not check_permission(token, 2):
return jsonify({'error': 'Unauthorized'}), 403
result = todos.delete_one({"uuid": uuid})
if result.deleted_count > 0:
return '', 204

View file

@ -0,0 +1,5 @@
import secrets
# Generate a random secret key for JWT
jwt_secret_key = secrets.token_urlsafe(32)
print(jwt_secret_key)

View file

@ -0,0 +1,35 @@
import jwt
import os
from datetime import datetime, timedelta
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# 加载密钥
jwt_secret_key = os.getenv('JWT_SECRET_KEY', '')
# 生成 JWT 的函数
def generate_token(level):
payload = {
'level': level, # 权限等级
'exp': datetime.utcnow() + timedelta(days=365 * 10 + 3) # 令牌过期时间
}
token = jwt.encode(payload, jwt_secret_key, algorithm='HS256')
return token
def generate_static_token():
payload = {
'level': 1, # 权限等级
'exp': 1024 # 令牌过期时间
}
token = jwt.encode(payload, jwt_secret_key, algorithm='HS256')
return token
# 生成不同权限等级的令牌
token_level_1 = generate_token(1)
token_level_2 = generate_token(2)
print("Token for level 1:", token_level_1)
print("Token for level 2:", token_level_2)
print("Static Token:", generate_static_token())

View file

@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"axios": "^1.6.2",
"framer-motion": "^12.4.0",
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
@ -872,6 +873,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.0.tgz",
"integrity": "sha512-QX92ThRniev3YTkjXOV/7m9fXBRQ5xDDRWDinuU//Xkjh+q9ppg3Nb0b95xgJYd2AE0rgJ7eoihY+Z+jjSv22w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.0.0",
"motion-utils": "^12.0.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1177,6 +1205,21 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/motion-dom": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz",
"integrity": "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.0.0"
}
},
"node_modules/motion-utils": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz",
"integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==",
"license": "MIT"
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",

View file

@ -10,18 +10,19 @@
"lint": "next lint"
},
"dependencies": {
"axios": "^1.6.2",
"framer-motion": "^12.4.0",
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.6.2"
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^20.10.4",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.17",
"typescript": "^5.3.3",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6"
"tailwindcss": "^3.3.6",
"typescript": "^5.3.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

View file

@ -0,0 +1,100 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { TodoAPI } from '../utils/api';
type TodoFormProps = {
onTodoAdded: () => void;
};
export default function TodoForm({ onTodoAdded }: TodoFormProps) {
const [content, setContent] = useState('');
const [degree, setDegree] = useState<'Important' | 'Unimportant'>('Important');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim()) return;
try {
await TodoAPI.create({ content, degree });
setContent('');
setDegree('Important');
onTodoAdded();
} catch (error) {
console.error('Error creating todo:', error);
}
};
return (
<motion.form
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
onSubmit={handleSubmit}
className="bg-white rounded-xl shadow-lg p-6 mb-8 space-y-6"
>
<div>
<label htmlFor="content" className="block text-gray-700 font-semibold mb-2">
</label>
<input
type="text"
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="输入待办事项..."
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
required
/>
</div>
<div className="space-y-4">
<p className="text-gray-700 font-semibold"></p>
<div className="flex items-center gap-8">
<label className="relative flex items-center cursor-pointer hover:opacity-80 transition-opacity">
<input
type="radio"
name="degree"
value="Important"
checked={degree === 'Important'}
onChange={(e) => setDegree(e.target.value as 'Important')}
className="hidden"
/>
<div className={`w-5 h-5 border-2 rounded-full mr-3 ${degree === 'Important' ? 'border-red-500 bg-red-500' : 'border-gray-300'}`}>
{degree === 'Important' && (
<div className="w-2 h-2 bg-white rounded-full absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" />
)}
</div>
<span className="text-gray-700"></span>
</label>
<label className="relative flex items-center cursor-pointer hover:opacity-80 transition-opacity">
<input
type="radio"
name="degree"
value="Unimportant"
checked={degree === 'Unimportant'}
onChange={(e) => setDegree(e.target.value as 'Unimportant')}
className="hidden"
/>
<div className={`w-5 h-5 border-2 rounded-full mr-3 ${degree === 'Unimportant' ? 'border-gray-500 bg-gray-500' : 'border-gray-300'}`}>
{degree === 'Unimportant' && (
<div className="w-2 h-2 bg-white rounded-full absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" />
)}
</div>
<span className="text-gray-700"></span>
</label>
</div>
</div>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
type="submit"
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
</motion.button>
</motion.form>
);
}

View file

@ -0,0 +1,55 @@
'use client';
import { motion } from 'framer-motion';
import { Todo } from '../utils/api';
type TodoItemProps = {
todo: Todo;
onDelete: (uuid: string) => void;
};
export default function TodoItem({ todo, onDelete }: TodoItemProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
className="bg-white rounded-xl shadow-sm hover:shadow-md p-6 flex justify-between items-start transition-all duration-200 border border-gray-100"
>
<div className="space-y-3">
<p className="text-lg font-semibold text-gray-800 leading-relaxed">{todo.content}</p>
<div className="flex items-center gap-3">
<span
className={`inline-flex items-center px-3 py-1 text-sm rounded-full font-medium transition-colors ${todo.degree === 'Important' ? 'bg-red-50 text-red-600 ring-1 ring-red-500/10' : 'bg-gray-50 text-gray-600 ring-1 ring-gray-500/10'}`}
>
{todo.degree === 'Important' ? (
<>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20" width="12" height="12">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20" width="12" height="12">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</>
) : '普通'}
</span>
<p className="text-sm text-gray-400">
{new Date(todo.timestamp.$date).toLocaleString('zh-CN')}
</p>
</div>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onDelete(todo.uuid)}
className="text-gray-400 hover:text-red-500 p-2 rounded-lg hover:bg-red-50/50 transition-all duration-200"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</motion.button>
</motion.div>
);
}

View file

@ -0,0 +1,45 @@
'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { Todo } from '../utils/api';
import TodoItem from '../components/TodoItem';
type TodoListProps = {
todos: Todo[];
isLoading: boolean;
onDelete: (uuid: string) => void;
};
export default function TodoList({ todos, isLoading, onDelete }: TodoListProps) {
if (isLoading) {
return (
<div className="text-center py-12 bg-white/50 backdrop-blur-sm rounded-xl shadow-sm">
<div className="animate-spin rounded-full h-14 w-14 border-4 border-blue-100 border-b-blue-600 mx-auto"></div>
<p className="mt-6 text-gray-600 font-medium">...</p>
</div>
);
}
if (todos.length === 0) {
return (
<div className="text-center py-16 bg-white/50 backdrop-blur-sm rounded-xl shadow-sm">
<div className="text-gray-400 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<p className="text-gray-500 text-lg font-medium"></p>
</div>
);
}
return (
<div className="space-y-6">
<AnimatePresence>
{todos.map((todo) => (
<TodoItem key={todo.uuid} todo={todo} onDelete={onDelete} />
))}
</AnimatePresence>
</div>
);
}

View file

@ -1,18 +1,16 @@
'use client';
import { useState, useEffect } from 'react';
import axios from 'axios';
interface Todo {
uuid: string;
content: string;
degree: 'Important' | 'Unimportant';
}
import { TodoAPI, Todo } from './utils/api';
import Image from 'next/image';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
export default function Home() {
const [todos, setTodos] = useState<Todo[]>([]);
const [content, setContent] = useState('');
const [degree, setDegree] = useState<'Important' | 'Unimportant'>('Important');
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchTodos();
@ -20,17 +18,22 @@ export default function Home() {
const fetchTodos = async () => {
try {
const response = await axios.get('http://localhost:5000/api/todos');
setTodos(response.data);
setIsLoading(true);
const data = await TodoAPI.getAll();
setTodos(data);
} catch (error) {
console.error('Error fetching todos:', error);
} finally {
setIsLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim()) return;
try {
await axios.post('http://localhost:5000/api/todos', { content, degree });
await TodoAPI.create({ content, degree });
setContent('');
setDegree('Important');
fetchTodos();
@ -40,9 +43,9 @@ export default function Home() {
};
const handleDelete = async (uuid: string) => {
if (!confirm('Are you sure you want to delete this entry?')) return;
if (!confirm('确定要删除这条待办事项吗?')) return;
try {
await axios.delete(`http://localhost:5000/api/todos/${uuid}`);
await TodoAPI.delete(uuid);
fetchTodos();
} catch (error) {
console.error('Error deleting todo:', error);
@ -50,78 +53,19 @@ export default function Home() {
};
return (
<main className="max-w-4xl mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">FlaskTODO</h1>
<hr className="mb-6" />
<form onSubmit={handleSubmit} className="mb-8 space-y-4">
<div>
<label htmlFor="content" className="block font-bold mb-2">
Todo content
</label>
<input
type="text"
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Todo Content"
className="w-full p-2 border rounded"
required
/>
<main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4 flex items-center justify-center gap-2">
<Image src="/todo-icon.png" alt="Todo Icon" width={40} height={40} className="rounded-lg" />
FlaskTODO
</h1>
<p className="text-lg text-gray-600"></p>
</div>
<div className="space-y-2">
<p className="font-bold">Degree</p>
<div className="space-x-4">
<label className="inline-flex items-center">
<input
type="radio"
name="degree"
value="Important"
checked={degree === 'Important'}
onChange={(e) => setDegree(e.target.value as 'Important')}
className="mr-2"
/>
Important
</label>
<label className="inline-flex items-center">
<input
type="radio"
name="degree"
value="Unimportant"
checked={degree === 'Unimportant'}
onChange={(e) => setDegree(e.target.value as 'Unimportant')}
className="mr-2"
/>
Unimportant
</label>
</div>
</div>
<TodoForm onTodoAdded={fetchTodos} />
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Submit
</button>
</form>
<hr className="mb-6" />
<div className="space-y-4">
{todos.map((todo) => (
<div key={todo.uuid} className="bg-gray-100 p-4 rounded">
<p>
[{todo.uuid}]: {todo.content} <i>({todo.degree})</i>
</p>
<button
onClick={() => handleDelete(todo.uuid)}
className="mt-2 bg-red-500 text-white px-3 py-1 rounded text-sm hover:bg-red-600"
>
Delete Todo
</button>
</div>
))}
<TodoList todos={todos} isLoading={isLoading} onDelete={handleDelete} />
</div>
</main>
);

View file

@ -0,0 +1,13 @@
export type Todo = {
uuid: string;
content: string;
degree: 'Important' | 'Unimportant';
timestamp: {
$date: string;
};
};
export type TodoFormData = {
content: string;
degree: 'Important' | 'Unimportant';
};

View file

@ -0,0 +1,46 @@
import axios from 'axios';
const API_BASE_URL = 'http://localhost:5000/api';
// 这里使用一个简单的token实际项目中应该通过登录获取
const TOKEN_LV1 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6MSwiZXhwIjoyMDU0NTIyMzYzfQ.omSk_0tirDTm3mNo9Mf9gKUnLs983pKC2auwI8WyA-s';
const TOKEN_LV2 = 'default_token_lv2';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${TOKEN_LV1}` // 默认使用level 1的token
}
});
export interface Todo {
uuid: string;
content: string;
degree: string;
timestamp: {
$date: string;
};
}
export const TodoAPI = {
async getAll(): Promise<Todo[]> {
const response = await api.get('/todos');
return response.data;
},
async create(data: { content: string; degree: string }): Promise<{ id: string }> {
const response = await api.post('/todos', data);
return response.data;
},
async delete(uuid: string): Promise<void> {
// 删除操作需要level 2的权限
const response = await api.delete(`/todos/${uuid}`, {
headers: {
'Authorization': `Bearer ${TOKEN_LV2}`
}
});
return response.data;
}
};