Skip to content

Commit ffba46b

Browse files
authored
feat: implement security headers (#84) and overhaul UI/UX (#115)
1 parent 378c533 commit ffba46b

6 files changed

Lines changed: 425 additions & 402 deletions

File tree

apps/backend/src/app.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,22 @@ export async function buildApp() {
3737
credentials: true,
3838
});
3939

40-
await app.register(helmet, { contentSecurityPolicy: false });
40+
await app.register(helmet, {
41+
contentSecurityPolicy: {
42+
directives: {
43+
defaultSrc: ["'self'"],
44+
baseUri: ["'self'"],
45+
fontSrc: ["'self'", 'https:', 'data:', 'https://fonts.gstatic.com'],
46+
frameAncestors: ["'self'"],
47+
imgSrc: ["'self'", 'data:', 'https:'],
48+
objectSrc: ["'none'"],
49+
scriptSrc: ["'self'"],
50+
scriptSrcAttr: ["'none'"],
51+
styleSrc: ["'self'", 'https:', "'unsafe-inline'", 'https://fonts.googleapis.com'],
52+
upgradeInsecureRequests: [],
53+
},
54+
},
55+
});
4156

4257
await app.register(jwt, {
4358
secret: process.env.JWT_SECRET || 'dev-secret-change-me',

apps/web/src/app.css

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,50 @@
1-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
1+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@500;600;700;800;900&display=swap');
22

3-
/* light theme — default */
43
:root {
5-
/* Primary */
4+
/* Primary Palette */
65
--primary: #6366f1;
7-
--primary-light: #818cf8;
8-
--primary-dark: #4f46e5;
9-
--accent: #8b5cf6;
10-
11-
/* Background */
12-
--bg-primary: #f8fafc;
13-
--bg-secondary: #f1f5f9;
6+
--primary-glow: rgba(99, 102, 241, 0.5);
7+
--accent: #a855f7;
8+
--accent-glow: rgba(168, 85, 247, 0.4);
9+
10+
/* Backgrounds */
11+
--bg-primary: #ffffff;
12+
--bg-secondary: #f8fafc;
13+
--bg-glass: rgba(255, 255, 255, 0.7);
1414
--bg-card: #ffffff;
15-
--bg-elevated: #e2e8f0;
16-
15+
1716
/* Text */
1817
--text-primary: #0f172a;
1918
--text-secondary: #475569;
2019
--text-muted: #94a3b8;
21-
22-
/* Border */
23-
--border: #e2e8f0;
24-
25-
/* Spacing */
20+
21+
/* Effects */
22+
--border: rgba(226, 232, 240, 0.8);
23+
--border-glass: rgba(255, 255, 255, 0.3);
24+
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
25+
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
26+
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
27+
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
28+
2629
--radius: 12px;
27-
--radius-lg: 16px;
28-
--radius-xl: 24px;
29-
30-
--theme-transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
30+
--radius-lg: 20px;
31+
--radius-xl: 32px;
32+
33+
--theme-transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
3134
}
3235

33-
/* dark theme */
3436
html.dark {
35-
--bg-primary: #0f0f1a;
36-
--bg-secondary: #1a1a2e;
37-
--bg-card: #16213e;
38-
--bg-elevated: #1e293b;
39-
37+
--bg-primary: #020617;
38+
--bg-secondary: #0f172a;
39+
--bg-glass: rgba(15, 23, 42, 0.6);
40+
--bg-card: #0f172a;
41+
4042
--text-primary: #f8fafc;
41-
--text-secondary: #94a3b8;
43+
--text-secondary: #cbd5e1;
4244
--text-muted: #64748b;
43-
44-
--border: #334155;
45+
46+
--border: rgba(30, 41, 59, 0.8);
47+
--border-glass: rgba(255, 255, 255, 0.1);
4548
}
4649

4750
* {
@@ -51,20 +54,53 @@ html.dark {
5154
}
5255

5356
body {
54-
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
57+
font-family: 'Inter', sans-serif;
5558
background-color: var(--bg-primary);
5659
color: var(--text-primary);
5760
transition: var(--theme-transition);
5861
-webkit-font-smoothing: antialiased;
5962
min-height: 100vh;
63+
overflow-x: hidden;
64+
}
65+
66+
h1, h2, h3, h4, h5, h6 {
67+
font-family: 'Outfit', sans-serif;
68+
font-weight: 700;
69+
line-height: 1.1;
6070
}
6171

6272
a {
63-
color: var(--primary);
73+
color: inherit;
6474
text-decoration: none;
65-
transition: color 0.2s;
75+
transition: var(--theme-transition);
76+
}
77+
78+
.glass {
79+
background: var(--bg-glass);
80+
backdrop-filter: blur(12px);
81+
-webkit-backdrop-filter: blur(12px);
82+
border: 1px solid var(--border-glass);
83+
}
84+
85+
.gradient-text {
86+
background: linear-gradient(135deg, var(--primary), var(--accent));
87+
-webkit-background-clip: text;
88+
-webkit-text-fill-color: transparent;
89+
background-clip: text;
90+
}
91+
92+
.btn-primary {
93+
background: linear-gradient(135deg, var(--primary), var(--accent));
94+
color: white;
95+
padding: 0.8rem 1.6rem;
96+
border-radius: var(--radius);
97+
font-weight: 600;
98+
box-shadow: 0 4px 15px var(--primary-glow);
99+
border: none;
100+
cursor: pointer;
66101
}
67102

68-
a:hover {
69-
color: var(--primary-dark);
103+
.btn-primary:hover {
104+
transform: translateY(-2px);
105+
box-shadow: 0 6px 20px var(--primary-glow);
70106
}

apps/web/src/hooks.server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Handle } from '@sveltejs/kit';
2+
3+
export const handle: Handle = async ({ event, resolve }) => {
4+
const response = await resolve(event);
5+
6+
// Security Headers (Note: CSP is handled in svelte.config.js)
7+
response.headers.set('X-Content-Type-Options', 'nosniff');
8+
response.headers.set('Referrer-Policy', 'no-referrer');
9+
response.headers.set('X-Frame-Options', 'DENY');
10+
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
11+
12+
return response;
13+
};

0 commit comments

Comments
 (0)