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:
2026-07-14 07:58:51 -04:00
commit a9d0dfff4a
599 changed files with 88557 additions and 0 deletions

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

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