Harden CMS security and theme contrast
This commit is contained in:
1 parent
2465ff2170
commit
4a1e1115b3
57 files changed
+1023
-231
No files matched your search
@@ -45,7 +45,7 @@ export function DiscordVerifyForm() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
|
||||
<p className="text-sm" style={{ color: "var(--color-text-muted-readable, var(--color-text-muted))" }}>
|
||||
Open Discord, ga naar <strong>Instellingen → Geavanceerd → Ontwikkelaarsmodus</strong> (aan). Klik met
|
||||
rechts op je eigen naam en kies <strong>ID kopiëren</strong>. Plak dat hier:
|
||||
</p>
|
||||
@@ -59,7 +59,7 @@ export function DiscordVerifyForm() {
|
||||
className="flex-1 focus:ring-0 border-4 rounded text-sm px-3 py-2"
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
color: "var(--color-text)",
|
||||
color: "var(--color-text-readable, var(--color-text))",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
|
||||
@@ -76,7 +76,7 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block font-semibold text-sm"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
style={{ color: "var(--color-text-readable, var(--color-text))" }}
|
||||
>
|
||||
{t("username")}
|
||||
</label>
|
||||
@@ -89,7 +89,7 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
|
||||
className="focus:ring-0 border-4 rounded w-full text-sm px-3 py-2 mt-1"
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
color: "var(--color-text)",
|
||||
color: "var(--color-text-readable, var(--color-text))",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
@@ -101,7 +101,7 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
|
||||
<label
|
||||
htmlFor="mail"
|
||||
className="block font-semibold text-sm"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
style={{ color: "var(--color-text-readable, var(--color-text))" }}
|
||||
>
|
||||
{t("email")}
|
||||
</label>
|
||||
@@ -114,13 +114,13 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
|
||||
className="focus:ring-0 border-4 rounded w-full text-sm px-3 py-2 mt-1"
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
color: "var(--color-text)",
|
||||
color: "var(--color-text-readable, var(--color-text))",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
<p className="text-xs mt-1" style={{ color: "var(--color-text-muted)" }}>
|
||||
<p className="text-xs mt-1" style={{ color: "var(--color-text-muted-readable, var(--color-text-muted))" }}>
|
||||
Geen e-mail? Je kunt later via Discord verifiëren.
|
||||
</p>
|
||||
</div>
|
||||
@@ -132,7 +132,7 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block font-semibold text-sm"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
style={{ color: "var(--color-text-readable, var(--color-text))" }}
|
||||
>
|
||||
{t("password")}
|
||||
</label>
|
||||
@@ -145,7 +145,7 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
|
||||
className="focus:ring-0 border-4 rounded w-full text-sm px-3 py-2 mt-1"
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
color: "var(--color-text)",
|
||||
color: "var(--color-text-readable, var(--color-text))",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
@@ -157,7 +157,7 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
|
||||
<label
|
||||
htmlFor="password_confirmation"
|
||||
className="block font-semibold text-sm"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
style={{ color: "var(--color-text-readable, var(--color-text))" }}
|
||||
>
|
||||
{t("confirmPassword")}
|
||||
</label>
|
||||
@@ -170,7 +170,7 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
|
||||
className="focus:ring-0 border-4 rounded w-full text-sm px-3 py-2 mt-1"
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
color: "var(--color-text)",
|
||||
color: "var(--color-text-readable, var(--color-text))",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
@@ -209,7 +209,7 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
|
||||
<input type="hidden" name="terms" value={termsAccepted ? "1" : ""} />
|
||||
<span
|
||||
className="font-semibold cursor-pointer"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
style={{ color: "var(--color-text-readable, var(--color-text))" }}
|
||||
onClick={() => setTermsAccepted(!termsAccepted)}
|
||||
>
|
||||
{t("termsAccept", { hotel: hotelName })}
|
||||
@@ -242,7 +242,7 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-sm font-semibold hover:underline"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
style={{ color: "var(--color-primary-readable, var(--color-primary))" }}
|
||||
>
|
||||
{t("alreadyHaveAccount")}
|
||||
</Link>
|
||||
|
||||
@@ -156,7 +156,7 @@ export default async function GuestView() {
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{articles.length === 0 ? (
|
||||
<p className="py-8 text-center" style={{ color: "var(--color-text-muted)" }}>
|
||||
<p className="py-8 text-center" style={{ color: "var(--color-text-muted-readable, var(--color-text-muted))" }}>
|
||||
{t("noArticles")}
|
||||
</p>
|
||||
) : (
|
||||
|
||||
@@ -89,7 +89,7 @@ export default async function UserView({ userId, username, look }: UserViewProps
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 92%, white), color-mix(in srgb, var(--color-primary) 92%, black))",
|
||||
color: "var(--button-text-color)",
|
||||
color: "var(--button-text-color-readable, var(--button-text-color))",
|
||||
border: "1px solid color-mix(in srgb, var(--color-primary) 60%, rgba(255,255,255,0.2))",
|
||||
boxShadow:
|
||||
"0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent), inset 0 1px 0 rgba(255,255,255,0.2)",
|
||||
@@ -127,7 +127,7 @@ export default async function UserView({ userId, username, look }: UserViewProps
|
||||
{onlineFriends.length === 0 ? (
|
||||
<p
|
||||
className="mb-3 block w-full text-center text-xs font-medium md:mb-0 md:text-left"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
style={{ color: "var(--color-text-muted-readable, var(--color-text-muted))" }}
|
||||
>
|
||||
{t("noFriendsOnline")}
|
||||
</p>
|
||||
@@ -185,7 +185,7 @@ export default async function UserView({ userId, username, look }: UserViewProps
|
||||
<div className="p-3">
|
||||
<h3 className="text-sm font-semibold">{latestArticle.title}</h3>
|
||||
{latestArticle.shortStory && (
|
||||
<p className="mt-1 text-xs" style={{ color: "var(--color-text-muted)" }}>
|
||||
<p className="mt-1 text-xs" style={{ color: "var(--color-text-muted-readable, var(--color-text-muted))" }}>
|
||||
{excerpt(latestArticle.shortStory, 100)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function LanguageSwitcher() {
|
||||
l.code === locale ? "opacity-100" : "opacity-70 hover:opacity-100"
|
||||
}`}
|
||||
style={{
|
||||
color: "var(--color-text)",
|
||||
color: "var(--color-text-readable, var(--color-text))",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
|
||||
@@ -18,7 +18,7 @@ export function MobileNav({ children, menuLabel = "Open menu", closeLabel = "Clo
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex md:hidden items-center justify-center w-10 h-10 rounded-lg transition-colors duration-150"
|
||||
style={{ color: "var(--color-navbar-text)" }}
|
||||
style={{ color: "var(--color-navbar-text-readable, var(--color-navbar-text))" }}
|
||||
aria-expanded={open}
|
||||
aria-controls="mobile-menu"
|
||||
aria-label={open ? closeLabel : menuLabel}
|
||||
|
||||
@@ -249,7 +249,7 @@ export default function RadioPlayer() {
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
background: primary,
|
||||
color: "var(--button-text-color)",
|
||||
color: "var(--button-text-color-readable, var(--button-text-color))",
|
||||
fontSize: "1rem",
|
||||
lineHeight: 1,
|
||||
}}
|
||||
|
||||
@@ -9,7 +9,10 @@ export async function SiteFooter() {
|
||||
return (
|
||||
<footer
|
||||
className="mt-auto flex h-14 w-full flex-col items-center justify-center text-sm md:flex-row md:px-8"
|
||||
style={{ backgroundColor: "var(--color-surface)", color: "var(--color-text-muted)" }}
|
||||
style={{
|
||||
backgroundColor: "var(--color-surface)",
|
||||
color: "var(--color-text-muted-readable, var(--color-text-muted))",
|
||||
}}
|
||||
>
|
||||
<div className="md:font-semibold text-[12px] md:text-[14px]">
|
||||
{t("copyright", { year, hotel: hotelName ?? "Atom" })}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { FONTS } from "@/lib/theme-presets";
|
||||
import { readableColor } from "@/lib/theme-contrast";
|
||||
|
||||
// Injects the DB-driven CSS custom properties into :root, exactly like
|
||||
// AtomCMS's app.blade.php. Falls back to the atom defaults when no DB. Covers
|
||||
@@ -78,12 +79,43 @@ export async function ThemeVars() {
|
||||
const safe = (v: string | null, d: string) => (v && /^[#a-zA-Z0-9(),.\s%-]+$/.test(v) ? v : d);
|
||||
const px = (v: string | null, d: string) => (/^\d{1,3}$/.test(v ?? "") ? (v as string) : d);
|
||||
|
||||
const safeBackground = safe(background, "#f8fafc");
|
||||
const safeSurface = safe(surface, "#ffffff");
|
||||
const publicBackgrounds = [safeBackground, safeSurface];
|
||||
const readableText = readableColor(safe(text, "#0f172a"), publicBackgrounds);
|
||||
const readableMuted = readableColor(safe(textMuted, "#64748b"), publicBackgrounds);
|
||||
const readablePrimary = readableColor(safe(primary, "#f59e0b"), publicBackgrounds);
|
||||
const readableAccent = readableColor(safe(accent, "#10b981"), publicBackgrounds);
|
||||
const readableLink = readableColor(safe(linkColor, "#eeb425"), publicBackgrounds);
|
||||
const readableLinkHover = readableColor(safe(linkHover, "#cf9d15"), publicBackgrounds);
|
||||
const readableButtonText = readableColor(
|
||||
safe(buttonText, "#1e293b"),
|
||||
[safe(buttonColor, "#f59e0b")],
|
||||
);
|
||||
const readableSecondaryText = readableColor(
|
||||
safe(buttonSecondaryText, "#ffffff"),
|
||||
[safe(buttonSecondary, "#22c55e")],
|
||||
);
|
||||
const readableDangerText = readableColor(
|
||||
safe(buttonDangerText, "#ffffff"),
|
||||
[safe(buttonDanger, "#ef4444")],
|
||||
);
|
||||
const readableNavbarText = readableColor(
|
||||
safe(navbarText, "#1e293b"),
|
||||
[safe(navbar, "#ffffff")],
|
||||
);
|
||||
const readablePrimaryForeground = readableColor(
|
||||
safe(buttonText, "#1e293b"),
|
||||
[safe(primary, "#f59e0b")],
|
||||
);
|
||||
const readableAccentForeground = readableColor("#ffffff", [safe(accent, "#10b981")]);
|
||||
|
||||
const font = FONTS[fontKey ?? "nunito"] ?? FONTS.nunito;
|
||||
|
||||
const css = `:root{
|
||||
--color-primary:${safe(primary, "#f59e0b")};
|
||||
--color-background:${safe(background, "#f8fafc")};
|
||||
--color-surface:${safe(surface, "#ffffff")};
|
||||
--color-background:${safeBackground};
|
||||
--color-surface:${safeSurface};
|
||||
--color-dropdown:${safe(dropdown, "#ffffff")};
|
||||
--color-navbar:${safe(navbar, "#ffffff")};
|
||||
--color-navbar-text:${safe(navbarText, "#1e293b")};
|
||||
@@ -107,6 +139,18 @@ export async function ThemeVars() {
|
||||
--border-color:${safe(borderColor, "#eeb425")};
|
||||
--gradient-from:${safe(gradientFrom, "#f59e0b")};
|
||||
--gradient-to:${safe(gradientTo, "#10b981")};
|
||||
--color-text-readable:${readableText};
|
||||
--color-text-muted-readable:${readableMuted};
|
||||
--color-primary-readable:${readablePrimary};
|
||||
--color-accent-readable:${readableAccent};
|
||||
--color-primary-foreground-readable:${readablePrimaryForeground};
|
||||
--color-accent-foreground-readable:${readableAccentForeground};
|
||||
--color-navbar-text-readable:${readableNavbarText};
|
||||
--button-text-color-readable:${readableButtonText};
|
||||
--button-secondary-text-color-readable:${readableSecondaryText};
|
||||
--button-danger-text-color-readable:${readableDangerText};
|
||||
--link-color-readable:${readableLink};
|
||||
--link-hover-color-readable:${readableLinkHover};
|
||||
--border-radius:${px(borderRadius, "12")}px;
|
||||
--font-family:${font.stack};
|
||||
--size-heading-h1:${px(h1, "30")}px;
|
||||
|
||||
@@ -9,8 +9,8 @@ function Currency({ icon, amount, label }: { icon: string; amount: number; label
|
||||
return (
|
||||
<div className="flex gap-x-3 sm:gap-x-2 items-center">
|
||||
<div className={`h-[25px] w-[25px] rounded-full outline-offset-[3px] ${icon}`} />
|
||||
<div style={{ color: "var(--color-navbar-text, var(--color-text-muted))" }}>
|
||||
<span className="font-semibold" style={{ color: "var(--color-navbar-text, var(--color-text))" }}>
|
||||
<div style={{ color: "var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text-muted)))" }}>
|
||||
<span className="font-semibold" style={{ color: "var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))" }}>
|
||||
{amount.toLocaleString()}
|
||||
</span>{" "}
|
||||
<span>{label}</span>
|
||||
@@ -62,7 +62,7 @@ export async function TopHeader() {
|
||||
<details className="relative">
|
||||
<summary
|
||||
className="list-none cursor-pointer font-semibold [&::-webkit-details-marker]:hidden"
|
||||
style={{ color: "var(--color-navbar-text, var(--color-text))" }}
|
||||
style={{ color: "var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))" }}
|
||||
>
|
||||
Administration
|
||||
</summary>
|
||||
@@ -101,7 +101,7 @@ export async function TopHeader() {
|
||||
/>
|
||||
<span
|
||||
className="-ml-2 font-semibold"
|
||||
style={{ color: "var(--color-navbar-text, var(--color-text))" }}
|
||||
style={{ color: "var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))" }}
|
||||
>
|
||||
{session.user.name}
|
||||
</span>
|
||||
|
||||
Reference in new issue
Block a user