Fix bit(1) import coercion and subpath login redirect
The WJ import loader coerced MySQL bit(1) columns with bool(), which is always true for the bytes pymysql returns; applications.isinstallable and notifications.isshopfloor imported as 1 for every row. Route both through the existing _truthy_bit helper. The loader also no longer requires the employee source database: without it, notifications keep the bare SSO and the verify stage skips the employee count. Under a subpath mount, the session-expiry interceptor stored the browser path (which includes the mount base) as the login redirect, and the router applied its base again after login. Add stripBase() and store base-free router paths; the login page also strips a stale base from old bookmarked redirects. Also point the external-plugin CI helper at the repo's GitHub URL.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { withBase } from './../utils/basePath'
|
import { withBase, stripBase } from './../utils/basePath'
|
||||||
|
|
||||||
// BASE_URL ends in '/', so this is '/api' at root or '/ops/api' under a subpath
|
// BASE_URL ends in '/', so this is '/api' at root or '/ops/api' under a subpath
|
||||||
// mount. Keeps the SPA, its API, and IIS all on the same mount path.
|
// mount. Keeps the SPA, its API, and IIS all on the same mount path.
|
||||||
@@ -35,8 +35,10 @@ api.interceptors.response.use(
|
|||||||
// Preserve the destination so login returns the user to this page.
|
// Preserve the destination so login returns the user to this page.
|
||||||
if (hadToken) {
|
if (hadToken) {
|
||||||
const loginPath = withBase('/login')
|
const loginPath = withBase('/login')
|
||||||
const here = window.location.pathname + window.location.search
|
// Router paths exclude the mount base; strip it or login's
|
||||||
const target = here && here !== loginPath
|
// router.push double-prefixes under a subpath mount.
|
||||||
|
const here = stripBase(window.location.pathname) + window.location.search
|
||||||
|
const target = here && here !== '/login'
|
||||||
? loginPath + '?redirect=' + encodeURIComponent(here)
|
? loginPath + '?redirect=' + encodeURIComponent(here)
|
||||||
: loginPath
|
: loginPath
|
||||||
window.location.href = target
|
window.location.href = target
|
||||||
|
|||||||
@@ -11,3 +11,15 @@ export function withBase(path) {
|
|||||||
if (/^([a-z]+:)?\/\//i.test(path) || path.startsWith('data:')) return path
|
if (/^([a-z]+:)?\/\//i.test(path) || path.startsWith('data:')) return path
|
||||||
return BASE_URL + String(path).replace(/^\//, '')
|
return BASE_URL + String(path).replace(/^\//, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inverse of withBase: turn a browser pathname (which includes the mount
|
||||||
|
// base, e.g. '/ops/computers') into a router path ('/computers'). Router
|
||||||
|
// navigation already applies the base; feeding it an un-stripped pathname
|
||||||
|
// double-prefixes ('/ops/ops/...').
|
||||||
|
export function stripBase(path) {
|
||||||
|
if (!path) return path
|
||||||
|
if (BASE_URL !== '/' && path.startsWith(BASE_URL)) {
|
||||||
|
return '/' + path.slice(BASE_URL.length)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
import { setupApi } from '../api'
|
import { setupApi } from '../api'
|
||||||
import { getSiteLogo } from '../utils/siteSettings'
|
import { getSiteLogo } from '../utils/siteSettings'
|
||||||
import { withBase } from '../utils/basePath'
|
import { withBase, stripBase } from '../utils/basePath'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -63,7 +63,9 @@ const authStore = useAuthStore()
|
|||||||
function postLoginTarget() {
|
function postLoginTarget() {
|
||||||
const redirect = route.query.redirect
|
const redirect = route.query.redirect
|
||||||
if (typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//')) {
|
if (typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//')) {
|
||||||
return redirect
|
// Tolerate a redirect that still carries the mount base (old bookmarks,
|
||||||
|
// pre-fix interceptor URLs): router paths must be base-free.
|
||||||
|
return stripBase(redirect)
|
||||||
}
|
}
|
||||||
return '/'
|
return '/'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<code>common</code> manifest uses PC types to hit a subset; a per-type
|
<code>common</code> manifest uses PC types to hit a subset; a per-type
|
||||||
manifest already only runs on its own type, so it shows just the niche
|
manifest already only runs on its own type, so it shows just the niche
|
||||||
gate it needs (CMM version, a bay number, a subtype).</p>
|
gate it needs (CMM version, a bay number, a subtype).</p>
|
||||||
<p>Full guide: the GE-ENFORCE page in the project wiki.</p>
|
<p>Full guide: <code>the GE-ENFORCE wiki page</code>.</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ the machineid->assetid crosswalk this harness persists.
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
import pymysql
|
||||||
|
|
||||||
from .harness import Harness
|
from .harness import Harness
|
||||||
|
|
||||||
|
|
||||||
@@ -451,7 +453,7 @@ def stage_applications(h):
|
|||||||
continue
|
continue
|
||||||
payload = {'appname': name, 'appdescription': a['appdescription'],
|
payload = {'appname': name, 'appdescription': a['appdescription'],
|
||||||
'supportteamid': h.ids.get('supportteam', a['supportteamid']),
|
'supportteamid': h.ids.get('supportteam', a['supportteamid']),
|
||||||
'isinstallable': bool(a['isinstallable']),
|
'isinstallable': _truthy_bit(a['isinstallable']),
|
||||||
'applicationnotes': a['applicationnotes'], 'installpath': a['installpath'],
|
'applicationnotes': a['applicationnotes'], 'installpath': a['installpath'],
|
||||||
'applicationlink': a['applicationlink'],
|
'applicationlink': a['applicationlink'],
|
||||||
'documentationpath': a['documentationpath']}
|
'documentationpath': a['documentationpath']}
|
||||||
@@ -534,13 +536,17 @@ def stage_notifications(h):
|
|||||||
|
|
||||||
# SSO -> "First Last" from the employee source, so recognition/training
|
# SSO -> "First Last" from the employee source, so recognition/training
|
||||||
# notifications display a name, not a bare SSO (the model shows employeename
|
# notifications display a name, not a bare SSO (the model shows employeename
|
||||||
# or falls back to employeesso).
|
# or falls back to employeesso). Employee source is optional: without it
|
||||||
|
# (shopdb-only import) notifications keep the bare SSO.
|
||||||
ssoname = {}
|
ssoname = {}
|
||||||
for e in h.source.rows('wjf_employees_src',
|
try:
|
||||||
'SELECT SSO, First_Name, Last_Name FROM employees'):
|
for e in h.source.rows('wjf_employees_src',
|
||||||
full = f"{(e['First_Name'] or '').strip()} {(e['Last_Name'] or '').strip()}".strip()
|
'SELECT SSO, First_Name, Last_Name FROM employees'):
|
||||||
if full:
|
full = f"{(e['First_Name'] or '').strip()} {(e['Last_Name'] or '').strip()}".strip()
|
||||||
ssoname[str(e['SSO']).strip()] = full
|
if full:
|
||||||
|
ssoname[str(e['SSO']).strip()] = full
|
||||||
|
except pymysql.err.OperationalError:
|
||||||
|
print(' (no wjf_employees_src - notifications keep bare SSOs)')
|
||||||
|
|
||||||
def _names(employeesso):
|
def _names(employeesso):
|
||||||
if not employeesso:
|
if not employeesso:
|
||||||
@@ -566,7 +572,7 @@ def stage_notifications(h):
|
|||||||
'appid': h.ids.get('app', n['appid']),
|
'appid': h.ids.get('app', n['appid']),
|
||||||
'starttime': str(n['starttime']) if n['starttime'] else None,
|
'starttime': str(n['starttime']) if n['starttime'] else None,
|
||||||
'endtime': endtime, 'ticketnumber': n['ticketnumber'], 'link': n['link'],
|
'endtime': endtime, 'ticketnumber': n['ticketnumber'], 'link': n['link'],
|
||||||
'isshopfloor': bool(n['isshopfloor']), 'employeesso': n['employeesso'],
|
'isshopfloor': _truthy_bit(n['isshopfloor']), 'employeesso': n['employeesso'],
|
||||||
'employeename': _names(n['employeesso'])}
|
'employeename': _names(n['employeesso'])}
|
||||||
status, _ = h.post('/api/notifications', payload)
|
status, _ = h.post('/api/notifications', payload)
|
||||||
if status in (200, 201):
|
if status in (200, 201):
|
||||||
@@ -759,7 +765,11 @@ def stage_verify(h):
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
report = {}
|
report = {}
|
||||||
for label, sdb, sql, target_table in checks:
|
for label, sdb, sql, target_table in checks:
|
||||||
src = h.source.rows(sdb, sql)[0]['c']
|
try:
|
||||||
|
src = h.source.rows(sdb, sql)[0]['c']
|
||||||
|
except pymysql.err.OperationalError:
|
||||||
|
report[label] = 'source db absent - skipped'
|
||||||
|
continue
|
||||||
tgt = db.session.execute(text(f'SELECT COUNT(*) FROM {target_table}')).scalar()
|
tgt = db.session.execute(text(f'SELECT COUNT(*) FROM {target_table}')).scalar()
|
||||||
report[label] = f'source~{src} target={tgt}'
|
report[label] = f'source~{src} target={tgt}'
|
||||||
return report
|
return report
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
# PLUGIN_DIR ($1) path to the plugin directory (holds manifest.json). Required.
|
# PLUGIN_DIR ($1) path to the plugin directory (holds manifest.json). Required.
|
||||||
# FRAMEWORK_REF ($2) git ref to test against in CI mode. Default: main.
|
# FRAMEWORK_REF ($2) git ref to test against in CI mode. Default: main.
|
||||||
# FRAMEWORK_URL framework git URL for CI mode.
|
# FRAMEWORK_URL framework git URL for CI mode.
|
||||||
# Default: https://<git-host>/ge-aerospace/shopdb-flask.git
|
# Default: https://github.com/ge-aero/shopdb-flask.git
|
||||||
# LOCAL_FRAMEWORK path to an existing framework checkout. Set it to run offline.
|
# LOCAL_FRAMEWORK path to an existing framework checkout. Set it to run offline.
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
@@ -43,7 +43,7 @@ set -eu
|
|||||||
|
|
||||||
PLUGIN_DIR="${PLUGIN_DIR:-${1:-}}"
|
PLUGIN_DIR="${PLUGIN_DIR:-${1:-}}"
|
||||||
FRAMEWORK_REF="${FRAMEWORK_REF:-${2:-main}}"
|
FRAMEWORK_REF="${FRAMEWORK_REF:-${2:-main}}"
|
||||||
FRAMEWORK_URL="${FRAMEWORK_URL:-https://<git-host>/ge-aerospace/shopdb-flask.git}"
|
FRAMEWORK_URL="${FRAMEWORK_URL:-https://github.com/ge-aero/shopdb-flask.git}"
|
||||||
LOCAL_FRAMEWORK="${LOCAL_FRAMEWORK:-}"
|
LOCAL_FRAMEWORK="${LOCAL_FRAMEWORK:-}"
|
||||||
|
|
||||||
if [ -z "$PLUGIN_DIR" ]; then
|
if [ -z "$PLUGIN_DIR" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user