Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ The system is flat by default. Surfaces sit on the page with borders and tonal f
### Shadow Vocabulary

- **Focus ring** (`box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb`): The keyboard-focus halo on form controls and buttons. On the dark welcome splash this becomes a gold ring (`0 0 0 0.1875rem rgba(255,197,25,0.85)`) since default outlines vanish on the photo.
- **Splash card** (`box-shadow: 0 1.875rem 5rem rgba(0,0,0,0.45)`): The single heavy elevation in the systemthe white sign-in card lifting off the full-bleed hero photo. Dramatic on purpose, and used exactly once.
- **Splash card** (`box-shadow: 0 1.875rem 5rem rgba(2,40,81,0.5)`): The single heavy elevation in the system: the white sign-in card lifting off the full-bleed hero photo. Dramatic on purpose, and used exactly once. Tinted Aggie Blue rather than black, like the splash's text halos, so every darkening layer on the page shares the hero's ambient hue.
- **Sticky table header** (`background-color: #eee`): Tonal layering, not shadow — the sticky `q-table` header reads as elevated through fill and stickiness alone.

### Named Rules
Expand Down
35 changes: 34 additions & 1 deletion VueApp/src/CAHFS/pages/CAHFSAuth.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
<template>
<ContentBlock content-block-name="cahfs-login"></ContentBlock>
<div>
<ContentBlock content-block-name="cahfs-login"></ContentBlock>

<q-card
flat
bordered
class="bg-ucdavis-blue-10 q-mt-md"
>
<q-card-section class="row items-start no-wrap">
<q-icon
name="login"
color="primary"
size="sm"
class="q-mr-md"
/>
<div>
<h2 class="text-subtitle1 text-weight-bold text-primary q-mt-none q-mb-xs">Members sign-in</h2>
<div class="text-body2">
Sign in with your UC Davis account to open web reports and section pages.
</div>
<q-btn
:href="loginHref"
type="a"
color="primary"
no-caps
label="Sign in"
class="q-mt-md"
/>
</div>
</q-card-section>
</q-card>
</div>
</template>

<script setup lang="ts">
Expand All @@ -8,12 +39,14 @@ import { inject, onMounted } from "vue"
import { useRouter } from "vue-router"
import { useFetch } from "@/composables/ViperFetch"
import { useUserStore } from "@/store/UserStore"
import { getLoginUrl } from "@/composables/RequireLogin"
import ContentBlock from "@/CMS/components/ContentBlock.vue"

const router = useRouter()
const baseUrl = inject<string>("apiURL")
const userStore = useUserStore()
const $q = useQuasar()
const loginHref = getLoginUrl()

async function checkAuth() {
$q.loading.show({
Expand Down
4 changes: 3 additions & 1 deletion VueApp/src/CTS/pages/CtsHome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ async function initPage() {
const r = await get(baseUrl + "loggedInUser")
if (!r.success || !r.result.userId) {
window.location.href =
import.meta.env.VITE_VIPER_HOME + "login?ReturnUrl=" + import.meta.env.VITE_VIPER_HOME + "CTS/"
import.meta.env.VITE_VIPER_HOME +
"welcome?ReturnUrl=" +
encodeURIComponent(import.meta.env.VITE_VIPER_HOME + "CTS/" + window.location.search)
} else {
userStore.loadUser(r.result)
}
Expand Down
Binary file removed VueApp/src/assets/UCDSVMLogo.png
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added VueApp/src/assets/logo-vetmed-stacked-lockup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added VueApp/src/assets/rod-of-asclepius-white.avif
Binary file not shown.
Binary file added VueApp/src/assets/rod-of-asclepius-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion VueApp/src/components/GenericError.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<a
:href="loginUrl"
target="_blank"
rel="noopener noreferrer"
>
Click to log in, and then try your action again.
<q-icon name="launch"></q-icon>
Expand All @@ -34,11 +35,12 @@
import { computed, ref, watch } from "vue"
import { useErrorStore } from "@/store/ErrorStore"
import { storeToRefs } from "pinia"
import { getLoginUrl } from "@/composables/RequireLogin"

const errorStore = useErrorStore()
const { errorMessage, status } = storeToRefs(errorStore)
const showErrorMessage = computed(() => errorMessage.value !== null && errorMessage.value.length > 0)
const loginUrl = import.meta.env.VITE_VIPER_HOME + "login?ReturnUrl=" + window.location.pathname
const loginUrl = getLoginUrl()

const showError = ref(false)
const showLogin = ref(false)
Expand Down
25 changes: 16 additions & 9 deletions VueApp/src/composables/RequireLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@ const ALLOWED_INTERNAL_PREFIXES = ["/", "/2/", "/vue/"]
* Builds a login URL with a validated return path.
* Falls back to home if the path fails validation.
*/
function buildLoginUrl(returnPath: string): string {
// The endpoint param selects the destination: "welcome" is the passive splash (auth-challenge /
// guard redirects); "login" goes straight to CAS for explicit "Log in" buttons, so a deliberate
// click isn't met with another sign-in screen.
function buildLoginUrl(returnPath: string, endpoint: "welcome" | "login" = "welcome"): string {
const viperHome = import.meta.env.VITE_VIPER_HOME ?? "/"
const applicationBase = viperHome.length === 1 ? "" : viperHome.slice(0, -1)
const fallbackPath = `${applicationBase}/`

if (isValidInternalPath(returnPath)) {
return `${viperHome}login?ReturnUrl=${encodeURIComponent(returnPath)}`
return `${viperHome}${endpoint}?ReturnUrl=${encodeURIComponent(returnPath)}`
}
return `${viperHome}login?ReturnUrl=${encodeURIComponent(fallbackPath)}`
return `${viperHome}${endpoint}?ReturnUrl=${encodeURIComponent(fallbackPath)}`
}

/**
Expand All @@ -41,12 +44,14 @@ function getCurrentPath(): string {
*/
function getLoginUrl(): ComputedRef<string> {
const route = useRoute()
const viperHome = import.meta.env.VITE_VIPER_HOME ?? "/"
const applicationBase = viperHome.length === 1 ? "" : viperHome.slice(0, -1)
return computed(() => {
// Use route.fullPath length check to create reactive dependency while using the value
if (route.fullPath.length >= 0) {
return buildLoginUrl(getCurrentPath())
}
return buildLoginUrl(getCurrentPath())
// Reading route.fullPath makes this reactive (the URL recomputes after navigation), but it
// omits the app base the router was created with, so prefix it back (same pattern as
// requireLogin). Fall back to the browser location outside a router context (e.g. unit tests).
const returnPath = route ? `${applicationBase}${route.fullPath}` : getCurrentPath()
return buildLoginUrl(returnPath, "login")
})
}

Expand All @@ -56,10 +61,12 @@ function isValidInternalPath(path: string): boolean {
return false
}

// Reject absolute URLs, path traversal, and encoded bypasses
// Reject absolute URLs, path traversal, backslashes, and encoded bypasses. Backslashes are
// rejected because some browsers treat "/\" or "/\\" as protocol-relative (external) redirects.
if (
ABSOLUTE_URL_REGEX.test(path) ||
path.includes("../") ||
path.includes("\\") ||
ENCODED_SLASH_REGEX.test(path) ||
ENCODED_DOT_REGEX.test(path)
) {
Expand Down
73 changes: 32 additions & 41 deletions VueApp/src/layouts/ViperLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,48 +12,35 @@
height-hint="98"
class="no-print"
>
<div
v-show="false"
id="headerPlaceholder"
>
<div
id="topPlaceholder"
class="row items-center"
>
<a
class="q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--rectangle q-btn--actionable q-focusable q-hoverable q-btn--no-uppercase q-btn--dense gt-sm text-white"
tabindex="0"
:href="viperHome"
<q-toolbar class="items-end">
<div class="viper-brand">
<div
class="viper-brand__mark"
aria-hidden="true"
>
<span class="q-focus-helper"></span>
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<i
class="q-icon notranslate material-icons"
aria-hidden="true"
role="img"
>home</i
>
<span class="mainLayoutViper">VIPER 2.0</span>
<span
v-if="environment == 'DEVELOPMENT'"
class="mainLayoutViperMode"
>Development</span
>
<span
v-if="environment == 'TEST'"
class="mainLayoutViperMode"
>Test</span
>
</span>
</a>
<picture>
<source
:srcset="rodMarkAvif"
type="image/avif"
/>
<img
:src="rodMark"
alt=""
/>
</picture>
</div>
<picture>
<source
:srcset="schoolLockupAvif"
type="image/avif"
/>
<img
class="viper-brand__name"
:src="schoolLockup"
alt="UC Davis Weill School of Veterinary Medicine"
/>
</picture>
</div>
</div>
<q-toolbar class="items-end">
<img
src="@/assets/UCDSVMLogo.png"
height="50"
alt="UC Davis School of Veterinary Medicine Logo"
/>

<!--Mini navigation for small screens-->
<q-btn
Expand Down Expand Up @@ -145,7 +132,7 @@
<router-view></router-view>
</div>
<div
v-show="!userStore.isLoggedIn"
v-show="!userStore.isLoggedIn && !showViewWhenNotLoggedIn"
class="q-pa-xl flex flex-center"
>
<q-card
Expand Down Expand Up @@ -194,6 +181,10 @@

<script setup lang="ts">
import { ref } from "vue"
import rodMark from "@/assets/rod-of-asclepius-white.png"
import rodMarkAvif from "@/assets/rod-of-asclepius-white.avif"
import schoolLockup from "@/assets/logo-vetmed-stacked-lockup.png"
import schoolLockupAvif from "@/assets/logo-vetmed-stacked-lockup.avif"
import { useUserStore } from "@/store/UserStore"
import { getLoginUrl } from "@/composables/RequireLogin"
import LeftNav from "@/layouts/LeftNav.vue"
Expand Down
49 changes: 0 additions & 49 deletions VueApp/src/layouts/ViperLayoutSimple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const props = defineProps<{
}>()

const userStore = useUserStore()
const environment = import.meta.env.VITE_ENVIRONMENT
const viperHome = import.meta.env.VITE_VIPER_HOME
const currentYear = new Date().getFullYear()
</script>
Expand All @@ -36,54 +35,6 @@ const currentYear = new Date().getFullYear()
height-hint="98"
class="bg-white text-dark"
>
<div
v-show="false"
id="headerPlaceholder"
>
<a href="/"
><img
src="https://viper.vetmed.ucdavis.edu/images/vetmed_logo.jpg"
alt="UC Davis Veterinary Medicine logo"
border="0"
width="134"
height="24"
/></a>
<div
id="topPlaceholder"
class="row items-center"
>
<a
class="q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--rectangle q-btn--actionable q-focusable q-hoverable q-btn--no-uppercase q-btn--dense gt-sm text-white"
tabindex="0"
:href="viperHome"
>
<span class="q-focus-helper"></span>
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<i
class="q-icon notranslate material-icons"
aria-hidden="true"
role="img"
>home</i
>
<span class="mainLayoutViper">VIPER 2.0</span>
<q-badge
v-if="environment == 'DEVELOPMENT'"
color="negative"
role="presentation"
class="mainLayoutViperMode"
>Development</q-badge
>
<q-badge
v-if="environment == 'TEST'"
color="negative"
role="presentation"
class="mainLayoutViperMode"
>Test</q-badge
>
</span>
</a>
</div>
</div>
<q-toolbar>
<q-btn
flat
Expand Down
Loading
Loading