diff --git a/.gitignore b/.gitignore index b1d225e..22b307b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ /tmp /out-tsc /bazel-out +/staff + # Node /node_modules diff --git a/BACKEND_API.md b/BACKEND_API.md deleted file mode 100644 index fe66bf3..0000000 --- a/BACKEND_API.md +++ /dev/null @@ -1,528 +0,0 @@ -# Backend API Documentation - Auth Service - -Complete API documentation for implementing the backend for the DX Authentication Service. - -## ๐Ÿ“‹ Overview - -This authentication service provides QR code-based authentication with comprehensive device tracking for all DX projects (Dexar, Novo Markets, FastCheck, BackOffice). - -**Base URL:** -- Development: `http://localhost:3000/api` -- Production: `https://api.dx-projects.com/api` - ---- - -## ๐Ÿ” Endpoints - -### 1. Generate QR Code - -Generate a new QR code session for authentication. - -**Endpoint:** `POST /auth/qr/generate` - -**Request Body:** -```json -{ - "project": "dexar", - "deviceInfo": { - "deviceType": "desktop", - "deviceOS": "windows", - "context": "browser", - "project": "dexar", - "userAgent": "Mozilla/5.0...", - "screenResolution": "1920x1080", - "browserName": "Chrome", - "browserVersion": "120.0.0" - } -} -``` - -**Request Schema:** -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `project` | string | Yes | Project ID: dexar, novo, fastcheck, backoffice | -| `deviceInfo.deviceType` | string\|null | No | mobile, desktop, tablet | -| `deviceInfo.deviceOS` | string\|null | No | android, ios, windows, macos, linux | -| `deviceInfo.context` | string\|null | No | browser, application, telegram | -| `deviceInfo.project` | string | Yes | Same as root project | -| `deviceInfo.userAgent` | string | No | Browser user agent | -| `deviceInfo.screenResolution` | string | No | e.g. "1920x1080" | -| `deviceInfo.browserName` | string | No | Browser name | -| `deviceInfo.browserVersion` | string | No | Browser version | - -**Response:** `200 OK` -```json -{ - "sessionId": "550e8400-e29b-41d4-a716-446655440000", - "qrCode": "data:image/png;base64,iVBORw0KGgo...", - "expiresAt": "2026-01-26T15:30:00.000Z", - "expiresIn": 60 -} -``` - -**QR Code Content Should Be:** -```json -{ - "sessionId": "550e8400-e29b-41d4-a716-446655440000", - "apiUrl": "https://api.dx-projects.com/api" -} -``` - ---- - -### 2. Scan QR Code (Mobile Auth) - -Scan and authenticate using a QR code session. - -**Endpoint:** `POST /auth/qr/scan` - -**Request Body:** -```json -{ - "sessionId": "550e8400-e29b-41d4-a716-446655440000", - "deviceInfo": { - "deviceType": "mobile", - "deviceOS": "android", - "context": "application", - "project": "dexar", - "userAgent": "Mozilla/5.0...", - "screenResolution": "1080x2400", - "browserName": "Chrome Mobile", - "browserVersion": "120.0.0" - } -} -``` - -**Response:** `200 OK` -```json -{ - "success": true, - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "userId": "user-123", - "expiresAt": "2026-01-27T14:30:00.000Z", - "message": "Authentication successful", - "userInfo": { - "username": "john_doe", - "email": "john@example.com", - "role": "admin" - } -} -``` - ---- - -### 3. Check Auth Status (Polling) - -Check if a QR session has been authenticated. Used for polling every 2 seconds. - -**Endpoint:** `GET /auth/qr/status/:sessionId` - -**Response (Not Authenticated):** `200 OK` -```json -{ - "authenticated": false -} -``` - -**Response (Authenticated):** `200 OK` -```json -{ - "authenticated": true, - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "userId": "user-123", - "expiresAt": "2026-01-27T14:30:00.000Z", - "userInfo": { - "username": "john_doe", - "email": "john@example.com", - "role": "admin" - } -} -``` - ---- - -### 4. Traditional Login - -Username/password authentication (alternative to QR). - -**Endpoint:** `POST /auth/login` - -**Request Body:** -```json -{ - "username": "john_doe", - "password": "secure_password_123", - "deviceInfo": { - "deviceType": "desktop", - "deviceOS": "windows", - "context": "browser", - "project": "backoffice" - } -} -``` - -**Response:** `200 OK` -```json -{ - "success": true, - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "userId": "user-123", - "expiresAt": "2026-01-27T14:30:00.000Z", - "message": "Login successful", - "userInfo": { - "username": "john_doe", - "email": "john@example.com", - "role": "admin" - } -} -``` - ---- - -### 5. Validate Session - -Validate an existing JWT token. - -**Endpoint:** `POST /auth/validate` - -**Headers:** -``` -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -``` - -**Request Body:** -```json -{ - "deviceInfo": { - "deviceType": "desktop", - "deviceOS": "windows", - "context": "browser", - "project": "dexar" - } -} -``` - -**Response:** `200 OK` -```json -{ - "valid": true, - "userId": "user-123", - "expiresAt": "2026-01-27T14:30:00.000Z", - "userInfo": { - "username": "john_doe", - "email": "john@example.com", - "role": "admin" - } -} -``` - ---- - -### 6. Logout - -Invalidate current session. - -**Endpoint:** `POST /auth/logout` - -**Headers:** -``` -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -``` - -**Request Body:** -```json -{ - "deviceInfo": { - "deviceType": "desktop", - "deviceOS": "windows", - "context": "browser", - "project": "dexar" - } -} -``` - -**Response:** `200 OK` -```json -{ - "success": true, - "message": "Logged out successfully" -} -``` - ---- - -## ๐Ÿ’พ Database Schema - -### Sessions Table -```sql -CREATE TABLE auth_sessions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - session_id VARCHAR(255) UNIQUE NOT NULL, - project VARCHAR(50) NOT NULL, - device_type VARCHAR(20), - device_os VARCHAR(20), - context VARCHAR(20), - user_agent TEXT, - screen_resolution VARCHAR(20), - browser_name VARCHAR(50), - browser_version VARCHAR(20), - authenticated BOOLEAN DEFAULT FALSE, - user_id UUID, - ip_address VARCHAR(45), - created_at TIMESTAMP DEFAULT NOW(), - expires_at TIMESTAMP NOT NULL, - authenticated_at TIMESTAMP, - INDEX idx_session_id (session_id), - INDEX idx_expires_at (expires_at), - INDEX idx_user_id (user_id) -); -``` - -### Auth Logs Table -```sql -CREATE TABLE auth_logs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID, - project VARCHAR(50) NOT NULL, - action VARCHAR(50) NOT NULL, -- login, logout, qr_scan, validate, qr_generate - device_type VARCHAR(20), - device_os VARCHAR(20), - context VARCHAR(20), - user_agent TEXT, - ip_address VARCHAR(45), - success BOOLEAN, - error_message TEXT, - created_at TIMESTAMP DEFAULT NOW(), - INDEX idx_user_id (user_id), - INDEX idx_created_at (created_at), - INDEX idx_project (project) -); -``` - ---- - -## ๐Ÿ”’ Security Implementation - -### Password Hashing -```javascript -const bcrypt = require('bcrypt'); -const saltRounds = 12; - -// Hash password -const hashedPassword = await bcrypt.hash(password, saltRounds); - -// Verify password -const isValid = await bcrypt.compare(password, hashedPassword); -``` - -### JWT Token Generation -```javascript -const jwt = require('jsonwebtoken'); - -const token = jwt.sign( - { - userId: user.id, - project: deviceInfo.project, - deviceType: deviceInfo.deviceType - }, - process.env.JWT_SECRET, - { expiresIn: '24h' } -); -``` - -### QR Code Generation -```javascript -const QRCode = require('qrcode'); - -const qrData = { - sessionId: sessionId, - apiUrl: process.env.API_URL -}; - -const qrCodeImage = await QRCode.toDataURL(JSON.stringify(qrData)); -``` - ---- - -## ๐Ÿš€ Implementation Flow - -### QR Authentication Flow - -1. **Desktop Browser Requests QR Code** - - POST `/auth/qr/generate` with desktop device info - - Backend creates session in database - - Returns QR code image - -2. **Frontend Starts Polling** - - Every 2 seconds: GET `/auth/qr/status/:sessionId` - - Backend returns `{ authenticated: false }` - -3. **Mobile App Scans QR** - - Parse QR code to get sessionId - - POST `/auth/qr/scan` with mobile device info - - Backend authenticates user - - Backend updates session as authenticated - - Returns JWT token - -4. **Desktop Polling Detects Auth** - - Next poll returns `{ authenticated: true, token: "..." }` - - Frontend stores token - - Redirects to application - ---- - -## ๐Ÿ“Š Device Info Tracking - -### When to Send Device Info - -**Send device info on:** -- โœ… QR code generation (desktop) -- โœ… QR code scanning (mobile) -- โœ… Traditional login -- โœ… Session validation -- โœ… Logout - -**Device Info Rules:** -- All fields except `project` are optional -- Send `null` for unknown values -- Never fail request if device info is incomplete -- Log all device info for analytics - ---- - -## โš ๏ธ Error Handling - -### Error Response Format -```json -{ - "error": true, - "message": "Invalid credentials", - "code": "INVALID_CREDENTIALS" -} -``` - -### Error Codes -| Code | HTTP Status | Description | -|------|-------------|-------------| -| `INVALID_PROJECT` | 400 | Unknown project identifier | -| `INVALID_SESSION` | 401 | Session not found or expired | -| `INVALID_CREDENTIALS` | 401 | Wrong username/password | -| `INVALID_TOKEN` | 401 | JWT token invalid or expired | -| `SESSION_EXPIRED` | 401 | QR code session expired | -| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests | -| `SERVER_ERROR` | 500 | Internal server error | - ---- - -## ๐Ÿงช Testing - -### Test Data - -**Test User:** -```json -{ - "username": "test_user", - "password": "Test123!", - "email": "test@dx-projects.com", - "role": "user" -} -``` - -### Example cURL Commands - -**Generate QR Code:** -```bash -curl -X POST http://localhost:3000/api/auth/qr/generate \ - -H "Content-Type: application/json" \ - -d '{ - "project": "dexar", - "deviceInfo": { - "deviceType": "desktop", - "deviceOS": "windows", - "context": "browser", - "project": "dexar" - } - }' -``` - -**Check Status:** -```bash -curl http://localhost:3000/api/auth/qr/status/{sessionId} -``` - -**Login:** -```bash -curl -X POST http://localhost:3000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{ - "username": "test_user", - "password": "Test123!", - "deviceInfo": { - "deviceType": "desktop", - "deviceOS": "windows", - "context": "browser", - "project": "dexar" - } - }' -``` - ---- - -## ๐Ÿ“Œ Important Notes - -1. **QR Code Expiration:** 60 seconds (configurable) -2. **JWT Token Expiration:** 24 hours (configurable) -3. **Polling Interval:** 2 seconds -4. **Rate Limiting:** 60 requests/minute per IP -5. **CORS:** Allow all project domains -6. **Session Cleanup:** Delete expired sessions every hour - ---- - -## ๐Ÿ”ง Environment Variables - -```env -# API Configuration -PORT=3000 -API_URL=http://localhost:3000/api -NODE_ENV=development - -# JWT -JWT_SECRET=your-super-secret-jwt-key-change-this -JWT_EXPIRATION=24h - -# Database -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=dx_auth -DB_USER=postgres -DB_PASSWORD=your-password - -# QR Code -QR_EXPIRATION=60 -QR_SIZE=240 - -# CORS -CORS_ORIGINS=http://localhost:4200,http://localhost:4300,http://localhost:4400 - -# Rate Limiting -RATE_LIMIT_WINDOW=60000 -RATE_LIMIT_MAX_REQUESTS=60 -``` - ---- - -## ๐Ÿ“š Additional Resources - -- **Frontend Integration:** See `HOW_TO_USE.md` -- **Project Setup:** See `README.md` -- **FastCheck APIs:** Reference similar implementation in FastCheck project - ---- - -## ๐Ÿ†˜ Support - -For backend implementation questions: -- Email: backend@dx-projects.com -- Slack: #backend-auth -- Documentation: https://docs.dx-projects.com/auth diff --git a/HOW_TO_USE.md b/HOW_TO_USE.md deleted file mode 100644 index 991b002..0000000 --- a/HOW_TO_USE.md +++ /dev/null @@ -1,376 +0,0 @@ -# How to Use Auth Service in Your Projects - -Step-by-step guide for integrating the authentication service into Dexar, Novo Markets, FastCheck, and BackOffice. - ---- - -## ๐Ÿš€ Quick Start - -### 1. Add Environment Configuration - -In each project, add auth service URL to your environment files: - -**`src/environments/environment.ts`** (Development): -```typescript -export const environment = { - production: false, - authServiceUrl: 'http://localhost:4300', - apiUrl: 'http://localhost:3000/api', - projectId: 'dexar' // Change per project: dexar/novo/fastcheck/backoffice -}; -``` - -**`src/environments/environment.production.ts`**: -```typescript -export const environment = { - production: true, - authServiceUrl: 'https://auth.dx-projects.com', - apiUrl: 'https://api.dx-projects.com/api', - projectId: 'dexar' -}; -``` - ---- - -## ๐Ÿ“ Files to Create - -### Step 1: Create Auth Interceptor - -**File:** `src/app/interceptors/auth.interceptor.ts` - -```typescript -import { HttpInterceptorFn } from '@angular/common/http'; -import { inject } from '@angular/core'; -import { catchError, throwError } from 'rxjs'; -import { environment } from '../../environments/environment'; - -export const authInterceptor: HttpInterceptorFn = (req, next) => { - const token = localStorage.getItem('authToken'); - - // Add token to API requests - if (token && req.url.includes(environment.apiUrl)) { - req = req.clone({ - setHeaders: { Authorization: `Bearer ${token}` } - }); - } - - return next(req).pipe( - catchError((error) => { - // Redirect to auth service on 401 - if (error.status === 401) { - localStorage.removeItem('authToken'); - const currentUrl = window.location.href; - window.location.href = `${environment.authServiceUrl}/login?project=${environment.projectId}&redirect=${encodeURIComponent(currentUrl)}`; - } - return throwError(() => error); - }) - ); -}; -``` - -### Step 2: Create Auth Guard - -**File:** `src/app/guards/auth.guard.ts` - -```typescript -import { inject } from '@angular/core'; -import { CanActivateFn } from '@angular/router'; -import { environment } from '../../environments/environment'; - -export const authGuard: CanActivateFn = () => { - const token = localStorage.getItem('authToken'); - - if (token) { - return true; - } - - // Redirect to auth service - const currentUrl = window.location.href; - window.location.href = `${environment.authServiceUrl}/login?project=${environment.projectId}&redirect=${encodeURIComponent(currentUrl)}`; - - return false; -}; -``` - -### Step 3: Update App Config - -**File:** `src/app/app.config.ts` - -```typescript -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; -import { provideRouter } from '@angular/router'; -import { provideHttpClient, withInterceptors } from '@angular/common/http'; -import { routes } from './app.routes'; -import { authInterceptor } from './interceptors/auth.interceptor'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideZoneChangeDetection({ eventCoalescing: true }), - provideRouter(routes), - provideHttpClient(withInterceptors([authInterceptor])) - ] -}; -``` - -### Step 4: Handle Auth Callback in App Component - -**Update:** `src/app/app.ts` or `src/app/app.component.ts` - -```typescript -import { Component, OnInit } from '@angular/core'; -import { Router, RouterOutlet } from '@angular/router'; - -@Component({ - selector: 'app-root', - imports: [RouterOutlet], - template: '', - styleUrls: ['./app.scss'] -}) -export class App implements OnInit { - constructor(private router: Router) {} - - ngOnInit(): void { - // Handle token from auth service redirect - const urlParams = new URLSearchParams(window.location.search); - const token = urlParams.get('token'); - - if (token) { - localStorage.setItem('authToken', token); - // Clean URL - window.history.replaceState({}, document.title, window.location.pathname); - // Navigate to dashboard - this.router.navigate(['/dashboard']); - } - } -} -``` - -### Step 5: Protect Routes - -**Update:** `src/app/app.routes.ts` - -```typescript -import { Routes } from '@angular/router'; -import { authGuard } from './guards/auth.guard'; - -export const routes: Routes = [ - { - path: 'dashboard', - loadComponent: () => import('./pages/dashboard/dashboard.component'), - canActivate: [authGuard] // โœ… Protected route - }, - { - path: 'products', - loadComponent: () => import('./pages/products/products.component'), - canActivate: [authGuard] // โœ… Protected route - }, - // ... more protected routes -]; -``` - ---- - -## ๐ŸŽฏ Project-Specific Integration - -### For Dexar (marketplaces): -```typescript -// src/environments/environment.ts -export const environment = { - production: false, - authServiceUrl: 'http://localhost:4300', - apiUrl: 'http://localhost:3000/api', - projectId: 'dexar' -}; -``` - -### For Novo Markets: -```typescript -// src/environments/environment.novo.ts -export const environment = { - production: false, - authServiceUrl: 'http://localhost:4300', - apiUrl: 'http://localhost:3000/api', - projectId: 'novo', - brandName: 'Novo' -}; -``` - -### For FastCheck: -```typescript -// FastCheck/src/environments/environment.ts -export const environment = { - production: false, - authServiceUrl: 'http://localhost:4300', - apiUrl: 'http://localhost:3000/api', - projectId: 'fastcheck' -}; -``` - -### For BackOffice: -```typescript -// market-backOffice/src/environments/environment.ts -export const environment = { - production: false, - authServiceUrl: 'http://localhost:4300', - apiUrl: 'http://localhost:3000/api', - projectId: 'backoffice' -}; -``` - ---- - -## ๐Ÿงช Testing Integration - -### Test Flow: - -1. **Clear localStorage:** - ```javascript - localStorage.removeItem('authToken'); - ``` - -2. **Navigate to protected route:** - ``` - http://localhost:4200/dashboard - ``` - -3. **Should redirect to:** - ``` - http://localhost:4300/login?project=dexar&redirect=http://localhost:4200/dashboard - ``` - -4. **After authentication, returns to:** - ``` - http://localhost:4200/dashboard?token=eyJ... - ``` - -5. **Token stored, URL cleaned automatically** - ---- - -## ๐Ÿ”ง Optional: Logout Functionality - -### Create Auth Service - -**File:** `src/app/services/auth.service.ts` - -```typescript -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { environment } from '../../environments/environment'; - -@Injectable({ providedIn: 'root' }) -export class AuthService { - private http = inject(HttpClient); - - logout(): void { - const token = localStorage.getItem('authToken'); - - if (token) { - this.http.post(`${environment.apiUrl}/auth/logout`, { - deviceInfo: this.getDeviceInfo() - }).subscribe({ - complete: () => this.clearAuth(), - error: () => this.clearAuth() - }); - } else { - this.clearAuth(); - } - } - - private clearAuth(): void { - localStorage.removeItem('authToken'); - window.location.href = `${environment.authServiceUrl}/login?project=${environment.projectId}`; - } - - private getDeviceInfo() { - return { - deviceType: window.innerWidth < 768 ? 'mobile' : 'desktop', - deviceOS: null, - context: 'browser', - project: environment.projectId - }; - } -} -``` - -### Use in Component: - -```typescript -import { Component, inject } from '@angular/core'; -import { AuthService } from './services/auth.service'; - -@Component({ - selector: 'app-header', - template: `` -}) -export class HeaderComponent { - private authService = inject(AuthService); - - logout(): void { - this.authService.logout(); - } -} -``` - ---- - -## ๐Ÿ“ฆ Summary - -**3 Files to Create:** -1. โœ… `interceptors/auth.interceptor.ts` -2. โœ… `guards/auth.guard.ts` -3. โœ… Update `app.config.ts` - -**1 File to Modify:** -4. โœ… Update `app.ts` (handle token callback) - -**1 Environment Update:** -5. โœ… Add `authServiceUrl` and `projectId` - -**That's it!** Your project now uses centralized authentication! ๐ŸŽ‰ - ---- - -## ๐Ÿš€ Deployment - -1. **Deploy Auth Service:** - ```bash - cd auth-service - npm run build:prod - # Deploy to auth.dx-projects.com - ``` - -2. **Update Production Environments:** - ```typescript - authServiceUrl: 'https://auth.dx-projects.com' - ``` - -3. **Configure CORS on Backend:** - - Allow `http://localhost:4300` (dev) - - Allow `https://auth.dx-projects.com` (prod) - - Allow all project domains - ---- - -## โ“ Troubleshooting - -### Infinite redirect loop? -- Ensure auth guard doesn't protect callback routes -- Check that token is being stored correctly - -### Token not saved? -- Check browser console for errors -- Verify localStorage is available -- Check URL parameters parsing - -### CORS errors? -- Backend must allow auth service domain -- Check CORS configuration - ---- - -## ๐Ÿ“š More Documentation - -- **Backend API:** See `BACKEND_API.md` -- **Project Setup:** See `README.md` -- **Support:** backend@dx-projects.com diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md deleted file mode 100644 index 11e5067..0000000 --- a/PROJECT_SUMMARY.md +++ /dev/null @@ -1,235 +0,0 @@ -# โœ… DX Auth Service - Project Summary - -**Status:** โœ… Complete and Ready for Production - -**Date:** January 26, 2026 - ---- - -## ๐Ÿ“ฆ What's Included - -### ๐ŸŽจ Frontend (Angular 21) -- **QR Code Authentication** - Generate and scan QR codes for login -- **Device Detection** - Automatic detection of device type, OS, and context -- **3 Components:** - - Login page with QR generation - - Success page with countdown redirect - - Error page with troubleshooting -- **3 Services:** - - Auth Service - Authentication logic and state management - - API Service - HTTP calls to backend - - Device Service - Device detection (type, OS, context) -- **Guards:** Route protection for authenticated/guest users -- **Clean UI:** Simple, no background design - -### ๐Ÿ“š Documentation -1. **[BACKEND_API.md](BACKEND_API.md)** - Complete API specs for backend developer - - 6 endpoints with request/response schemas - - Device tracking requirements - - JWT implementation guide - -2. **[HOW_TO_USE.md](HOW_TO_USE.md)** - Integration guide for your projects - - Step-by-step for Dexar, Novo, FastCheck, BackOffice - - Auth guard implementation - - Interceptor setup - - Route protection - -3. **[README.md](README.md)** - Project overview and quick start - ---- - -## ๐Ÿ—๏ธ Project Structure - -``` -auth-service/ -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ app/ -โ”‚ โ”‚ โ”œโ”€โ”€ components/ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ login/ # QR code generation & display -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ error/ # Error page -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ success/ # Success confirmation -โ”‚ โ”‚ โ”œโ”€โ”€ services/ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ auth.service.ts # Authentication logic -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ api.service.ts # Backend API calls -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ device.service.ts # Device detection -โ”‚ โ”‚ โ”œโ”€โ”€ guards/ -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ auth.guard.ts # Route protection -โ”‚ โ”‚ โ”œโ”€โ”€ models/ -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ auth.model.ts # TypeScript types -โ”‚ โ”‚ โ”œโ”€โ”€ app.routes.ts # Routing configuration -โ”‚ โ”‚ โ”œโ”€โ”€ app.config.ts # App configuration -โ”‚ โ”‚ โ””โ”€โ”€ app.ts # Root component -โ”‚ โ”œโ”€โ”€ environments/ -โ”‚ โ”‚ โ”œโ”€โ”€ environment.ts # Development config -โ”‚ โ”‚ โ””โ”€โ”€ environment.production.ts # Production config -โ”‚ โ””โ”€โ”€ styles.scss # Global styles -โ”œโ”€โ”€ BACKEND_API.md # For backend developer -โ”œโ”€โ”€ HOW_TO_USE.md # Integration guide -โ”œโ”€โ”€ README.md # Project overview -โ””โ”€โ”€ package.json # Dependencies - -``` - ---- - -## ๐Ÿ”ง Technologies - -- **Angular:** 21.0.0 (Latest) -- **TypeScript:** 5.9.2 -- **RxJS:** 7.8.0 -- **QRCode.js:** 1.5.4 -- **UAParser.js:** 2.0.8 -- **Port:** 4300 - ---- - -## ๐Ÿ“ก Backend Requirements - -Backend developer must implement **6 endpoints**: - -1. `POST /auth/qr/generate` - Generate QR code -2. `POST /auth/qr/scan` - Mobile scans QR -3. `GET /auth/qr/status/:sessionId` - Check auth status (polling) -4. `POST /auth/login` - Traditional login (optional) -5. `POST /auth/validate` - Validate JWT token -6. `POST /auth/logout` - Logout user - -**See [BACKEND_API.md](BACKEND_API.md) for complete specs** - ---- - -## ๐Ÿ” Device Tracking - -All fields are **nullable** (send `null` if unavailable): - -| Field | Type | Values | -|-------|------|--------| -| `deviceType` | string\|null | mobile, desktop, tablet | -| `deviceOS` | string\|null | android, ios, windows, macos, linux | -| `context` | string\|null | browser, application, telegram | -| `project` | string | **Required**: dexar, novo, fastcheck, backoffice | - -Additional info: `userAgent`, `screenResolution`, `browserName`, `browserVersion` - ---- - -## ๐Ÿš€ How to Use - -### 1๏ธโƒฃ Push to Git -```bash -git add . -git commit -m "Complete auth service" -git push origin main -``` - -### 2๏ธโƒฃ Deploy Frontend -- **Option A:** Vercel/Netlify (recommended) -- **Option B:** Your own server - -### 3๏ธโƒฃ Backend Implementation -- Give [BACKEND_API.md](BACKEND_API.md) to backend developer -- Backend implements 6 endpoints -- Deploy backend API - -### 4๏ธโƒฃ Integration -- Follow [HOW_TO_USE.md](HOW_TO_USE.md) for each project -- Add auth guard, interceptor, route protection -- Update environment variables with production URLs - ---- - -## โœ… Pre-Flight Checklist - -- [x] Angular 21 project created -- [x] All components implemented (login, error, success) -- [x] All services implemented (auth, api, device) -- [x] Guards configured (auth, guest) -- [x] Device detection working (5 tracked fields) -- [x] QR code generation and display -- [x] Polling mechanism (2-second intervals) -- [x] Background removed (clean UI) -- [x] Documentation complete (2 main docs) -- [x] Build successful (no errors) -- [x] Server running on port 4300 -- [x] Unused files removed (test files, duplicate docs) - ---- - -## ๐ŸŽฏ Next Steps - -1. **Backend Developer:** Implement 6 endpoints from [BACKEND_API.md](BACKEND_API.md) -2. **You:** Integrate into your 4 projects using [HOW_TO_USE.md](HOW_TO_USE.md) -3. **Deploy:** Push to git, deploy frontend & backend -4. **Test:** Verify QR authentication flow works end-to-end - ---- - -## ๐Ÿ“‹ Integration Summary - -Each project needs **5 changes**: - -1. **Environment config** - Add auth service URL -2. **Auth guard** - Redirect unauthenticated users -3. **Auth interceptor** - Add JWT token to requests -4. **App config** - Register interceptor -5. **Route protection** - Apply guard to routes -6. **Callback handler** - Save token from URL - -**Time per project:** ~15 minutes - ---- - -## ๐Ÿ”— URLs (After Deployment) - -- Auth Service: `https://auth.dx-projects.com` -- API Backend: `https://api.dx-projects.com/api/auth` -- Dexar: `https://dexar.com` โ†’ uses auth -- Novo: `https://novo-markets.com` โ†’ uses auth -- FastCheck: `https://fastcheck.com` โ†’ uses auth -- BackOffice: `https://backoffice.dx-projects.com` โ†’ uses auth - ---- - -## ๐Ÿ“Š Project Stats - -- **Lines of Code:** ~2,500 -- **Components:** 3 -- **Services:** 3 -- **Models:** 1 (with 10+ interfaces) -- **Guards:** 2 -- **Routes:** 4 -- **Dependencies:** 12 -- **Documentation:** 529 lines (BACKEND_API.md) - ---- - -## โœจ Features - -โœ… QR code authentication -โœ… Device detection (type, OS, context) -โœ… Multi-project support (4 projects) -โœ… JWT token management -โœ… Real-time polling (2s intervals) -โœ… Error handling with troubleshooting -โœ… Success confirmation with redirect -โœ… Clean, simple UI -โœ… TypeScript type safety -โœ… Route guards -โœ… HTTP interceptors -โœ… Comprehensive documentation - ---- - -## ๐ŸŽ‰ Project Complete! - -Everything is ready for: -- โœ… Git push -- โœ… Backend implementation -- โœ… Frontend deployment -- โœ… Integration into 4 projects - -**All required documentation provided.** - ---- - -*Generated: January 26, 2026* diff --git a/README.md b/README.md index dece523..0738822 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,68 @@ -# AuthService +# Telegram UserAuth UI -This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.4. +Reusable Angular-hosted UI for the Telegram login dialog. -## Development server +The app now contains a single standalone page based on the extracted dialog design. It preserves the same visual states and the same state-switcher behavior from the provided HTML: -To start a local development server, run: +- `ready` +- `loading` +- `checking` +- `expired` +- `error` + +## Run ```bash -ng serve +npm start ``` -Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. +The dev server runs on port `4300`. -## Code scaffolding - -Angular CLI includes powerful code scaffolding tools. To generate a new component, run: +## Build ```bash -ng generate component component-name +npm run build ``` -For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: +## Backend contract -```bash -ng generate --help +This UI is intended to work against a reusable Telegram auth backend with these endpoints: + +- `GET /userauth/session` +- `POST /userauth/qr/create` +- `GET /userauth/qr/poll?token=...` +- `POST /userauth/qr/confirm` +- `GET /userauth/telegram/callback` +- `POST /userauth/logout` +- `POST /usersession/{sessionId}` + +Expected authenticated session payload: + +```json +{ + "sessionId": "550e8400-e29b-41d4-a716-446655440000", + "telegramUserId": 123456789, + "username": "ivan_petrov", + "displayName": "Ivan Petrov", + "active": true, + "expiresAt": "2026-05-21T14:30:00Z" +} ``` -## Building +Runtime expectations preserved by the UI: -To build the project run: +- QR polling every 3 seconds +- QR expiry after 100 checks on the frontend +- direct Telegram login button support +- fallback session re-check if QR creation fails -```bash -ng build -``` +Cookie requirements expected by consumers: -This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. +- name: `userauth_session` +- path: `/` +- `HttpOnly: true` +- `Secure: true` +- `SameSite: None` +- `MaxAge: 86400` -## Running unit tests - -To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: - -```bash -ng test -``` - -## Running end-to-end tests - -For end-to-end (e2e) testing, run: - -```bash -ng e2e -``` - -Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. - -## Additional Resources - -For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. +Credentialed CORS is required on the backend. diff --git a/angular.json b/angular.json index a6cf11d..95906f4 100644 --- a/angular.json +++ b/angular.json @@ -43,8 +43,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kB", - "maximumError": "8kB" + "maximumWarning": "8kB", + "maximumError": "12kB" } ], "outputHashing": "all" diff --git a/package-lock.json b/package-lock.json index dd05c30..3bbd6f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,15 +11,9 @@ "@angular/common": "^21.0.0", "@angular/compiler": "^21.0.0", "@angular/core": "^21.0.0", - "@angular/forms": "^21.0.0", "@angular/platform-browser": "^21.0.0", - "@angular/router": "^21.0.0", - "@types/qrcode": "^1.5.6", - "@types/ua-parser-js": "^0.7.39", - "qrcode": "^1.5.4", "rxjs": "~7.8.0", - "tslib": "^2.3.0", - "ua-parser-js": "^2.0.8" + "tslib": "^2.3.0" }, "devDependencies": { "@angular/build": "^21.0.4", @@ -548,25 +542,6 @@ } } }, - "node_modules/@angular/forms": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.1.1.tgz", - "integrity": "sha512-NBbJOynLOeMsPo03+3dfdxE0P7SB7SXRqoFJ7WP2sOgOIxODna/huo2blmRlnZAVPTn1iQEB9Q+UeyP5c4/1+w==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/common": "21.1.1", - "@angular/core": "21.1.1", - "@angular/platform-browser": "21.1.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/@angular/platform-browser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.1.1.tgz", @@ -589,24 +564,6 @@ } } }, - "node_modules/@angular/router": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.1.1.tgz", - "integrity": "sha512-3ypbtH3KfzuVgebdEET9+bRwn1VzP//KI0tIqleCGi4rblP3WQ/HwIGa5Qhdcxmw/kbmABKLRXX2kRUvidKs/Q==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/common": "21.1.1", - "@angular/core": "21.1.1", - "@angular/platform-browser": "21.1.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/@asamuzakjp/css-color": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", @@ -3861,6 +3818,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, "license": "MIT" }, "node_modules/@tufjs/canonical-json": { @@ -3927,26 +3885,14 @@ "version": "25.0.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, - "node_modules/@types/qrcode": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", - "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ua-parser-js": { - "version": "0.7.39", - "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz", - "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==", - "license": "MIT" - }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", @@ -4412,15 +4358,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001766", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", @@ -4609,6 +4546,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4621,6 +4559,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/colorette": { @@ -4826,15 +4765,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -4852,26 +4782,6 @@ "node": ">= 0.8" } }, - "node_modules/detect-europe-js": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", - "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - } - ], - "license": "MIT" - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -4883,12 +4793,6 @@ "node": ">=8" } }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -5352,19 +5256,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5437,6 +5328,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -5869,26 +5761,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-standalone-pwa": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", - "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - } - ], - "license": "MIT" - }, "node_modules/is-unicode-supported": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", @@ -6142,18 +6014,6 @@ "@lmdb/lmdb-win32-x64": "3.4.4" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -6927,33 +6787,6 @@ "license": "MIT", "optional": true }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", @@ -6967,15 +6800,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/pacote": { "version": "21.0.4", "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.4.tgz", @@ -7085,15 +6909,6 @@ "node": ">= 0.8" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7199,15 +7014,6 @@ "node": ">=16.20.0" } }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -7292,125 +7098,6 @@ "node": ">=6" } }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/qrcode/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/qrcode/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/qrcode/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/qrcode/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -7474,15 +7161,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -7493,12 +7171,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -7788,12 +7460,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -8353,57 +8019,6 @@ "node": ">=14.17" } }, - "node_modules/ua-is-frozen": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", - "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - } - ], - "license": "MIT" - }, - "node_modules/ua-parser-js": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.8.tgz", - "integrity": "sha512-BdnBM5waFormdrOFBU+cA90R689V0tWUWlIG2i30UXxElHjuCu5+dOV2Etw3547jcQ/yaLtPm9wrqIuOY2bSJg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "AGPL-3.0-or-later", - "dependencies": { - "detect-europe-js": "^0.1.2", - "is-standalone-pwa": "^0.1.1", - "ua-is-frozen": "^0.1.2" - }, - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/undici": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", @@ -8418,7 +8033,10 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/unique-filename": { "version": "5.0.0", @@ -8756,12 +8374,6 @@ "node": ">= 8" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -8783,6 +8395,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -8797,6 +8410,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8806,6 +8420,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -8821,12 +8436,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8836,6 +8453,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -8850,6 +8468,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" diff --git a/package.json b/package.json index e5d16a5..aaebc18 100644 --- a/package.json +++ b/package.json @@ -27,15 +27,9 @@ "@angular/common": "^21.0.0", "@angular/compiler": "^21.0.0", "@angular/core": "^21.0.0", - "@angular/forms": "^21.0.0", "@angular/platform-browser": "^21.0.0", - "@angular/router": "^21.0.0", - "@types/qrcode": "^1.5.6", - "@types/ua-parser-js": "^0.7.39", - "qrcode": "^1.5.4", "rxjs": "~7.8.0", - "tslib": "^2.3.0", - "ua-parser-js": "^2.0.8" + "tslib": "^2.3.0" }, "devDependencies": { "@angular/build": "^21.0.4", diff --git a/src/app/app.config.ts b/src/app/app.config.ts index a744b02..cb999ab 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,12 +1,5 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; -import { provideRouter } from '@angular/router'; -import { provideHttpClient } from '@angular/common/http'; -import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { - providers: [ - provideBrowserGlobalErrorListeners(), - provideRouter(routes), - provideHttpClient() - ] + providers: [provideBrowserGlobalErrorListeners()] }; diff --git a/src/app/app.html b/src/app/app.html index e0118a1..df0929a 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,342 +1,120 @@ - - - - - - - - +
+
+

Telegram Login Dialog

+

+ Standalone extraction of the current login popup: same layout, same visual states, + same QR flow, but with reusable neutral endpoint names for moving into a separate repo. +

- - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. ๐ŸŽ‰

+
+ @for (dialogState of states; track dialogState) { + + }
- -
+ +
+ - +
- - - - - - - - - - - diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts deleted file mode 100644 index 16efca5..0000000 --- a/src/app/app.routes.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Routes } from '@angular/router'; -import { authGuard, guestGuard } from './guards/auth.guard'; -import { LoginComponent } from './components/login/login'; -import { ErrorComponent } from './components/error/error'; -import { SuccessComponent } from './components/success/success'; - -export const routes: Routes = [ - { - path: '', - redirectTo: '/login', - pathMatch: 'full' - }, - { - path: 'login', - component: LoginComponent, - canActivate: [guestGuard] - }, - { - path: 'error', - component: ErrorComponent - }, - { - path: 'success', - component: SuccessComponent, - canActivate: [authGuard] - }, - { - path: '**', - redirectTo: '/error' - } -]; diff --git a/src/app/app.scss b/src/app/app.scss index e69de29..8607116 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -0,0 +1,415 @@ +:host { + --bg-page: linear-gradient(135deg, #f4f7fb 0%, #e8eef4 100%); + --bg-card: #ffffff; + --bg-hover: #f0f0f0; + --text-primary: #1a1a1a; + --text-secondary: #666666; + --accent-color: #497671; + --accent-light: rgba(73, 118, 113, 0.1); + --telegram: #2aabee; + --telegram-hover: #229ed9; + --border: #e8e8e8; + --shadow: 0 20px 60px rgba(0, 0, 0, 0.15); + + display: block; + min-height: 100vh; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: var(--text-primary); + background: var(--bg-page); +} + +* { + box-sizing: border-box; +} + +.page { + min-height: 100vh; + display: grid; + grid-template-columns: minmax(320px, 448px) minmax(320px, 560px); + gap: 32px; + padding: 40px 32px; + align-items: center; + justify-content: center; +} + +.panel { + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 28px; + padding: 24px; + box-shadow: 0 18px 50px rgba(38, 52, 73, 0.12); + backdrop-filter: blur(14px); +} + +.info h1 { + margin: 0 0 12px; + font-size: 32px; + line-height: 1.1; +} + +.info p { + margin: 0 0 18px; + color: var(--text-secondary); + line-height: 1.6; +} + +.state-switcher { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 20px 0 24px; +} + +.state-switcher button { + border: 1px solid #cfd8e3; + border-radius: 999px; + background: #fff; + color: var(--text-primary); + padding: 10px 14px; + font-size: 14px; + cursor: pointer; + transition: 0.2s ease; +} + +.state-switcher button.active { + border-color: var(--accent-color); + background: var(--accent-light); + color: var(--accent-color); +} + +.api-grid { + display: grid; + gap: 12px; + margin-top: 20px; +} + +.api-card { + background: #fff; + border: 1px solid #eef2f7; + border-radius: 16px; + padding: 14px 16px; +} + +.api-card strong { + display: block; + margin-bottom: 6px; + font-size: 14px; +} + +.api-card code { + display: inline-block; + padding: 2px 8px; + border-radius: 999px; + background: #f3f7fb; + color: #21425f; + font-size: 13px; +} + +.api-card p { + margin: 8px 0 0; + font-size: 13px; +} + +.login-overlay { + position: relative; + min-height: 700px; + border-radius: 28px; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.2s ease; + padding: 16px; +} + +.login-dialog { + position: relative; + background: var(--bg-card); + border-radius: 20px; + padding: 32px 28px; + max-width: 400px; + width: 100%; + text-align: center; + box-shadow: var(--shadow); + animation: scaleIn 0.25s ease; +} + +.close-btn { + position: absolute; + top: 12px; + right: 12px; + width: 32px; + height: 32px; + border: none; + border-radius: 50%; + background: var(--bg-hover); + color: var(--text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.close-btn:hover { + background: #e0e0e0; + color: #333; +} + +.login-icon { + margin: 0 auto 16px; + width: 72px; + height: 72px; + border-radius: 50%; + background: var(--accent-light); + color: var(--accent-color); + display: flex; + align-items: center; + justify-content: center; +} + +.login-dialog h2 { + margin: 0 0 8px; + font-size: 20px; + font-weight: 700; + color: var(--text-primary); +} + +.login-desc { + margin: 0 0 24px; + font-size: 14px; + color: var(--text-secondary); + line-height: 1.5; +} + +.telegram-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + padding: 14px 24px; + border: none; + border-radius: 12px; + background: var(--telegram); + color: #fff; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.telegram-btn:hover { + background: var(--telegram-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3); +} + +.telegram-btn:active { + transform: translateY(0); +} + +.tg-icon { + flex-shrink: 0; +} + +.qr-section { + margin-top: 20px; +} + +.qr-hint { + margin: 0 0 12px; + font-size: 13px; + color: #999; +} + +.qr-container { + display: inline-flex; + padding: 12px; + background: #fff; + border-radius: 12px; + border: 1px solid var(--border); +} + +.qr-container img { + display: block; + border-radius: 4px; +} + +.qr-loading { + align-items: center; + justify-content: center; + width: 204px; + height: 204px; +} + +.qr-loading .spinner, +.login-status .spinner { + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.qr-loading .spinner { + width: 32px; + height: 32px; + border: 3px solid #e0e0e0; + border-top-color: var(--accent-color); +} + +.qr-expired { + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + width: 204px; + height: 204px; + cursor: pointer; + color: #999; + transition: color 0.2s ease; +} + +.qr-expired:hover { + color: var(--accent-color); +} + +.qr-expired span { + font-size: 13px; +} + +.login-note { + margin: 16px 0 0; + font-size: 12px; + color: #999; + line-height: 1.4; +} + +.login-status { + display: none; + align-items: center; + justify-content: center; + gap: 10px; + padding: 16px; + color: var(--text-secondary); + font-size: 14px; +} + +.login-status .spinner { + width: 20px; + height: 20px; + border: 2px solid #e0e0e0; + border-top-color: var(--accent-color); +} + +.dialog-content[data-state='checking'] .login-status { + display: flex; +} + +.dialog-content[data-state='checking'] .action-block, +.dialog-content[data-state='loading'] .qr-ready, +.dialog-content[data-state='loading'] .qr-expired, +.dialog-content[data-state='expired'] .qr-ready, +.dialog-content[data-state='expired'] .qr-loading, +.dialog-content[data-state='error'] .qr-loading, +.dialog-content[data-state='error'] .qr-expired, +.dialog-content[data-state='checking'] .qr-section, +.dialog-content[data-state='checking'] .login-note { + display: none; +} + +.dialog-content[data-state='ready'] .qr-loading, +.dialog-content[data-state='ready'] .qr-expired, +.dialog-content[data-state='ready'] .qr-error, +.dialog-content[data-state='loading'] .qr-ready, +.dialog-content[data-state='loading'] .qr-expired, +.dialog-content[data-state='loading'] .qr-error, +.dialog-content[data-state='expired'] .qr-loading, +.dialog-content[data-state='expired'] .qr-ready, +.dialog-content[data-state='expired'] .qr-error, +.dialog-content[data-state='error'] .qr-loading, +.dialog-content[data-state='error'] .qr-expired { + display: none; +} + +.dialog-content[data-state='error'] .qr-ready { + display: inline-flex; +} + +.metadata { + margin-top: 22px; + padding-top: 18px; + border-top: 1px solid #e9edf2; + font-size: 13px; + color: var(--text-secondary); +} + +.metadata ul { + margin: 10px 0 0; + padding-left: 18px; +} + +.metadata li + li { + margin-top: 6px; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 980px) { + .page { + grid-template-columns: 1fr; + padding: 24px 16px 32px; + } + + .login-overlay { + min-height: 560px; + } +} + +@media (max-width: 480px) { + .panel { + border-radius: 22px; + padding: 18px; + } + + .login-dialog { + padding: 24px 20px; + border-radius: 16px; + } + + .qr-container img { + width: 140px; + height: 140px; + } + + .qr-loading, + .qr-expired { + width: 164px; + height: 164px; + } +} diff --git a/src/app/app.ts b/src/app/app.ts index 8a361b7..6498997 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,12 +1,17 @@ import { Component, signal } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; + +type DialogState = 'ready' | 'loading' | 'checking' | 'expired' | 'error'; @Component({ selector: 'app-root', - imports: [RouterOutlet], - template: '', + templateUrl: './app.html', styleUrl: './app.scss' }) export class App { - protected readonly title = signal('auth-service'); + protected readonly state = signal('ready'); + protected readonly states: DialogState[] = ['ready', 'loading', 'checking', 'expired', 'error']; + + protected setState(state: DialogState): void { + this.state.set(state); + } } diff --git a/src/app/components/error/error.html b/src/app/components/error/error.html deleted file mode 100644 index 4afef6f..0000000 --- a/src/app/components/error/error.html +++ /dev/null @@ -1,50 +0,0 @@ -
-
-
-
โš ๏ธ
-
- -

Something Went Wrong

- -

{{ errorMessage }}

- -
- Error Code: - {{ errorCode }} -
- -
- - -
- - @if (errorDetails) { -
- - - @if (showDetails) { -
-
{{ errorDetails | json }}
-
- } -
- } - -
-

Need help?

- - Contact Support - -
-
- - -
diff --git a/src/app/components/error/error.scss b/src/app/components/error/error.scss deleted file mode 100644 index 1a89584..0000000 --- a/src/app/components/error/error.scss +++ /dev/null @@ -1,215 +0,0 @@ -.error-container { - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); - padding: 20px; -} - -.error-card { - background: white; - border-radius: 16px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); - max-width: 600px; - width: 100%; - padding: 48px 40px; - text-align: center; - animation: slideIn 0.4s ease-out; -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.error-icon-container { - margin-bottom: 24px; - - .error-icon { - font-size: 80px; - animation: shake 0.5s ease-in-out; - } -} - -@keyframes shake { - 0%, 100% { transform: translateX(0); } - 25% { transform: translateX(-10px); } - 75% { transform: translateX(10px); } -} - -.error-title { - font-size: 32px; - font-weight: 700; - color: #2d3748; - margin-bottom: 16px; -} - -.error-message { - font-size: 18px; - color: #4a5568; - margin-bottom: 24px; - line-height: 1.6; -} - -.error-code { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 20px; - background: #fff5f5; - border: 1px solid #feb2b2; - border-radius: 8px; - margin-bottom: 32px; - - .label { - font-size: 14px; - color: #742a2a; - font-weight: 600; - } - - .code { - font-size: 14px; - color: #e53e3e; - font-family: 'Courier New', monospace; - font-weight: 600; - } -} - -.error-actions { - display: flex; - gap: 12px; - justify-content: center; - margin-bottom: 32px; - - .btn { - padding: 14px 32px; - border: none; - border-radius: 8px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - - &.btn-primary { - background: #667eea; - color: white; - - &:hover { - background: #5568d3; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); - } - } - - &.btn-secondary { - background: #e2e8f0; - color: #4a5568; - - &:hover { - background: #cbd5e0; - transform: translateY(-2px); - } - } - } -} - -.error-details-section { - margin-bottom: 32px; - text-align: left; - - .details-toggle { - width: 100%; - padding: 12px 16px; - background: #f7fafc; - border: 1px solid #e2e8f0; - border-radius: 8px; - font-size: 14px; - color: #4a5568; - cursor: pointer; - text-align: left; - transition: background 0.2s; - - &:hover { - background: #edf2f7; - } - } - - .error-details { - margin-top: 12px; - padding: 16px; - background: #1a202c; - border-radius: 8px; - overflow-x: auto; - - pre { - margin: 0; - color: #68d391; - font-size: 12px; - font-family: 'Courier New', monospace; - white-space: pre-wrap; - word-wrap: break-word; - } - } -} - -.help-section { - padding-top: 32px; - border-top: 1px solid #e2e8f0; - - .help-text { - font-size: 14px; - color: #718096; - margin-bottom: 8px; - } - - .help-link { - color: #667eea; - text-decoration: none; - font-weight: 600; - font-size: 16px; - - &:hover { - text-decoration: underline; - } - } -} - -.error-footer { - margin-top: 32px; - text-align: center; - - p { - font-size: 13px; - color: rgba(255, 255, 255, 0.9); - } -} - -@media (max-width: 640px) { - .error-card { - padding: 32px 24px; - } - - .error-title { - font-size: 24px; - } - - .error-message { - font-size: 16px; - } - - .error-actions { - flex-direction: column; - - .btn { - width: 100%; - } - } -} diff --git a/src/app/components/error/error.ts b/src/app/components/error/error.ts deleted file mode 100644 index 2b5fe53..0000000 --- a/src/app/components/error/error.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component, OnInit, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; - -@Component({ - selector: 'app-error', - imports: [CommonModule], - templateUrl: './error.html', - styleUrl: './error.scss' -}) -export class ErrorComponent implements OnInit { - private route = inject(ActivatedRoute); - private router = inject(Router); - - errorMessage: string = 'An unexpected error occurred'; - errorCode: string = 'UNKNOWN_ERROR'; - showDetails: boolean = false; - errorDetails: any = null; - - ngOnInit(): void { - this.route.queryParams.subscribe(params => { - this.errorMessage = params['message'] || this.errorMessage; - this.errorCode = params['code'] || this.errorCode; - - if (params['details']) { - try { - this.errorDetails = JSON.parse(params['details']); - } catch (e) { - this.errorDetails = params['details']; - } - } - }); - } - - goBack(): void { - this.router.navigate(['/login']); - } - - toggleDetails(): void { - this.showDetails = !this.showDetails; - } - - retry(): void { - window.location.reload(); - } -} diff --git a/src/app/components/login/login.html b/src/app/components/login/login.html deleted file mode 100644 index 006dfb2..0000000 --- a/src/app/components/login/login.html +++ /dev/null @@ -1,66 +0,0 @@ - diff --git a/src/app/components/login/login.scss b/src/app/components/login/login.scss deleted file mode 100644 index ebd6699..0000000 --- a/src/app/components/login/login.scss +++ /dev/null @@ -1,214 +0,0 @@ -.login-container { - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 20px; -} - -.login-card { - max-width: 500px; - width: 100%; - padding: 40px; -} - -.login-header { - text-align: center; - margin-bottom: 30px; - - h1 { - font-size: 28px; - font-weight: 700; - color: #2d3748; - margin-bottom: 10px; - } - - .project-name { - font-size: 18px; - font-weight: 600; - color: #667eea; - margin-bottom: 8px; - } - - .device-info { - font-size: 13px; - color: #718096; - } -} - -.qr-section { - text-align: center; -} - -.qr-code-container { - background: #f7fafc; - border: 2px solid #e2e8f0; - border-radius: 12px; - padding: 20px; - margin-bottom: 24px; - display: inline-block; - - .qr-code { - width: 240px; - height: 240px; - display: block; - } -} - -.instructions { - margin-bottom: 24px; - text-align: left; - - h3 { - font-size: 18px; - font-weight: 600; - color: #2d3748; - margin-bottom: 12px; - } - - ol { - padding-left: 20px; - color: #4a5568; - line-height: 1.8; - - li { - margin-bottom: 8px; - } - } -} - -.qr-info { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 20px; - padding: 12px 16px; - background: #f7fafc; - border-radius: 8px; - - .expires { - font-size: 14px; - color: #718096; - margin: 0; - } - - .refresh-btn { - background: #667eea; - color: white; - border: none; - padding: 8px 16px; - border-radius: 6px; - font-size: 14px; - cursor: pointer; - transition: background 0.2s; - - &:hover { - background: #5568d3; - } - } -} - -.polling-indicator { - padding: 16px; - background: #ebf4ff; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - gap: 12px; - - p { - margin: 0; - color: #2c5282; - font-size: 14px; - } -} - -.loading-section { - text-align: center; - padding: 40px 0; - - p { - margin-top: 16px; - color: #718096; - font-size: 16px; - } -} - -.error-section { - text-align: center; - padding: 40px 0; - - .error-icon { - font-size: 48px; - margin-bottom: 16px; - } - - .error-message { - color: #e53e3e; - margin-bottom: 20px; - font-size: 16px; - } - - .retry-btn { - background: #667eea; - color: white; - border: none; - padding: 12px 32px; - border-radius: 8px; - font-size: 16px; - cursor: pointer; - transition: background 0.2s; - - &:hover { - background: #5568d3; - } - } -} - -.spinner { - width: 20px; - height: 20px; - border: 3px solid #e2e8f0; - border-top-color: #667eea; - border-radius: 50%; - animation: spin 0.8s linear infinite; - - &.large { - width: 48px; - height: 48px; - border-width: 4px; - } -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -.login-footer { - margin-top: 32px; - text-align: center; - - p { - font-size: 13px; - color: rgba(255, 255, 255, 0.8); - } -} - -@media (max-width: 640px) { - .login-card { - padding: 24px; - } - - .login-header h1 { - font-size: 24px; - } - - .qr-code-container .qr-code { - width: 200px; - height: 200px; - } -} diff --git a/src/app/components/login/login.ts b/src/app/components/login/login.ts deleted file mode 100644 index c49f479..0000000 --- a/src/app/components/login/login.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Component, OnInit, OnDestroy, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Subject, takeUntil } from 'rxjs'; -import { AuthService } from '../../services/auth.service'; -import { DeviceService } from '../../services/device.service'; -import { QRGenerationResponse, ProjectType } from '../../models/auth.model'; -import { environment } from '../../../environments/environment'; - -@Component({ - selector: 'app-login', - imports: [CommonModule], - templateUrl: './login.html', - styleUrl: './login.scss' -}) -export class LoginComponent implements OnInit, OnDestroy { - private authService = inject(AuthService); - private deviceService = inject(DeviceService); - private route = inject(ActivatedRoute); - private router = inject(Router); - private destroy$ = new Subject(); - - qrCode: string = ''; - sessionId: string = ''; - project: ProjectType = 'unknown'; - projectName: string = ''; - isLoading: boolean = false; - error: string = ''; - expiresIn: number = 0; - isPolling: boolean = false; - deviceInfo: string = ''; - - ngOnInit(): void { - // Get project from query params - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe(params => { - this.project = params['project'] || 'unknown'; - this.projectName = this.getProjectName(this.project); - - // Get redirect URL if provided - const redirect = params['redirect']; - if (redirect) { - localStorage.setItem('redirectUrl', redirect); - } - - // Get context if provided - const context = params['context']; - if (context) { - this.deviceService.setContext(context); - } - - this.deviceService.setProject(this.project); - this.deviceInfo = this.deviceService.getDeviceDescription(); - - // Generate QR code - this.generateQRCode(); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - /** - * Generate QR code for authentication - */ - generateQRCode(): void { - this.isLoading = true; - this.error = ''; - - this.authService.generateQRCode(this.project) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (response: QRGenerationResponse) => { - this.qrCode = response.qrCode; - this.sessionId = response.sessionId; - this.expiresIn = response.expiresIn; - this.isLoading = false; - - // Start polling for authentication status - this.startPolling(); - - // Auto-refresh QR code before expiration - setTimeout(() => { - if (!this.authService.isAuthenticated()) { - this.generateQRCode(); - } - }, (response.expiresIn - 5) * 1000); - }, - error: (error) => { - this.error = error.message || 'Failed to generate QR code'; - this.isLoading = false; - this.router.navigate(['/error'], { - queryParams: { - message: this.error, - code: 'QR_GENERATION_FAILED' - } - }); - } - }); - } - - /** - * Start polling for authentication status - */ - private startPolling(): void { - if (this.isPolling) return; - - this.isPolling = true; - - this.authService.startQRAuthPolling(this.sessionId) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (status) => { - if (status.authenticated) { - console.log('Authentication successful!'); - // Redirect will be handled by AuthService - const redirectUrl = localStorage.getItem('redirectUrl'); - if (redirectUrl) { - this.authService.redirectAfterAuth(redirectUrl); - } else { - this.router.navigate(['/success']); - } - } - }, - error: (error) => { - this.isPolling = false; - console.error('Polling error:', error); - }, - complete: () => { - this.isPolling = false; - } - }); - } - - /** - * Get project display name - */ - private getProjectName(project: ProjectType): string { - const projects = environment.projects as any; - return projects[project] || project; - } - - /** - * Refresh QR code manually - */ - refreshQR(): void { - this.generateQRCode(); - } -} diff --git a/src/app/components/success/success.html b/src/app/components/success/success.html deleted file mode 100644 index b41e56e..0000000 --- a/src/app/components/success/success.html +++ /dev/null @@ -1,50 +0,0 @@ -
-
-
-
โœ“
-
- -

Authentication Successful!

- - @if (user) { -

- Welcome back, {{ user.username }} -

- } - - @if (!user) { -

- You have been successfully authenticated. -

- } - - @if (countdown > 0) { -
-
- {{ countdown }} -
-

Redirecting in {{ countdown }} second{{ countdown !== 1 ? 's' : '' }}...

-
- } - - @if (countdown < 0) { -
-

- You can now close this window and return to your application. -

-
- } - - @if (countdown > 0) { -
- -
- } -
- - -
diff --git a/src/app/components/success/success.scss b/src/app/components/success/success.scss deleted file mode 100644 index 8ba199f..0000000 --- a/src/app/components/success/success.scss +++ /dev/null @@ -1,181 +0,0 @@ -.success-container { - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%); - padding: 20px; -} - -.success-card { - background: white; - border-radius: 16px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); - max-width: 500px; - width: 100%; - padding: 48px 40px; - text-align: center; - animation: slideIn 0.4s ease-out; -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.success-icon-container { - margin-bottom: 24px; - - .success-icon { - width: 80px; - height: 80px; - margin: 0 auto; - background: #48bb78; - color: white; - font-size: 48px; - font-weight: bold; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - animation: scaleIn 0.5s ease-out; - } -} - -@keyframes scaleIn { - 0% { - transform: scale(0); - opacity: 0; - } - 50% { - transform: scale(1.1); - } - 100% { - transform: scale(1); - opacity: 1; - } -} - -.success-title { - font-size: 28px; - font-weight: 700; - color: #2d3748; - margin-bottom: 16px; -} - -.success-message { - font-size: 18px; - color: #4a5568; - margin-bottom: 32px; - line-height: 1.6; - - strong { - color: #48bb78; - } -} - -.redirect-info { - margin-bottom: 32px; - - .countdown-circle { - width: 80px; - height: 80px; - margin: 0 auto 16px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - position: relative; - animation: pulse 1s ease-in-out infinite; - - .countdown-number { - font-size: 36px; - font-weight: 700; - color: white; - } - } - - p { - font-size: 16px; - color: #718096; - } -} - -@keyframes pulse { - 0%, 100% { - transform: scale(1); - opacity: 1; - } - 50% { - transform: scale(1.05); - opacity: 0.9; - } -} - -.no-redirect { - margin-bottom: 32px; - - .info-text { - font-size: 16px; - color: #4a5568; - padding: 16px; - background: #f7fafc; - border-radius: 8px; - border-left: 4px solid #48bb78; - } -} - -.success-actions { - .btn { - padding: 14px 32px; - border: none; - border-radius: 8px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - - &.btn-primary { - background: #667eea; - color: white; - - &:hover { - background: #5568d3; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); - } - } - } -} - -.success-footer { - margin-top: 32px; - text-align: center; - - p { - font-size: 13px; - color: rgba(255, 255, 255, 0.9); - } -} - -@media (max-width: 640px) { - .success-card { - padding: 32px 24px; - } - - .success-title { - font-size: 24px; - } - - .success-message { - font-size: 16px; - } -} diff --git a/src/app/components/success/success.ts b/src/app/components/success/success.ts deleted file mode 100644 index c77b8b6..0000000 --- a/src/app/components/success/success.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component, OnInit, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { Router } from '@angular/router'; -import { AuthService } from '../../services/auth.service'; - -@Component({ - selector: 'app-success', - imports: [CommonModule], - templateUrl: './success.html', - styleUrl: './success.scss' -}) -export class SuccessComponent implements OnInit { - private authService = inject(AuthService); - private router = inject(Router); - - countdown: number = 3; - user: any = null; - - ngOnInit(): void { - this.user = this.authService.getCurrentUser(); - - // Countdown before redirect - const interval = setInterval(() => { - this.countdown--; - - if (this.countdown <= 0) { - clearInterval(interval); - this.redirect(); - } - }, 1000); - } - - redirect(): void { - const redirectUrl = localStorage.getItem('redirectUrl'); - if (redirectUrl) { - this.authService.redirectAfterAuth(redirectUrl); - } else { - // No redirect URL, show message - this.countdown = -1; - } - } - - redirectNow(): void { - this.redirect(); - } -} diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts deleted file mode 100644 index 3a362ee..0000000 --- a/src/app/guards/auth.guard.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { inject } from '@angular/core'; -import { Router, CanActivateFn } from '@angular/router'; -import { AuthService } from '../services/auth.service'; - -/** - * Auth guard to protect routes - * Redirects to login if user is not authenticated - */ -export const authGuard: CanActivateFn = (route, state) => { - const authService = inject(AuthService); - const router = inject(Router); - - if (authService.isAuthenticated()) { - return true; - } - - // Store the attempted URL for redirecting after login - const project = route.queryParams['project'] || 'unknown'; - localStorage.setItem('redirectUrl', state.url); - - // Redirect to login - router.navigate(['/login'], { - queryParams: { - project: project, - redirect: state.url - } - }); - - return false; -}; - -/** - * Guest guard - prevents authenticated users from accessing certain routes - * Useful for login pages - */ -export const guestGuard: CanActivateFn = (route, state) => { - const authService = inject(AuthService); - - if (!authService.isAuthenticated()) { - return true; - } - - // Already authenticated, redirect to home or stored redirect URL - const redirectUrl = localStorage.getItem('redirectUrl') || '/'; - window.location.href = redirectUrl; - - return false; -}; diff --git a/src/app/models/auth.model.ts b/src/app/models/auth.model.ts deleted file mode 100644 index e928520..0000000 --- a/src/app/models/auth.model.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Project identifier for authentication - */ -export type ProjectType = 'dexar' | 'novo' | 'fastcheck' | 'backoffice' | string; - -/** - * Device type classification - */ -export type DeviceType = 'mobile' | 'desktop' | 'tablet' | null; - -/** - * Operating system type - */ -export type DeviceOS = 'android' | 'ios' | 'windows' | 'macos' | 'linux' | null; - -/** - * Authentication context - */ -export type AuthContext = 'browser' | 'application' | 'telegram' | null; - -/** - * Complete device information - */ -export interface DeviceInfo { - deviceType: DeviceType; - deviceOS: DeviceOS; - context: AuthContext; - project: ProjectType; - userAgent?: string; - screenResolution?: string; - browserName?: string; - browserVersion?: string; -} - -/** - * QR code generation request - */ -export interface QRGenerationRequest { - project: ProjectType; - deviceInfo: DeviceInfo; -} - -/** - * QR code generation response - */ -export interface QRGenerationResponse { - sessionId: string; - qrCode: string; // base64 image or URL - expiresAt: string; - expiresIn: number; // seconds -} - -/** - * QR code scan request - */ -export interface QRScanRequest { - sessionId: string; - deviceInfo: DeviceInfo; -} - -/** - * Authentication status check - */ -export interface AuthStatusResponse { - authenticated: boolean; - token?: string; - userId?: string; - expiresAt?: string; - userInfo?: { - username: string; - email?: string; - role?: string; - }; -} - -/** - * Login request (traditional) - */ -export interface LoginRequest { - username: string; - password: string; - deviceInfo: DeviceInfo; -} - -/** - * Login response - */ -export interface LoginResponse { - success: boolean; - token?: string; - userId?: string; - expiresAt?: string; - message?: string; - userInfo?: { - username: string; - email?: string; - role?: string; - }; -} - -/** - * Session validation request - */ -export interface SessionValidationRequest { - token: string; - deviceInfo: DeviceInfo; -} - -/** - * Session validation response - */ -export interface SessionValidationResponse { - valid: boolean; - userId?: string; - expiresAt?: string; - userInfo?: { - username: string; - email?: string; - role?: string; - }; -} - -/** - * Logout request - */ -export interface LogoutRequest { - token: string; - deviceInfo: DeviceInfo; -} - -/** - * Error response - */ -export interface ErrorResponse { - error: boolean; - message: string; - code?: string; - details?: any; -} diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts deleted file mode 100644 index dee3eeb..0000000 --- a/src/app/services/api.service.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Observable, throwError } from 'rxjs'; -import { catchError } from 'rxjs/operators'; -import { environment } from '../../environments/environment'; -import { - QRGenerationRequest, - QRGenerationResponse, - QRScanRequest, - AuthStatusResponse, - LoginRequest, - LoginResponse, - SessionValidationRequest, - SessionValidationResponse, - LogoutRequest, - ErrorResponse -} from '../models/auth.model'; - -@Injectable({ - providedIn: 'root' -}) -export class ApiService { - private http = inject(HttpClient); - private apiUrl = environment.apiUrl; - - /** - * Generate QR code for authentication - */ - generateQR(request: QRGenerationRequest): Observable { - return this.http - .post(`${this.apiUrl}/auth/qr/generate`, request) - .pipe(catchError(this.handleError)); - } - - /** - * Scan QR code and authenticate - */ - scanQR(request: QRScanRequest): Observable { - return this.http - .post(`${this.apiUrl}/auth/qr/scan`, request) - .pipe(catchError(this.handleError)); - } - - /** - * Check authentication status for a session - */ - checkAuthStatus(sessionId: string): Observable { - return this.http - .get(`${this.apiUrl}/auth/qr/status/${sessionId}`) - .pipe(catchError(this.handleError)); - } - - /** - * Traditional login with username and password - */ - login(request: LoginRequest): Observable { - return this.http - .post(`${this.apiUrl}/auth/login`, request) - .pipe(catchError(this.handleError)); - } - - /** - * Validate session token - */ - validateSession(request: SessionValidationRequest): Observable { - const headers = new HttpHeaders({ - Authorization: `Bearer ${request.token}` - }); - - return this.http - .post( - `${this.apiUrl}/auth/validate`, - { deviceInfo: request.deviceInfo }, - { headers } - ) - .pipe(catchError(this.handleError)); - } - - /** - * Logout and invalidate session - */ - logout(request: LogoutRequest): Observable<{ success: boolean }> { - const headers = new HttpHeaders({ - Authorization: `Bearer ${request.token}` - }); - - return this.http - .post<{ success: boolean }>( - `${this.apiUrl}/auth/logout`, - { deviceInfo: request.deviceInfo }, - { headers } - ) - .pipe(catchError(this.handleError)); - } - - /** - * Handle HTTP errors - */ - private handleError(error: any): Observable { - let errorMessage = 'An error occurred'; - - if (error.error instanceof ErrorEvent) { - // Client-side error - errorMessage = error.error.message; - } else { - // Server-side error - errorMessage = error.error?.message || error.message || errorMessage; - } - - console.error('API Error:', errorMessage, error); - return throwError(() => ({ - error: true, - message: errorMessage, - code: error.status, - details: error.error - } as ErrorResponse)); - } -} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts deleted file mode 100644 index 5b5c1b3..0000000 --- a/src/app/services/auth.service.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { BehaviorSubject, Observable, interval } from 'rxjs'; -import { switchMap, takeWhile, tap } from 'rxjs/operators'; -import { ApiService } from './api.service'; -import { DeviceService } from './device.service'; -import { - LoginRequest, - LoginResponse, - QRGenerationResponse, - AuthStatusResponse, - ProjectType -} from '../models/auth.model'; -import { environment } from '../../environments/environment'; - -@Injectable({ - providedIn: 'root' -}) -export class AuthService { - private apiService = inject(ApiService); - private deviceService = inject(DeviceService); - private router = inject(Router); - - private authTokenSubject = new BehaviorSubject(this.getStoredToken()); - public authToken$ = this.authTokenSubject.asObservable(); - - private currentUserSubject = new BehaviorSubject(null); - public currentUser$ = this.currentUserSubject.asObservable(); - - private isAuthenticatedSubject = new BehaviorSubject(this.hasValidToken()); - public isAuthenticated$ = this.isAuthenticatedSubject.asObservable(); - - constructor() { - // Validate token on init - if (this.hasValidToken()) { - this.validateCurrentSession(); - } - } - - /** - * Generate QR code for authentication - */ - generateQRCode(project: ProjectType): Observable { - const deviceInfo = this.deviceService.getDeviceInfo(project); - - return this.apiService.generateQR({ project, deviceInfo }); - } - - /** - * Scan QR code and authenticate - */ - scanQRCode(sessionId: string): Observable { - const project = this.deviceService.getDeviceInfo().project; - const deviceInfo = this.deviceService.getDeviceInfo(project); - - return this.apiService.scanQR({ sessionId, deviceInfo }).pipe( - tap(response => { - if (response.success && response.token) { - this.handleSuccessfulAuth(response); - } - }) - ); - } - - /** - * Start polling for QR code authentication status - */ - startQRAuthPolling(sessionId: string): Observable { - return interval(environment.sessionCheckInterval).pipe( - switchMap(() => this.apiService.checkAuthStatus(sessionId)), - takeWhile(status => !status.authenticated, true), - tap(status => { - if (status.authenticated && status.token) { - this.handleSuccessfulAuth({ - success: true, - token: status.token, - userId: status.userId, - expiresAt: status.expiresAt, - userInfo: status.userInfo - }); - } - }) - ); - } - - /** - * Traditional login with username and password - */ - login(username: string, password: string, project: ProjectType): Observable { - const deviceInfo = this.deviceService.getDeviceInfo(project); - const request: LoginRequest = { - username, - password, - deviceInfo - }; - - return this.apiService.login(request).pipe( - tap(response => { - if (response.success && response.token) { - this.handleSuccessfulAuth(response); - } - }) - ); - } - - /** - * Logout current user - */ - logout(): void { - const token = this.getStoredToken(); - - if (token) { - const deviceInfo = this.deviceService.getDeviceInfo(); - this.apiService.logout({ token, deviceInfo }).subscribe({ - next: () => this.clearAuth(), - error: () => this.clearAuth() - }); - } else { - this.clearAuth(); - } - } - - /** - * Validate current session - */ - private validateCurrentSession(): void { - const token = this.getStoredToken(); - - if (!token) { - this.clearAuth(); - return; - } - - const deviceInfo = this.deviceService.getDeviceInfo(); - - this.apiService.validateSession({ token, deviceInfo }).subscribe({ - next: (response) => { - if (response.valid) { - this.currentUserSubject.next(response.userInfo); - this.isAuthenticatedSubject.next(true); - } else { - this.clearAuth(); - } - }, - error: () => { - this.clearAuth(); - } - }); - } - - /** - * Handle successful authentication - */ - private handleSuccessfulAuth(response: LoginResponse): void { - if (response.token) { - localStorage.setItem('authToken', response.token); - localStorage.setItem('userId', response.userId || ''); - localStorage.setItem('expiresAt', response.expiresAt || ''); - - this.authTokenSubject.next(response.token); - this.currentUserSubject.next(response.userInfo); - this.isAuthenticatedSubject.next(true); - } - } - - /** - * Clear authentication data - */ - private clearAuth(): void { - localStorage.removeItem('authToken'); - localStorage.removeItem('userId'); - localStorage.removeItem('expiresAt'); - - this.authTokenSubject.next(null); - this.currentUserSubject.next(null); - this.isAuthenticatedSubject.next(false); - } - - /** - * Get stored auth token - */ - getStoredToken(): string | null { - return localStorage.getItem('authToken'); - } - - /** - * Check if user has valid token - */ - hasValidToken(): boolean { - const token = this.getStoredToken(); - const expiresAt = localStorage.getItem('expiresAt'); - - if (!token || !expiresAt) { - return false; - } - - // Check if token is expired - const expiryDate = new Date(expiresAt); - const now = new Date(); - - return expiryDate > now; - } - - /** - * Check if user is authenticated - */ - isAuthenticated(): boolean { - return this.isAuthenticatedSubject.value; - } - - /** - * Get current user - */ - getCurrentUser(): any { - return this.currentUserSubject.value; - } - - /** - * Redirect after authentication - */ - redirectAfterAuth(defaultUrl: string = '/'): void { - const redirectUrl = localStorage.getItem('redirectUrl') || defaultUrl; - localStorage.removeItem('redirectUrl'); - window.location.href = redirectUrl; - } -} diff --git a/src/app/services/device.service.ts b/src/app/services/device.service.ts deleted file mode 100644 index f10720b..0000000 --- a/src/app/services/device.service.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { Injectable } from '@angular/core'; -import { UAParser } from 'ua-parser-js'; -import { DeviceInfo, DeviceType, DeviceOS, AuthContext, ProjectType } from '../models/auth.model'; - -@Injectable({ - providedIn: 'root' -}) -export class DeviceService { - private parser: UAParser; - private detectedContext: AuthContext = null; - private detectedProject: ProjectType = 'unknown'; - - constructor() { - this.parser = new UAParser(); - this.detectContext(); - } - - /** - * Get complete device information - */ - getDeviceInfo(project?: ProjectType, context?: AuthContext): DeviceInfo { - return { - deviceType: this.getDeviceType(), - deviceOS: this.getDeviceOS(), - context: context || this.detectedContext, - project: project || this.detectedProject, - userAgent: navigator.userAgent, - screenResolution: `${window.screen.width}x${window.screen.height}`, - browserName: this.getBrowserName(), - browserVersion: this.getBrowserVersion() - }; - } - - /** - * Set project identifier - */ - setProject(project: ProjectType): void { - this.detectedProject = project; - } - - /** - * Set authentication context - */ - setContext(context: AuthContext): void { - this.detectedContext = context; - } - - /** - * Detect device type (mobile, desktop, tablet) - */ - private getDeviceType(): DeviceType { - const device = this.parser.getDevice(); - - if (device.type === 'mobile') { - return 'mobile'; - } else if (device.type === 'tablet') { - return 'tablet'; - } else if (device.type === 'smarttv' || device.type === 'wearable' || device.type === 'embedded') { - return null; - } - - // Check by screen size if device type is not detected - const width = window.innerWidth; - if (width < 768) { - return 'mobile'; - } else if (width >= 768 && width < 1024) { - return 'tablet'; - } else { - return 'desktop'; - } - } - - /** - * Detect operating system - */ - private getDeviceOS(): DeviceOS { - const os = this.parser.getOS(); - const osName = os.name?.toLowerCase() || ''; - - if (osName.includes('android')) { - return 'android'; - } else if (osName.includes('ios') || osName.includes('iphone') || osName.includes('ipad')) { - return 'ios'; - } else if (osName.includes('windows')) { - return 'windows'; - } else if (osName.includes('mac')) { - return 'macos'; - } else if (osName.includes('linux')) { - return 'linux'; - } - - return null; - } - - /** - * Detect authentication context - */ - private detectContext(): void { - // Check if running in Telegram WebApp - if (this.isTelegramWebApp()) { - this.detectedContext = 'telegram'; - return; - } - - // Check if running as standalone app (PWA) - if (this.isStandaloneApp()) { - this.detectedContext = 'application'; - return; - } - - // Check if running in Electron - if (this.isElectron()) { - this.detectedContext = 'application'; - return; - } - - // Default to browser - this.detectedContext = 'browser'; - } - - /** - * Check if running in Telegram WebApp - */ - private isTelegramWebApp(): boolean { - return !!(window as any).Telegram?.WebApp; - } - - /** - * Check if running as standalone app (PWA) - */ - private isStandaloneApp(): boolean { - return ( - window.matchMedia('(display-mode: standalone)').matches || - (window.navigator as any).standalone === true || - document.referrer.includes('android-app://') - ); - } - - /** - * Check if running in Electron - */ - private isElectron(): boolean { - const ua = navigator.userAgent.toLowerCase(); - return ua.includes('electron'); - } - - /** - * Get browser name - */ - private getBrowserName(): string { - const browser = this.parser.getBrowser(); - return browser.name || 'Unknown'; - } - - /** - * Get browser version - */ - private getBrowserVersion(): string { - const browser = this.parser.getBrowser(); - return browser.version || 'Unknown'; - } - - /** - * Check if device is mobile - */ - isMobile(): boolean { - return this.getDeviceType() === 'mobile'; - } - - /** - * Check if device is desktop - */ - isDesktop(): boolean { - return this.getDeviceType() === 'desktop'; - } - - /** - * Check if device is tablet - */ - isTablet(): boolean { - return this.getDeviceType() === 'tablet'; - } - - /** - * Get readable device description - */ - getDeviceDescription(): string { - const deviceType = this.getDeviceType() || 'Unknown device'; - const os = this.getDeviceOS() || 'unknown OS'; - const context = this.detectedContext || 'unknown context'; - - return `${deviceType} with ${os} via ${context}`; - } -} diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts deleted file mode 100644 index 2e28ce0..0000000 --- a/src/environments/environment.production.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const environment = { - production: true, - apiUrl: 'https://api.dx-projects.com/api', - qrRefreshInterval: 60000, - sessionCheckInterval: 2000, - projects: { - dexar: 'Dexar Platform', - novo: 'Novo Markets', - fastcheck: 'FastCheck', - backoffice: 'Market BackOffice' - } -}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts deleted file mode 100644 index 2b21e62..0000000 --- a/src/environments/environment.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const environment = { - production: false, - apiUrl: 'http://localhost:3000/api', - qrRefreshInterval: 60000, // 60 seconds - sessionCheckInterval: 2000, // 2 seconds - projects: { - dexar: 'Dexar Platform', - novo: 'Novo Markets', - fastcheck: 'FastCheck', - backoffice: 'Market BackOffice' - } -}; diff --git a/src/index.html b/src/index.html index 0175939..0b1cbe7 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ - AuthService + Telegram Login Dialog diff --git a/src/styles.scss b/src/styles.scss index 5cc49b9..b062178 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,24 +1,9 @@ * { - margin: 0; - padding: 0; box-sizing: border-box; } -html, body { - height: 100%; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -} - -:root { - --primary-color: #667eea; - --primary-dark: #5568d3; - --secondary-color: #764ba2; - --success-color: #48bb78; - --error-color: #f56565; - --warning-color: #ed8936; - --text-primary: #2d3748; - --text-secondary: #4a5568; - --text-muted: #718096; - --bg-light: #f7fafc; - --border-color: #e2e8f0; +html, +body { + margin: 0; + min-height: 100%; }