From b483218d9912163013ee45922716448dcd2fdac5 Mon Sep 17 00:00:00 2001 From: Neethika Prathigadapa Date: Wed, 18 Feb 2026 16:52:51 -0500 Subject: [PATCH 1/2] feat(auth): redesign access experience with enterprise UX (ref Sentinent-AI/Sentinent#12) --- src/app/app.routes.ts | 3 +- src/app/components/login/login.css | 500 ++++++++++++++++++++++++++++ src/app/components/login/login.html | 177 +++++++++- src/app/components/login/login.ts | 246 +++++++++++++- src/index.html | 3 + 5 files changed, 904 insertions(+), 25 deletions(-) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index df324c4..2f9e5b3 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,12 +1,11 @@ import { Routes } from '@angular/router'; import { Login } from './components/login/login'; -import { Signup } from './components/signup/signup'; import { authGuard } from './guards/auth-guard'; import { CreateWorkspace } from './components/workspace/create-workspace'; export const routes: Routes = [ { path: 'login', component: Login }, - { path: 'signup', component: Signup }, + { path: 'signup', component: Login }, { path: 'dashboard', loadComponent: () => import('./components/dashboard/dashboard').then(m => m.Dashboard), diff --git a/src/app/components/login/login.css b/src/app/components/login/login.css index e69de29..638f06a 100644 --- a/src/app/components/login/login.css +++ b/src/app/components/login/login.css @@ -0,0 +1,500 @@ +:root { + --bg: #fafafa; + --fg: #0a0a0a; + --subtle: rgba(10, 10, 10, 0.03); + --border: rgba(10, 10, 10, 0.12); + --muted: rgba(10, 10, 10, 0.6); + --error: #dc2626; + --success: #16a34a; + --font-sans: Inter, sans-serif; + --font-display: "Space Grotesk", sans-serif; +} + +:host-context(.dark) { + --bg: #050505; + --fg: #fafafa; + --subtle: rgba(250, 250, 250, 0.05); + --border: rgba(250, 250, 250, 0.12); + --muted: rgba(250, 250, 250, 0.5); +} + +* { + box-sizing: border-box; +} + +.noise { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + opacity: 0.02; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); +} + +.theme-toggle { + position: fixed; + top: 20px; + right: 20px; + z-index: 50; + width: 40px; + height: 40px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--bg); + color: var(--fg); + cursor: pointer; +} + +.split-layout { + min-height: 100vh; + display: grid; + grid-template-columns: 1fr 480px; + background: var(--bg); + color: var(--fg); + font-family: var(--font-sans); +} + +.brand-side { + position: relative; + overflow: hidden; + background: var(--fg); + color: var(--bg); + padding: 48px; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.brand-pattern { + position: absolute; + inset: 0; + opacity: 0.04; + background-image: radial-gradient(circle at 1px 1px, currentColor 1px, transparent 0); + background-size: 36px 36px; +} + +.brand-content, +.brand-footer { + position: relative; + z-index: 1; +} + +.brand-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 48px; +} + +.brand-icon { + width: 40px; + height: 40px; + border-radius: 10px; + background: var(--bg); + color: var(--fg); + display: grid; + place-items: center; +} + +.brand-name { + font-size: 28px; + font-weight: 700; + font-family: var(--font-display); +} + +h1 { + margin: 0 0 16px; + max-width: 560px; + font-family: var(--font-display); + font-size: 44px; + line-height: 1.12; +} + +.brand-copy { + margin: 0 0 26px; + max-width: 520px; + font-size: 18px; + line-height: 1.6; + opacity: 0.75; +} + +.trust-list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 10px; + font-size: 13px; + letter-spacing: 0.06em; + text-transform: uppercase; + opacity: 0.65; +} + +.trust-list li::before { + content: "•"; + margin-right: 8px; +} + +.brand-footer { + font-size: 12px; + opacity: 0.5; +} + +.auth-side { + position: relative; + z-index: 1; + display: grid; + place-items: center; + padding: 36px; +} + +.auth-shell { + width: 100%; + max-width: 360px; +} + +.auth-header h2 { + margin: 0 0 8px; + font-size: 32px; + font-family: var(--font-display); +} + +.auth-header p { + margin: 0 0 24px; + color: var(--muted); + font-size: 14px; +} + +.oauth-btn { + width: 100%; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 12px 16px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--bg); + color: var(--fg); + cursor: pointer; +} + +.oauth-btn:hover { + border-color: var(--fg); +} + +.oauth-btn:disabled { + opacity: 0.6; + cursor: default; +} + +.divider { + display: flex; + align-items: center; + gap: 12px; + margin: 18px 0; + color: var(--muted); + font-size: 12px; + text-transform: uppercase; +} + +.divider::before, +.divider::after { + content: ""; + flex: 1; + height: 1px; + background: var(--border); +} + +.auth-tabs { + display: flex; + background: var(--subtle); + border-radius: 10px; + padding: 4px; + gap: 6px; + margin-bottom: 20px; +} + +.auth-tab { + border: none; + background: transparent; + color: var(--muted); + border-radius: 8px; + padding: 8px 12px; + font-weight: 600; + cursor: pointer; + flex: 1; +} + +.auth-tab.active { + background: var(--bg); + color: var(--fg); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12); +} + +.form-section { + display: grid; + gap: 10px; + animation: fadeIn 0.25s ease; +} + +.input-label { + font-size: 13px; + color: var(--muted); + font-weight: 600; +} + +.input-field { + width: 100%; + border: 1px solid var(--border); + border-radius: 10px; + padding: 12px 14px; + background: var(--bg); + color: var(--fg); +} + +.input-field:focus { + outline: none; + border-color: var(--fg); + box-shadow: 0 0 0 3px var(--subtle); +} + +.input-field.error { + border-color: var(--error); +} + +.form-row { + margin: 2px 0 6px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; +} + +.checkbox-wrapper { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--muted); + font-size: 13px; +} + +.checkbox-wrapper input { + margin: 0; +} + +.checkbox-wrapper.terms { + margin-top: 6px; +} + +.checkbox-wrapper a, +.text-link, +.sso-link { + color: var(--fg); +} + +.text-link { + border: none; + background: none; + font-size: 13px; + cursor: pointer; +} + +.btn-primary { + width: 100%; + border: none; + border-radius: 10px; + padding: 12px 14px; + background: var(--fg); + color: var(--bg); + font-family: var(--font-display); + font-size: 14px; + font-weight: 700; + cursor: pointer; +} + +.btn-primary:disabled { + opacity: 0.6; + cursor: default; +} + +.sso-notice { + border-radius: 10px; + border: 1px solid var(--border); + background: var(--subtle); + padding: 14px; + font-size: 13px; + color: var(--muted); +} + +.sso-notice strong { + display: block; + color: var(--fg); + margin-bottom: 5px; +} + +.sso-notice p { + margin: 0; +} + +.domain-row { + display: grid; + grid-template-columns: 1fr auto; +} + +.domain-row .input-field { + border-radius: 10px 0 0 10px; +} + +.domain-suffix { + border: 1px solid var(--border); + border-left: none; + border-radius: 0 10px 10px 0; + padding: 12px 14px; + background: var(--subtle); + color: var(--muted); + font-size: 13px; +} + +.strength-bar { + height: 4px; + border-radius: 999px; + background: var(--border); + overflow: hidden; +} + +.strength-value { + height: 100%; + width: 0%; + transition: width 0.2s ease; +} + +.strength-value.weak { + background: #ef4444; +} + +.strength-value.good { + background: #f59e0b; +} + +.strength-value.strong { + background: #22c55e; +} + +.password-hint { + margin: -2px 0 0; + font-size: 12px; + color: var(--muted); +} + +.password-hint.weak { + color: #ef4444; +} + +.password-hint.good { + color: #d97706; +} + +.password-hint.strong { + color: #16a34a; +} + +.subtle-copy { + margin: 0 0 6px; + color: var(--muted); + font-size: 13px; +} + +.error-text, +.global-error { + margin: 0; + color: var(--error); + font-size: 13px; +} + +.back-btn { + border: none; + background: none; + color: var(--muted); + font-size: 13px; + cursor: pointer; + padding-top: 4px; +} + +.success-message { + text-align: center; + padding: 24px 8px 8px; +} + +.success-icon { + width: 48px; + height: 48px; + border-radius: 50%; + display: grid; + place-items: center; + background: var(--success); + color: #fff; + margin: 0 auto 14px; + font-size: 22px; +} + +.success-message h3 { + margin: 0 0 8px; +} + +.success-message p { + margin: 0; + color: var(--muted); +} + +.sso-link { + margin-top: 12px; + border: none; + background: none; + text-decoration: underline; + cursor: pointer; + font-size: 13px; +} + +.spinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + margin-left: 8px; + animation: spin 0.6s linear infinite; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 1024px) { + .split-layout { + grid-template-columns: 1fr; + } + + .brand-side { + display: none; + } + + .auth-side { + min-height: 100vh; + padding: 24px; + } +} diff --git a/src/app/components/login/login.html b/src/app/components/login/login.html index 4bb93e0..15a0c85 100644 --- a/src/app/components/login/login.html +++ b/src/app/components/login/login.html @@ -1,16 +1,167 @@ -
-

Login

-
-
- - + + +
+ +
+
+
+
+
+
+ +
+ Sentinent +
+ +

The System of Record for Decisions

+

+ Join enterprise teams who replace chaotic Slack threads with structured, auditable decision-making. +

+ +
    +
  • SOC 2 Type II Certified
  • +
  • GDPR Compliant
  • +
  • 99.99% Uptime SLA
  • +
-
- - + + +
+ +
+
+
+

Welcome back

+

Sign in to access your Decision Ledger

+
+ + + +
Or continue with email
+ + + + + + + + + + +
+ + +
+ + + + +
+ + + + + +

Please use your company email.

+ + + +
+
+
+

{{ passwordHint }}

+ + + + +
+ +
+ + +

We'll send a password reset link to your email address.

+ + + +
+ +
+
+ Enterprise Single Sign-On +

Enter your company domain to redirect to your identity provider.

+
+ + +
+ + .sentinent.io +
+ + + +
+ +
+
+

{{ successTitle }}

+

{{ successText }}

+
+ + + +

{{ loginError }}

- - -

{{ error }}

-

Don't have an account? Sign up

+
diff --git a/src/app/components/login/login.ts b/src/app/components/login/login.ts index 2af8e38..c5c81f8 100644 --- a/src/app/components/login/login.ts +++ b/src/app/components/login/login.ts @@ -1,8 +1,8 @@ -import { Component, inject } from '@angular/core'; +import { CommonModule, DOCUMENT } from '@angular/common'; +import { Component, OnInit, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Router, RouterModule } from '@angular/router'; import { AuthService } from '../../services/auth'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-login', @@ -11,18 +11,244 @@ import { CommonModule } from '@angular/common'; templateUrl: './login.html', styleUrl: './login.css', }) -export class Login { - email = ''; - password = ''; - error = ''; +export class Login implements OnInit { + activeTab: 'login' | 'register' = 'login'; + + loginEmail = ''; + loginPassword = ''; + rememberMe = true; + loginError = ''; + + regName = ''; + regEmail = ''; + regPassword = ''; + regTerms = false; + regEmailHasPersonalDomain = false; + + forgotEmail = ''; + ssoDomain = ''; + + showForgot = false; + showSSO = false; + showSuccess = false; + successTitle = 'Check your email'; + successText = 'We sent you a verification link.'; + + isDarkMode = false; + isOAuthLoading = false; + isLoginSubmitting = false; + isRegisterSubmitting = false; + isForgotSubmitting = false; + isSSOSubmitting = false; + + passwordStrength = 0; + passwordHint = 'Use 8+ characters with mixed case, numbers, and symbols'; + passwordStrengthClass = 'weak'; + + private readonly personalDomains = new Set(['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'aol.com']); private authService = inject(AuthService); private router = inject(Router); + private document = inject(DOCUMENT); + + ngOnInit(): void { + this.activeTab = this.router.url.startsWith('/signup') ? 'register' : 'login'; + const storedTheme = localStorage.getItem('theme'); + const prefersDark = typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches; + this.isDarkMode = storedTheme === 'dark' || (!storedTheme && prefersDark); + this.applyTheme(); + } - onSubmit() { - this.authService.login(this.email, this.password).subscribe({ - next: () => this.router.navigate(['/']), - error: (err) => this.error = 'Invalid credentials' + toggleTheme(): void { + this.isDarkMode = !this.isDarkMode; + this.applyTheme(); + localStorage.setItem('theme', this.isDarkMode ? 'dark' : 'light'); + } + + switchTab(tab: 'login' | 'register'): void { + this.activeTab = tab; + this.showForgot = false; + this.showSSO = false; + this.showSuccess = false; + this.loginError = ''; + this.router.navigate([tab === 'login' ? '/login' : '/signup']); + } + + showForgotPassword(): void { + this.showForgot = true; + this.showSSO = false; + this.showSuccess = false; + } + + showSSOForm(): void { + this.showForgot = false; + this.showSSO = true; + this.showSuccess = false; + } + + backToLogin(): void { + this.switchTab('login'); + } + + handleOAuth(provider: 'google'): void { + this.isOAuthLoading = true; + setTimeout(() => { + this.isOAuthLoading = false; + this.showSuccessMessage('Connected!', `Redirecting to ${provider}...`); + }, 1200); + } + + handleLogin(): void { + this.loginError = ''; + this.isLoginSubmitting = true; + + this.authService.login(this.loginEmail.trim(), this.loginPassword).subscribe({ + next: () => { + this.showSuccessMessage('Welcome back', 'Redirecting to your Decision Ledger...'); + setTimeout(() => this.router.navigate(['/dashboard']), 1200); + }, + error: () => { + this.loginError = 'Invalid credentials.'; + this.isLoginSubmitting = false; + } }); } + + handleRegister(): void { + this.isRegisterSubmitting = true; + this.loginError = ''; + + this.authService.signup(this.regEmail.trim(), this.regPassword).subscribe({ + next: () => { + this.showSuccessMessage('Account created!', 'Please check your email to verify your account.'); + this.isRegisterSubmitting = false; + }, + error: () => { + this.loginError = 'Registration failed. Email might already be taken.'; + this.isRegisterSubmitting = false; + } + }); + } + + handleForgot(): void { + this.isForgotSubmitting = true; + setTimeout(() => { + this.isForgotSubmitting = false; + this.showSuccessMessage('Check your email', 'We sent a password reset link.'); + }, 1200); + } + + handleSSO(): void { + this.isSSOSubmitting = true; + const domain = this.ssoDomain.trim(); + setTimeout(() => { + this.isSSOSubmitting = false; + this.showSuccessMessage('Redirecting...', `Connecting to ${domain}.sentinent.io identity provider.`); + }, 1200); + } + + checkPasswordStrength(password: string): void { + let score = 0; + + if (password.length > 8) score++; + if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++; + if (/[0-9]/.test(password)) score++; + if (/[^a-zA-Z0-9]/.test(password)) score++; + + this.passwordStrength = (score / 4) * 100; + + if (score < 2) { + this.passwordStrengthClass = 'weak'; + this.passwordHint = 'Weak password'; + return; + } + if (score < 4) { + this.passwordStrengthClass = 'good'; + this.passwordHint = 'Good, but could be stronger'; + return; + } + + this.passwordStrengthClass = 'strong'; + this.passwordHint = 'Strong password'; + } + + validateEnterpriseEmail(): void { + const domain = this.regEmail.split('@')[1]?.toLowerCase(); + this.regEmailHasPersonalDomain = !!domain && this.personalDomains.has(domain); + } + + private showSuccessMessage(title: string, text: string): void { + this.showForgot = false; + this.showSSO = false; + this.showSuccess = true; + this.successTitle = title; + this.successText = text; + } + + private applyTheme(): void { + const root = this.document.documentElement; + if (this.isDarkMode) { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + } + + get isRegisterDisabled(): boolean { + return !this.regName.trim() || !this.regEmail.trim() || !this.regPassword || !this.regTerms || this.regEmailHasPersonalDomain; + } + + get isAuthFormVisible(): boolean { + return !this.showForgot && !this.showSSO && !this.showSuccess; + } + + get isLoginVisible(): boolean { + return this.isAuthFormVisible && this.activeTab === 'login'; + } + + get isRegisterVisible(): boolean { + return this.isAuthFormVisible && this.activeTab === 'register'; + } + + get hasGlobalError(): boolean { + return !!this.loginError; + } + + clearGlobalError(): void { + this.loginError = ''; + } + + onLoginEmailInput(): void { + this.clearGlobalError(); + } + + onLoginPasswordInput(): void { + this.clearGlobalError(); + } + + onRegisterEmailInput(): void { + this.clearGlobalError(); + this.validateEnterpriseEmail(); + } + + onRegisterPasswordInput(): void { + this.clearGlobalError(); + this.checkPasswordStrength(this.regPassword); + } + + onRegisterNameInput(): void { + this.clearGlobalError(); + } + + onRegisterTermsChange(): void { + this.clearGlobalError(); + } + + onForgotEmailInput(): void { + this.clearGlobalError(); + } + + onSSODomainInput(): void { + this.clearGlobalError(); + } } diff --git a/src/index.html b/src/index.html index 047cb55..32dadb0 100644 --- a/src/index.html +++ b/src/index.html @@ -6,6 +6,9 @@ + + + From 4cf70477b0ace2f492d8c43ecee3e95eaa7e60d3 Mon Sep 17 00:00:00 2001 From: Neethika Prathigadapa Date: Wed, 18 Feb 2026 19:02:48 -0500 Subject: [PATCH 2/2] feat(auth): stabilize login/signup UX and error handling (ref Sentinent-AI/Sentinent#12) --- angular.json | 5 +- proxy.conf.json | 7 ++ src/app/components/login/login.css | 180 ++++++---------------------- src/app/components/login/login.html | 85 +++---------- src/app/components/login/login.ts | 172 ++++++++++++-------------- src/app/services/auth.ts | 15 ++- src/styles.css | 51 ++------ 7 files changed, 157 insertions(+), 358 deletions(-) create mode 100644 proxy.conf.json diff --git a/angular.json b/angular.json index df96668..3cc2f9b 100644 --- a/angular.json +++ b/angular.json @@ -80,6 +80,9 @@ }, "serve": { "builder": "@angular/build:dev-server", + "options": { + "proxyConfig": "proxy.conf.json" + }, "configurations": { "production": { "buildTarget": "sentinent-frontend:build:production" @@ -113,4 +116,4 @@ } } } -} \ No newline at end of file +} diff --git a/proxy.conf.json b/proxy.conf.json new file mode 100644 index 0000000..e7c761e --- /dev/null +++ b/proxy.conf.json @@ -0,0 +1,7 @@ +{ + "/api": { + "target": "http://127.0.0.1:8080", + "secure": false, + "changeOrigin": true + } +} diff --git a/src/app/components/login/login.css b/src/app/components/login/login.css index 638f06a..556a354 100644 --- a/src/app/components/login/login.css +++ b/src/app/components/login/login.css @@ -1,4 +1,5 @@ -:root { +:host { + display: block; --bg: #fafafa; --fg: #0a0a0a; --subtle: rgba(10, 10, 10, 0.03); @@ -41,8 +42,27 @@ border-radius: 8px; border: 1px solid var(--border); background: var(--bg); - color: var(--fg); cursor: pointer; + display: grid; + place-items: center; +} + +.theme-toggle:hover { + border-color: var(--fg); +} + +.theme-glyph { + display: block; + width: 18px; + height: 18px; + border-radius: 50%; + border: 1px solid rgba(10, 10, 10, 0.45); + background: linear-gradient(90deg, #0a0a0a 50%, #ffffff 50%); + transition: transform 0.2s ease; +} + +.theme-glyph.dark-active { + transform: rotate(180deg); } .split-layout { @@ -111,10 +131,10 @@ h1 { } .brand-copy { - margin: 0 0 26px; - max-width: 520px; + margin: 0 0 36px; + max-width: 600px; font-size: 18px; - line-height: 1.6; + line-height: 1.75; opacity: 0.75; } @@ -146,6 +166,7 @@ h1 { display: grid; place-items: center; padding: 36px; + background: var(--bg); } .auth-shell { @@ -165,47 +186,6 @@ h1 { font-size: 14px; } -.oauth-btn { - width: 100%; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 12px; - padding: 12px 16px; - border-radius: 10px; - border: 1px solid var(--border); - background: var(--bg); - color: var(--fg); - cursor: pointer; -} - -.oauth-btn:hover { - border-color: var(--fg); -} - -.oauth-btn:disabled { - opacity: 0.6; - cursor: default; -} - -.divider { - display: flex; - align-items: center; - gap: 12px; - margin: 18px 0; - color: var(--muted); - font-size: 12px; - text-transform: uppercase; -} - -.divider::before, -.divider::after { - content: ""; - flex: 1; - height: 1px; - background: var(--border); -} - .auth-tabs { display: flex; background: var(--subtle); @@ -265,22 +245,29 @@ h1 { .form-row { margin: 2px 0 6px; - display: flex; - justify-content: space-between; + width: 100%; + display: grid; + grid-template-columns: 1fr auto; align-items: center; gap: 10px; } .checkbox-wrapper { - display: inline-flex; + display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; + white-space: nowrap; } .checkbox-wrapper input { margin: 0; + flex: 0 0 auto; +} + +.checkbox-wrapper span { + white-space: nowrap; } .checkbox-wrapper.terms { @@ -288,8 +275,7 @@ h1 { } .checkbox-wrapper a, -.text-link, -.sso-link { +.text-link { color: var(--fg); } @@ -298,6 +284,8 @@ h1 { background: none; font-size: 13px; cursor: pointer; + white-space: nowrap; + justify-self: end; } .btn-primary { @@ -318,87 +306,6 @@ h1 { cursor: default; } -.sso-notice { - border-radius: 10px; - border: 1px solid var(--border); - background: var(--subtle); - padding: 14px; - font-size: 13px; - color: var(--muted); -} - -.sso-notice strong { - display: block; - color: var(--fg); - margin-bottom: 5px; -} - -.sso-notice p { - margin: 0; -} - -.domain-row { - display: grid; - grid-template-columns: 1fr auto; -} - -.domain-row .input-field { - border-radius: 10px 0 0 10px; -} - -.domain-suffix { - border: 1px solid var(--border); - border-left: none; - border-radius: 0 10px 10px 0; - padding: 12px 14px; - background: var(--subtle); - color: var(--muted); - font-size: 13px; -} - -.strength-bar { - height: 4px; - border-radius: 999px; - background: var(--border); - overflow: hidden; -} - -.strength-value { - height: 100%; - width: 0%; - transition: width 0.2s ease; -} - -.strength-value.weak { - background: #ef4444; -} - -.strength-value.good { - background: #f59e0b; -} - -.strength-value.strong { - background: #22c55e; -} - -.password-hint { - margin: -2px 0 0; - font-size: 12px; - color: var(--muted); -} - -.password-hint.weak { - color: #ef4444; -} - -.password-hint.good { - color: #d97706; -} - -.password-hint.strong { - color: #16a34a; -} - .subtle-copy { margin: 0 0 6px; color: var(--muted); @@ -447,15 +354,6 @@ h1 { color: var(--muted); } -.sso-link { - margin-top: 12px; - border: none; - background: none; - text-decoration: underline; - cursor: pointer; - font-size: 13px; -} - .spinner { display: inline-block; width: 14px; diff --git a/src/app/components/login/login.html b/src/app/components/login/login.html index 15a0c85..8911f46 100644 --- a/src/app/components/login/login.html +++ b/src/app/components/login/login.html @@ -1,6 +1,5 @@
@@ -41,35 +40,18 @@

Welcome back

Sign in to access your Decision Ledger

- - -
Or continue with email
- -
- - + + + - @@ -85,37 +67,23 @@

Welcome back

Sign In to Dashboard +

{{ loginError }}

-
- - - - - -

Please use your company email.

+ + + - -
-
-
-

{{ passwordHint }}

- - +

{{ registerError }}

@@ -131,37 +99,12 @@

Welcome back

-
-
- Enterprise Single Sign-On -

Enter your company domain to redirect to your identity provider.

-
- - -
- - .sentinent.io -
- - - -
-

{{ successTitle }}

{{ successText }}

- - -

{{ loginError }}

diff --git a/src/app/components/login/login.ts b/src/app/components/login/login.ts index c5c81f8..7687a5a 100644 --- a/src/app/components/login/login.ts +++ b/src/app/components/login/login.ts @@ -1,7 +1,9 @@ import { CommonModule, DOCUMENT } from '@angular/common'; -import { Component, OnInit, inject } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ChangeDetectorRef, Component, OnInit, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Router, RouterModule } from '@angular/router'; +import { finalize, timeout } from 'rxjs'; import { AuthService } from '../../services/auth'; @Component({ @@ -18,38 +20,27 @@ export class Login implements OnInit { loginPassword = ''; rememberMe = true; loginError = ''; + registerError = ''; - regName = ''; regEmail = ''; regPassword = ''; - regTerms = false; - regEmailHasPersonalDomain = false; forgotEmail = ''; - ssoDomain = ''; showForgot = false; - showSSO = false; showSuccess = false; successTitle = 'Check your email'; successText = 'We sent you a verification link.'; isDarkMode = false; - isOAuthLoading = false; isLoginSubmitting = false; isRegisterSubmitting = false; isForgotSubmitting = false; - isSSOSubmitting = false; - - passwordStrength = 0; - passwordHint = 'Use 8+ characters with mixed case, numbers, and symbols'; - passwordStrengthClass = 'weak'; - - private readonly personalDomains = new Set(['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'aol.com']); private authService = inject(AuthService); private router = inject(Router); private document = inject(DOCUMENT); + private cdr = inject(ChangeDetectorRef); ngOnInit(): void { this.activeTab = this.router.url.startsWith('/signup') ? 'register' : 'login'; @@ -68,21 +59,14 @@ export class Login implements OnInit { switchTab(tab: 'login' | 'register'): void { this.activeTab = tab; this.showForgot = false; - this.showSSO = false; this.showSuccess = false; this.loginError = ''; + this.registerError = ''; this.router.navigate([tab === 'login' ? '/login' : '/signup']); } showForgotPassword(): void { this.showForgot = true; - this.showSSO = false; - this.showSuccess = false; - } - - showSSOForm(): void { - this.showForgot = false; - this.showSSO = true; this.showSuccess = false; } @@ -90,42 +74,85 @@ export class Login implements OnInit { this.switchTab('login'); } - handleOAuth(provider: 'google'): void { - this.isOAuthLoading = true; - setTimeout(() => { - this.isOAuthLoading = false; - this.showSuccessMessage('Connected!', `Redirecting to ${provider}...`); - }, 1200); - } - handleLogin(): void { this.loginError = ''; + if (!this.loginEmail.trim() || !this.loginPassword.trim()) { + this.loginError = 'Invalid credentials'; + return; + } this.isLoginSubmitting = true; - this.authService.login(this.loginEmail.trim(), this.loginPassword).subscribe({ + this.authService.login(this.loginEmail.trim(), this.loginPassword).pipe( + timeout(8000), + finalize(() => { + this.isLoginSubmitting = false; + this.syncView(); + }) + ).subscribe({ next: () => { this.showSuccessMessage('Welcome back', 'Redirecting to your Decision Ledger...'); + this.syncView(); setTimeout(() => this.router.navigate(['/dashboard']), 1200); }, - error: () => { - this.loginError = 'Invalid credentials.'; - this.isLoginSubmitting = false; + error: (err: HttpErrorResponse) => { + const backendMessage = typeof err.error === 'string' ? err.error.toLowerCase() : ''; + if (err.status === 401 || backendMessage.includes('invalid credentials')) { + this.loginError = 'Invalid credentials'; + this.syncView(); + return; + } + this.loginError = 'Sign in failed. Please try again.'; + this.syncView(); } }); } handleRegister(): void { + if (!this.regEmail.trim() || !this.regPassword.trim()) { + this.registerError = 'Invalid credentials'; + return; + } this.isRegisterSubmitting = true; - this.loginError = ''; + this.registerError = ''; - this.authService.signup(this.regEmail.trim(), this.regPassword).subscribe({ - next: () => { - this.showSuccessMessage('Account created!', 'Please check your email to verify your account.'); + this.authService.signup(this.regEmail.trim(), this.regPassword).pipe( + timeout(8000), + finalize(() => { this.isRegisterSubmitting = false; + this.syncView(); + }) + ).subscribe({ + next: () => { + this.regPassword = ''; + this.loginEmail = this.regEmail.trim(); + this.showForgot = false; + this.loginError = ''; + this.registerError = ''; + this.showSuccessMessage( + 'Account is successfully created', + 'You will be redirected to login page shortly.' + ); + this.syncView(); + setTimeout(() => { + this.activeTab = 'login'; + this.showSuccess = false; + this.router.navigate(['/login']); + this.syncView(); + }, 1500); }, - error: () => { - this.loginError = 'Registration failed. Email might already be taken.'; - this.isRegisterSubmitting = false; + error: (err: HttpErrorResponse) => { + this.activeTab = 'register'; + this.showForgot = false; + this.showSuccess = false; + + const backendMessage = typeof err.error === 'string' ? err.error.toLowerCase() : ''; + if (err.status === 409 || backendMessage.includes('already exists')) { + this.registerError = 'Email already exists'; + this.syncView(); + return; + } + this.registerError = 'Registration failed. Please try again.'; + this.syncView(); } }); } @@ -138,48 +165,8 @@ export class Login implements OnInit { }, 1200); } - handleSSO(): void { - this.isSSOSubmitting = true; - const domain = this.ssoDomain.trim(); - setTimeout(() => { - this.isSSOSubmitting = false; - this.showSuccessMessage('Redirecting...', `Connecting to ${domain}.sentinent.io identity provider.`); - }, 1200); - } - - checkPasswordStrength(password: string): void { - let score = 0; - - if (password.length > 8) score++; - if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++; - if (/[0-9]/.test(password)) score++; - if (/[^a-zA-Z0-9]/.test(password)) score++; - - this.passwordStrength = (score / 4) * 100; - - if (score < 2) { - this.passwordStrengthClass = 'weak'; - this.passwordHint = 'Weak password'; - return; - } - if (score < 4) { - this.passwordStrengthClass = 'good'; - this.passwordHint = 'Good, but could be stronger'; - return; - } - - this.passwordStrengthClass = 'strong'; - this.passwordHint = 'Strong password'; - } - - validateEnterpriseEmail(): void { - const domain = this.regEmail.split('@')[1]?.toLowerCase(); - this.regEmailHasPersonalDomain = !!domain && this.personalDomains.has(domain); - } - private showSuccessMessage(title: string, text: string): void { this.showForgot = false; - this.showSSO = false; this.showSuccess = true; this.successTitle = title; this.successText = text; @@ -195,11 +182,11 @@ export class Login implements OnInit { } get isRegisterDisabled(): boolean { - return !this.regName.trim() || !this.regEmail.trim() || !this.regPassword || !this.regTerms || this.regEmailHasPersonalDomain; + return !this.regEmail.trim() || !this.regPassword; } get isAuthFormVisible(): boolean { - return !this.showForgot && !this.showSSO && !this.showSuccess; + return !this.showForgot && !this.showSuccess; } get isLoginVisible(): boolean { @@ -227,28 +214,19 @@ export class Login implements OnInit { } onRegisterEmailInput(): void { - this.clearGlobalError(); - this.validateEnterpriseEmail(); + this.registerError = ''; } onRegisterPasswordInput(): void { - this.clearGlobalError(); - this.checkPasswordStrength(this.regPassword); - } - - onRegisterNameInput(): void { - this.clearGlobalError(); - } - - onRegisterTermsChange(): void { - this.clearGlobalError(); + this.registerError = ''; } onForgotEmailInput(): void { this.clearGlobalError(); } - onSSODomainInput(): void { - this.clearGlobalError(); + private syncView(): void { + this.cdr.detectChanges(); } + } diff --git a/src/app/services/auth.ts b/src/app/services/auth.ts index 00d9eba..2f58e01 100644 --- a/src/app/services/auth.ts +++ b/src/app/services/auth.ts @@ -1,6 +1,6 @@ import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, tap } from 'rxjs'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { map, Observable, tap } from 'rxjs'; interface AuthResponse { token: string; @@ -11,11 +11,16 @@ interface AuthResponse { }) export class AuthService { private http = inject(HttpClient); - private apiUrl = 'http://localhost:8080/api'; + private apiUrl = '/api'; private tokenKey = 'sentinent_token'; - signup(email: string, password: string): Observable { - return this.http.post(`${this.apiUrl}/signup`, { email, password }); + signup(email: string, password: string): Observable { + return this.http.post(`${this.apiUrl}/signup`, { email, password }, { + observe: 'response', + responseType: 'text' + }).pipe( + map((_res: HttpResponse) => undefined) + ); } login(email: string, password: string): Observable { diff --git a/src/styles.css b/src/styles.css index 119f86c..53f96eb 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,47 +1,12 @@ -.auth-container { - max-width: 400px; - margin: 50px auto; - padding: 20px; - border: 1px solid #ccc; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); +html, +body { + margin: 0; + padding: 0; + min-height: 100%; } -.form-group { - margin-bottom: 15px; -} - -.form-group label { - display: block; - margin-bottom: 5px; -} - -.form-group input { - width: 100%; - padding: 8px; +*, +*::before, +*::after { box-sizing: border-box; } - -button { - width: 100%; - padding: 10px; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; -} - -button:hover { - background-color: #0056b3; -} - -.error { - color: red; - margin-top: 10px; -} - -.success { - color: green; - margin-top: 10px; -}