Initial import: shopdb-flask multi-site asset platform
Flask + Vue rewrite of the classic-ASP shopdb, built as a plugin framework so sister GE Aerospace sites can adopt it. Product version 0.7.0-pre-0.8.0, plugin contract 0.11.0, 966 backend tests. Key entry points: - docs/INSTALL-WINDOWS-IIS.md - tested Windows + IIS install runbook (own-site and subpath deployment methods) - docs/PILOT-DEPLOY.md - production pilot: stand up + import + verify - docs/IMPORT-API.md - schema-agnostic legacy data import contract - scripts/site_imports/wjf/ - West Jefferson reference import loader - docs/adr/ - twelve accepted architecture decision records
This commit is contained in:
99
frontend/src/stores/auth.js
Normal file
99
frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { authApi, employeesApi } from '../api'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
user: JSON.parse(localStorage.getItem('user') || 'null'),
|
||||
token: localStorage.getItem('token') || null
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isAuthenticated: (state) => !!state.token,
|
||||
username: (state) => state.user?.username || '',
|
||||
roles: (state) => state.user?.roles || [],
|
||||
hasRole: (state) => (role) => state.user?.roles?.includes(role) || false,
|
||||
isAdmin: (state) => state.user?.roles?.includes('admin') || false,
|
||||
// True when an admin-set temporary password must be changed before use.
|
||||
mustChangePassword: (state) => !!state.user?.mustchangepassword,
|
||||
// Full name from the employee directory (falls back to username/SSO).
|
||||
displayName: (state) => state.user?.directoryname || state.user?.username || '',
|
||||
// Employee photo URL if the directory has one for this SSO. The directory
|
||||
// resolver already returns a usable URL (self-hosted upload or external HR
|
||||
// path), so it is used as-is.
|
||||
avatarUrl: (state) => state.user?.directoryphotourl || null
|
||||
},
|
||||
|
||||
actions: {
|
||||
async login(username, password) {
|
||||
try {
|
||||
const response = await authApi.login(username, password)
|
||||
const { access_token, refresh_token, user } = response.data.data
|
||||
|
||||
this.token = access_token
|
||||
this.user = user
|
||||
|
||||
this.token = access_token
|
||||
localStorage.setItem('token', access_token)
|
||||
localStorage.setItem('refreshToken', refresh_token)
|
||||
await this.enrichFromDirectory()
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || 'Login failed'
|
||||
return { success: false, message }
|
||||
}
|
||||
},
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
await authApi.logout()
|
||||
} catch (e) {
|
||||
// Ignore logout errors
|
||||
}
|
||||
|
||||
this.token = null
|
||||
this.user = null
|
||||
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('refreshToken')
|
||||
localStorage.removeItem('user')
|
||||
},
|
||||
|
||||
// Clear the forced-password-change flag after a successful change so the
|
||||
// router guard stops steering the user to the change-password view.
|
||||
clearMustChangePassword() {
|
||||
if (this.user) {
|
||||
this.user.mustchangepassword = false
|
||||
localStorage.setItem('user', JSON.stringify(this.user))
|
||||
}
|
||||
},
|
||||
|
||||
async fetchUser() {
|
||||
try {
|
||||
const response = await authApi.me()
|
||||
this.user = response.data.data
|
||||
await this.enrichFromDirectory()
|
||||
} catch (error) {
|
||||
this.logout()
|
||||
}
|
||||
},
|
||||
|
||||
// Pull the logged-in user's full name + photo from the employee directory
|
||||
// by their SSO (= username). Best-effort: skipped/ignored when the employees
|
||||
// plugin is off, the SSO is not in the directory, or the lookup fails.
|
||||
async enrichFromDirectory() {
|
||||
const sso = this.user?.username
|
||||
if (sso && /^\d+$/.test(sso)) {
|
||||
try {
|
||||
const response = await employeesApi.lookup(sso)
|
||||
const emp = response.data?.data
|
||||
if (emp) {
|
||||
this.user.directoryname = `${emp.First_Name || ''} ${emp.Last_Name || ''}`.trim() || null
|
||||
this.user.directoryphotourl = emp.photourl || null
|
||||
}
|
||||
} catch (err) { /* directory unavailable - fall back to username */ }
|
||||
}
|
||||
localStorage.setItem('user', JSON.stringify(this.user))
|
||||
}
|
||||
}
|
||||
})
|
||||
39
frontend/src/stores/theme.js
Normal file
39
frontend/src/stores/theme.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const STORAGE_KEY = 'shopdb-theme'
|
||||
|
||||
// Get initial theme from localStorage or system preference
|
||||
function getInitialTheme() {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) return stored
|
||||
|
||||
// Fall back to system preference
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark'
|
||||
}
|
||||
return 'light'
|
||||
}
|
||||
|
||||
export const currentTheme = ref(getInitialTheme())
|
||||
|
||||
// Apply theme to document
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
localStorage.setItem(STORAGE_KEY, theme)
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
applyTheme(currentTheme.value)
|
||||
|
||||
// Watch for changes
|
||||
watch(currentTheme, (newTheme) => {
|
||||
applyTheme(newTheme)
|
||||
})
|
||||
|
||||
export function toggleTheme() {
|
||||
currentTheme.value = currentTheme.value === 'dark' ? 'light' : 'dark'
|
||||
}
|
||||
|
||||
export function setTheme(theme) {
|
||||
currentTheme.value = theme
|
||||
}
|
||||
Reference in New Issue
Block a user