Refactor frontend to Vue 3 + Vite + Pinia + Cypress E2E
- Replace single-file 8441-line HTML with Vue 3 SPA - Pinia stores: auth, oils, recipes, diary, ui - Composables: useApi, useDialog, useSmartPaste, useOilTranslation - 6 shared components: RecipeCard, RecipeDetailOverlay, TagPicker, etc. - 9 page views: RecipeSearch, RecipeManager, Inventory, OilReference, etc. - 14 Cypress E2E test specs (113 tests), all passing - Multi-stage Dockerfile (Node build + Python runtime) - Demo video generation scripts (TTS + subtitles + screen recording) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
103
frontend/src/stores/auth.js
Normal file
103
frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { api } from '../composables/useApi'
|
||||
|
||||
const DEFAULT_USER = {
|
||||
id: null,
|
||||
role: 'viewer',
|
||||
username: 'anonymous',
|
||||
display_name: '匿名',
|
||||
has_password: false,
|
||||
business_verified: false,
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref(localStorage.getItem('oil_auth_token') || '')
|
||||
const user = ref({ ...DEFAULT_USER })
|
||||
|
||||
// Getters
|
||||
const isLoggedIn = computed(() => user.value.id !== null)
|
||||
const isAdmin = computed(() => user.value.role === 'admin')
|
||||
const canEdit = computed(() =>
|
||||
['editor', 'senior_editor', 'admin'].includes(user.value.role)
|
||||
)
|
||||
const isBusiness = computed(() => user.value.business_verified)
|
||||
|
||||
// Actions
|
||||
async function initToken() {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const urlToken = params.get('token')
|
||||
if (urlToken) {
|
||||
token.value = urlToken
|
||||
localStorage.setItem('oil_auth_token', urlToken)
|
||||
// Clean URL
|
||||
const url = new URL(window.location)
|
||||
url.searchParams.delete('token')
|
||||
window.history.replaceState({}, '', url)
|
||||
}
|
||||
if (token.value) {
|
||||
await loadMe()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMe() {
|
||||
try {
|
||||
const data = await api.get('/api/me')
|
||||
user.value = {
|
||||
id: data.id,
|
||||
role: data.role,
|
||||
username: data.username,
|
||||
display_name: data.display_name,
|
||||
has_password: data.has_password ?? false,
|
||||
business_verified: data.business_verified ?? false,
|
||||
}
|
||||
} catch {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
|
||||
async function login(username, password) {
|
||||
const data = await api.post('/api/login', { username, password })
|
||||
token.value = data.token
|
||||
localStorage.setItem('oil_auth_token', data.token)
|
||||
await loadMe()
|
||||
}
|
||||
|
||||
async function register(username, password, displayName) {
|
||||
const data = await api.post('/api/register', {
|
||||
username,
|
||||
password,
|
||||
display_name: displayName,
|
||||
})
|
||||
token.value = data.token
|
||||
localStorage.setItem('oil_auth_token', data.token)
|
||||
await loadMe()
|
||||
}
|
||||
|
||||
function logout() {
|
||||
token.value = ''
|
||||
localStorage.removeItem('oil_auth_token')
|
||||
user.value = { ...DEFAULT_USER }
|
||||
}
|
||||
|
||||
function canEditRecipe(recipe) {
|
||||
if (isAdmin.value || user.value.role === 'senior_editor') return true
|
||||
if (user.value.role === 'editor' && recipe._owner_id === user.value.id) return true
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
isLoggedIn,
|
||||
isAdmin,
|
||||
canEdit,
|
||||
isBusiness,
|
||||
initToken,
|
||||
loadMe,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
canEditRecipe,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user