diff --git a/FastCheck/BACKEND_IMPLEMENTATION.md b/FastCheck/BACKEND_IMPLEMENTATION.md new file mode 100644 index 0000000..5a28721 --- /dev/null +++ b/FastCheck/BACKEND_IMPLEMENTATION.md @@ -0,0 +1,426 @@ +# FastCheck Backend Implementation Guide + +## QR Code Authentication Flow + +### Overview +The frontend displays a QR code that contains a session ID. When a user scans this QR code with the mobile app, the mobile app authenticates and links to that session. The frontend polls the backend every 2 seconds to check if the session has been authenticated. + +### Step-by-Step Implementation + +--- + +## 1. Create WebSession (QR Code Generation) + +### Frontend Request: +```typescript +GET https://api.fastcheck.store/websession +Headers: { + "Content-Type": "application/json" +} +``` + +### Backend Response: +```json +{ + "sessionId": "1AF3781BF6B94604B771AEA1D44FA63A", + "userId": "", + "expires": "2026-01-19T10:50:00Z", + "userSessionId": "", + "Status": false +} +``` + +### Backend Implementation: +```javascript +// Example Node.js/Express +app.get('/websession', (req, res) => { + // Generate unique session ID (UUID or similar) + const sessionId = generateUUID(); // e.g., "1AF3781BF6B94604B771AEA1D44FA63A" + + // Set expiration time (e.g., 5 minutes from now) + const expires = new Date(Date.now() + 5 * 60 * 1000).toISOString(); + + // Store session in database or cache (Redis recommended) + await sessionStore.create({ + sessionId: sessionId, + userId: null, + userSessionId: null, + status: false, + expiresAt: expires, + createdAt: new Date() + }); + + // Return session data + res.json({ + sessionId: sessionId, + userId: "", + expires: expires, + userSessionId: "", + Status: false + }); +}); +``` + +### What Frontend Does: +```typescript +// Frontend generates QR code data from session ID +const qrData = `fastcheck://login?session=${sessionId}`; +// Example: "fastcheck://login?session=1AF3781BF6B94604B771AEA1D44FA63A" +``` + +**QR Code Contains:** Deep link URL with session ID +- Format: `fastcheck://login?session={sessionId}` +- Mobile app will parse this URL and extract the sessionId +- Mobile app will then authenticate and update this session + +--- + +## 2. Check WebSession Status (Polling) + +### Frontend Request (Every 2 seconds): +```typescript +GET https://api.fastcheck.store/websession/1AF3781BF6B94604B771AEA1D44FA63A +Headers: { + "Content-Type": "application/json" +} +``` + +### Backend Response (Not Authenticated Yet): +```json +{ + "sessionId": "1AF3781BF6B94604B771AEA1D44FA63A", + "userId": "", + "expires": "2026-01-19T10:50:00Z", + "userSessionId": "", + "Status": false +} +``` + +### Backend Response (Authenticated): +```json +{ + "sessionId": "1AF3781BF6B94604B771AEA1D44FA63A", + "userId": "kHaAe9roaC2uq63AKGE/8+Ti/t/iFro68QhEZ1dRGLo", + "expires": "2026-01-19T12:00:00Z", + "userSessionId": "8A94EFEFD003426A9B456C48CAC99BE6", + "Status": true +} +``` + +### Backend Implementation: +```javascript +app.get('/websession/:sessionId', async (req, res) => { + const { sessionId } = req.params; + + // Retrieve session from database/cache + const session = await sessionStore.get(sessionId); + + if (!session) { + return res.status(404).json({ message: "Session not found" }); + } + + // Check if session expired + if (new Date() > new Date(session.expiresAt)) { + await sessionStore.delete(sessionId); + return res.status(404).json({ message: "Session expired" }); + } + + // Return session status + res.json({ + sessionId: session.sessionId, + userId: session.userId || "", + expires: session.expiresAt, + userSessionId: session.userSessionId || "", + Status: session.status || false + }); +}); +``` + +--- + +## 3. Mobile App Authenticates Session + +**This is what the MOBILE APP does** (not the web frontend): + +### Mobile App Flow: +1. User scans QR code: `fastcheck://login?session=1AF3781BF6B94604B771AEA1D44FA63A` +2. Mobile app extracts sessionId: `1AF3781BF6B94604B771AEA1D44FA63A` +3. Mobile app authenticates user (PIN, biometrics, etc.) +4. Mobile app sends authentication request to backend: + +```typescript +POST https://api.fastcheck.store/websession/authenticate +Headers: { + "Authorization": "Bearer {mobile_app_token}", + "Content-Type": "application/json" +} +Body: { + "sessionId": "1AF3781BF6B94604B771AEA1D44FA63A", + "userId": "kHaAe9roaC2uq63AKGE/8+Ti/t/iFro68QhEZ1dRGLo" +} +``` + +### Backend Implementation: +```javascript +app.post('/websession/authenticate', authenticateMobileApp, async (req, res) => { + const { sessionId, userId } = req.body; + const mobileUserId = req.user.id; // From mobile app authentication + + // Verify the mobile user matches + if (userId !== mobileUserId) { + return res.status(403).json({ message: "Unauthorized" }); + } + + // Update session with user information + const userSessionId = generateUUID(); + await sessionStore.update(sessionId, { + userId: userId, + userSessionId: userSessionId, + status: true, + authenticatedAt: new Date() + }); + + res.json({ message: "Session authenticated" }); +}); +``` + +--- + +## 4. Logout (Delete Session) + +### Frontend Request: +```typescript +DELETE https://api.fastcheck.store/websession/1AF3781BF6B94604B771AEA1D44FA63A +Headers: { + "Authorization": "{\"sessionID\": \"1AF3781BF6B94604B771AEA1D44FA63A\"}", + "Content-Type": "application/json" +} +``` + +### Backend Implementation: +```javascript +app.delete('/websession/:sessionId', async (req, res) => { + const { sessionId } = req.params; + + // Delete session from database/cache + await sessionStore.delete(sessionId); + + res.json({ message: "Session deleted" }); +}); +``` + +--- + +## 5. Authenticated API Requests + +After login, all API requests include the sessionId in the Authorization header: + +### Frontend Request: +```typescript +POST https://api.fastcheck.store/fastcheck +Headers: { + "Authorization": "{\"sessionID\": \"1AF3781BF6B94604B771AEA1D44FA63A\"}", + "Content-Type": "application/json" +} +Body: { + "amount": 150000, + "currency": "RUB" +} +``` + +### Backend Authentication Middleware: +```javascript +// Middleware to verify session +const authenticateSession = async (req, res, next) => { + try { + // Parse Authorization header + const authHeader = req.headers.authorization; + if (!authHeader) { + return res.status(401).json({ message: "not authorized" }); + } + + // Parse JSON from Authorization header + const { sessionID } = JSON.parse(authHeader); + + // Verify session exists and is authenticated + const session = await sessionStore.get(sessionID); + + if (!session || !session.status) { + return res.status(401).json({ message: "not authorized" }); + } + + // Check if session expired + if (new Date() > new Date(session.expiresAt)) { + await sessionStore.delete(sessionID); + return res.status(401).json({ message: "not authorized" }); + } + + // Attach user info to request + req.user = { + userId: session.userId, + userSessionId: session.userSessionId, + sessionId: sessionID + }; + + next(); + } catch (error) { + return res.status(401).json({ message: "not authorized" }); + } +}; + +// Use middleware on protected routes +app.post('/fastcheck', authenticateSession, async (req, res) => { + const { amount, currency } = req.body; + const userId = req.user.userId; + + // Create FastCheck logic... +}); +``` + +--- + +## QR Code Data Format + +### What the QR Code Contains: +``` +fastcheck://login?session=1AF3781BF6B94604B771AEA1D44FA63A +``` + +**Format breakdown:** +- **Scheme**: `fastcheck://` - Deep link scheme for mobile app +- **Path**: `login` - Indicates this is a login QR code +- **Parameter**: `session={sessionId}` - The web session ID + +### Frontend QR Code Implementation: +```typescript +// In login.component.ts +const sessionResponse = await createWebSession(); +const qrData = `fastcheck://login?session=${sessionResponse.sessionId}`; + +// QR code component displays this as a QR image + +``` + +--- + +## Database Schema Recommendations + +### WebSession Table: +```sql +CREATE TABLE web_sessions ( + session_id VARCHAR(64) PRIMARY KEY, + user_id VARCHAR(255), + user_session_id VARCHAR(64), + status BOOLEAN DEFAULT FALSE, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + authenticated_at TIMESTAMP, + INDEX idx_expires (expires_at), + INDEX idx_status (status) +); + +-- Auto-delete expired sessions +CREATE EVENT cleanup_expired_sessions +ON SCHEDULE EVERY 1 HOUR +DO + DELETE FROM web_sessions WHERE expires_at < NOW(); +``` + +### Or use Redis (Recommended for sessions): +```javascript +// Redis structure +const sessionKey = `websession:${sessionId}`; +await redis.setex(sessionKey, 300, JSON.stringify({ + sessionId: sessionId, + userId: userId, + userSessionId: userSessionId, + status: true +})); +``` + +--- + +## Security Considerations + +1. **Session Expiration**: Sessions should expire after 5 minutes if not authenticated +2. **HTTPS Only**: All communication must be over HTTPS +3. **CORS Configuration**: Configure CORS to allow frontend domain +4. **Session Cleanup**: Regularly clean up expired sessions +5. **Rate Limiting**: Limit polling requests to prevent abuse +6. **Mobile App Authentication**: Mobile app must authenticate before linking session + +--- + +## Testing the Flow + +### 1. Test Session Creation: +```bash +curl -X GET https://api.fastcheck.store/websession +``` + +Expected: New session with Status: false + +### 2. Test Polling: +```bash +curl -X GET https://api.fastcheck.store/websession/{sessionId} +``` + +Expected: Same session, Status: false (until mobile app authenticates) + +### 3. Test Mobile Authentication (simulate): +```bash +curl -X POST https://api.fastcheck.store/websession/authenticate \ + -H "Authorization: Bearer {mobile_token}" \ + -H "Content-Type: application/json" \ + -d '{"sessionId": "{sessionId}", "userId": "{userId}"}' +``` + +Expected: Session updated with Status: true + +### 4. Test Polling After Auth: +```bash +curl -X GET https://api.fastcheck.store/websession/{sessionId} +``` + +Expected: Session with Status: true, userId populated + +--- + +## Frontend Polling Implementation (Already Done) + +```typescript +// In auth.service.ts +startPolling(sessionId: string): Observable { + return interval(2000).pipe( // Poll every 2 seconds + switchMap(() => this.checkWebSessionStatus(sessionId)), + tap(session => { + if (session.Status) { + this.setAuthenticated(session); + } + }), + takeWhile(session => !session.Status, true) // Stop when authenticated + ); +} +``` + +--- + +## Summary for Backend Team + +**Required Endpoints:** +1. ✅ `GET /websession` - Create session for QR +2. ✅ `GET /websession/:id` - Check session status (polled) +3. ⚠️ `POST /websession/authenticate` - Mobile app authenticates session (NEW) +4. ✅ `DELETE /websession/:id` - Logout + +**Required Logic:** +- Generate unique session IDs +- Store sessions with expiration +- Mobile app updates session status +- Web frontend polls until Status = true +- All authenticated APIs verify session in Authorization header + +**QR Code Data:** +- Format: `fastcheck://login?session={sessionId}` +- Mobile app parses and authenticates +- Web polls until mobile authenticates diff --git a/FastCheck/IMPLEMENTATION.md b/FastCheck/IMPLEMENTATION.md new file mode 100644 index 0000000..d5a5797 --- /dev/null +++ b/FastCheck/IMPLEMENTATION.md @@ -0,0 +1,200 @@ +# FastCheck Application - Implementation Summary + +## ✅ Completed Features + +### 1. Project Structure +- ✅ Angular 21 standalone components architecture +- ✅ TypeScript models for type safety +- ✅ SCSS styling with modern design +- ✅ Modular service-based architecture + +### 2. Authentication System +- ✅ QR Code login component +- ✅ WebSession management +- ✅ Auto-polling every 2 seconds to check login status +- ✅ Session persistence in sessionStorage +- ✅ Route guards for protected pages + +### 3. Dashboard +- ✅ Balance display (mocked) +- ✅ Create FastCheck with custom amount +- ✅ Accept FastCheck with number (xxxx-xxxx-xxxx) and code (xxxx) +- ✅ FastCheck number auto-formatting +- ✅ Success/error handling +- ✅ Modal to display created check details + +### 4. Active Checks Page +- ✅ List all unused FastChecks +- ✅ Copy to clipboard functionality +- ✅ Display check details (number, code, amount, expiration) +- ✅ Security warnings + +### 5. Transaction History +- ✅ View all used/expired checks +- ✅ Distinguish between created and accepted checks +- ✅ Timestamps and status display + +## 📁 File Structure + +``` +FastCheck/ +├── public/ +│ ├── api.txt # Original API documentation +│ └── missing-apis.txt # Missing API specifications for backend +├── src/ +│ ├── app/ +│ │ ├── components/ +│ │ │ ├── login/ # QR login with polling +│ │ │ ├── dashboard/ # Main dashboard +│ │ │ ├── active-checks/ # Active checks list +│ │ │ └── history/ # Transaction history +│ │ ├── services/ +│ │ │ ├── api.service.ts # HTTP client wrapper +│ │ │ ├── auth.service.ts # Authentication & session management +│ │ │ └── fastcheck.service.ts # FastCheck operations +│ │ ├── models/ +│ │ │ ├── session.model.ts # Session interfaces +│ │ │ ├── fastcheck.model.ts # FastCheck interfaces +│ │ │ └── api.model.ts # API response interfaces +│ │ ├── guards/ +│ │ │ └── auth.guard.ts # Route protection +│ │ ├── app.routes.ts # Route configuration +│ │ ├── app.config.ts # App configuration +│ │ ├── app.ts # Root component +│ │ ├── app.html # Root template +│ │ └── app.scss # Global styles +│ ├── environments/ +│ │ └── environment.ts # Environment configuration +│ ├── index.html # Main HTML +│ ├── main.ts # Bootstrap +│ └── styles.scss # Global styles +├── package.json +└── README.md # Project documentation +``` + +## 🔧 Technologies Used + +- **Angular 21** - Modern standalone components +- **TypeScript** - Type-safe development +- **RxJS** - Reactive programming (polling, API calls) +- **SCSS** - Styling +- **angularx-qrcode** - QR code generation +- **HttpClient** - API communication + +## 🎨 Design Features + +- Modern gradient UI (purple/violet theme) +- Responsive layout +- Smooth animations and transitions +- Loading states and spinners +- Error handling with user-friendly messages +- Copy-to-clipboard functionality +- Modal dialogs for important information + +## 🔌 API Integration + +### Fully Integrated: +1. `GET /ping` - Server health check +2. `GET /websession` - Create login session +3. `GET /websession/:id` - Poll login status +4. `DELETE /websession/:id` - Logout +5. `POST /fastcheck` - Create new check (with Authorization) +6. `POST /fastcheck` - Accept check (with Authorization) +7. `GET /fastcheck` - Check status + +### Mocked (Need Backend Implementation): +1. `GET /balance` - Get user balance +2. `GET /fastcheck/active` - List active checks +3. `GET /fastcheck/history` - Transaction history + +See `public/missing-apis.txt` for complete API specifications. + +## 🚀 Running the Application + +```bash +# Navigate to project directory +cd F:\dx\remote\FastCheck\FastCheck + +# Install dependencies (already done) +npm install + +# Start development server +npm start + +# Open browser at http://localhost:4200 +``` + +## 📝 Next Steps for Backend Team + +1. **Implement Missing APIs:** + - Balance endpoint + - Active checks endpoint + - History endpoint + +2. **Bank Integration:** + - Payment gateway API + - Redirect URLs for payment flow + - Webhook for payment confirmation + - Balance top-up mechanism + +3. **Update Frontend When Ready:** + - Uncomment real API calls in `fastcheck.service.ts` + - Remove mock `of()` observables + - Test with real data + +## 🔐 Security Considerations + +- SessionID stored in sessionStorage (clears on tab close) +- Authorization header on all authenticated requests +- CORS must be configured on backend +- HTTPS required in production +- FastCheck codes are sensitive - handle securely + +## 📱 User Flow + +1. **Login:** + - User opens app → sees QR code + - Scans with mobile app + - Frontend polls every 2s + - Redirects to dashboard on success + +2. **Create FastCheck:** + - Enter amount + - Click create + - Get number + code in modal + - Save credentials securely + +3. **Accept FastCheck:** + - Enter number (auto-formatted) + - Enter code + - Submit + - Money added to balance + +4. **View Checks:** + - Active checks → unused checks with copy feature + - History → all used/expired transactions + +## 🐛 Known Limitations (Temporary) + +- Balance API is mocked (returns 150,000 RUB) +- Active checks are mocked (returns 2 sample checks) +- History is mocked (returns 2 sample transactions) +- Bank integration not implemented yet +- No actual QR scanning (need mobile app integration) + +## 📞 Contact + +Developer: sdarbinyan@4pay.ru +Project: FastCheck СБП Payment System +Company: 4Pay + +## ✨ Status + +**Development Server:** ✅ Running on http://localhost:4200 +**All Components:** ✅ Implemented +**Routing:** ✅ Configured with guards +**Styling:** ✅ Complete with modern UI +**Mock Data:** ✅ In place for testing +**Documentation:** ✅ Complete + +Ready for backend integration and testing! diff --git a/FastCheck/README.md b/FastCheck/README.md index e77c3cc..788f771 100644 --- a/FastCheck/README.md +++ b/FastCheck/README.md @@ -1,59 +1,175 @@ -# FastCheck +# FastCheck - СБП Payment System -This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.4. +FastCheck is an online payment system that allows users to create and manage payment checks with СБП (Faster Payment System). -## Development server +## Features -To start a local development server, run: +- ✅ QR Code Authentication +- ✅ Balance Management +- ✅ Create FastCheck with custom amount +- ✅ Accept FastCheck with number and PIN +- ✅ View Active Checks +- ✅ Transaction History +- ⏳ Bank Integration (To be implemented) + +## Tech Stack + +- **Framework**: Angular 21 +- **Language**: TypeScript +- **Styling**: SCSS +- **HTTP Client**: Angular HttpClient +- **QR Code**: angularx-qrcode +- **API**: RESTful API (api.fastcheck.store) + +## Getting Started + +### Prerequisites + +- Node.js (v18 or higher) +- npm (v10 or higher) + +### Installation ```bash -ng serve +# Install dependencies +npm install + +# Start development server +npm start + +# The app will run on http://localhost:4200 ``` -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. - -## Code scaffolding - -Angular CLI includes powerful code scaffolding tools. To generate a new component, run: +### Build ```bash -ng generate component component-name +# Production build +npm run build + +# Output will be in dist/ folder ``` -For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: +## Project Structure -```bash -ng generate --help +``` +src/ +├── app/ +│ ├── components/ +│ │ ├── login/ # QR login with polling +│ │ ├── dashboard/ # Main dashboard +│ │ ├── active-checks/ # Active FastChecks list +│ │ └── history/ # Transaction history +│ ├── services/ +│ │ ├── api.service.ts # HTTP client wrapper +│ │ ├── auth.service.ts # Authentication logic +│ │ └── fastcheck.service.ts # FastCheck operations +│ ├── models/ # TypeScript interfaces +│ ├── guards/ # Route guards +│ └── app.routes.ts # Route configuration ``` -## Building +## API Documentation -To build the project run: +### Implemented APIs -```bash -ng build +- ✅ `GET /ping` - Check server availability +- ✅ `GET /websession` - Create QR session +- ✅ `GET /websession/:id` - Check login status (polling) +- ✅ `DELETE /websession/:id` - Logout +- ✅ `POST /fastcheck` - Create new FastCheck +- ✅ `POST /fastcheck` - Accept FastCheck +- ✅ `GET /fastcheck` - Check FastCheck status + +### Missing APIs (Mocked in Frontend) + +See `public/missing-apis.txt` for complete specifications: + +- ❌ `GET /balance` - Get user balance +- ❌ `GET /fastcheck/active` - Get active checks +- ❌ `GET /fastcheck/history` - Get transaction history + +**Note**: These APIs are currently mocked in the frontend. The backend team needs to implement them. + +## Features Overview + +### 1. Authentication +- Scan QR code with mobile app +- Auto-polling every 2 seconds +- Session management with sessionStorage + +### 2. Dashboard +- View current balance +- Create new FastCheck +- Accept existing FastCheck +- FastCheck format: `xxxx-xxxx-xxxx` +- Code format: `xxxx` + +### 3. Active Checks +- View all unused FastChecks +- Copy number and code to clipboard +- See expiration dates + +### 4. Transaction History +- View used/expired checks +- Filter by created/accepted +- See timestamps + +### 5. Balance Top-Up (To be implemented) +- Bank integration needed +- Will redirect to bank payment page +- Auto-refresh balance after payment + +## Development Notes + +### Mock Data + +The following services return mock data: +- `getBalance()` - Returns 150,000 RUB +- `getActiveFastChecks()` - Returns 2 sample active checks +- `getFastCheckHistory()` - Returns 2 sample history records + +Replace the mocked `of()` observables with real API calls once backend is ready. + +### Environment Configuration + +Update `src/environments/environment.ts` for different API URLs: + +```typescript +export const environment = { + production: false, + apiUrl: 'https://api.fastcheck.store' +}; ``` -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. +## Backend Requirements -## Running unit tests +Backend team needs to implement: -To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: +1. **Balance API** - `GET /balance` +2. **Active Checks API** - `GET /fastcheck/active` +3. **History API** - `GET /fastcheck/history` +4. **Bank Integration** - Payment gateway integration -```bash -ng test -``` +See `public/missing-apis.txt` for detailed API specifications. -## Running end-to-end tests +## Security Notes -For end-to-end (e2e) testing, run: +- SessionId stored in sessionStorage (clears on tab close) +- All authenticated requests include Authorization header +- FastCheck codes are sensitive - handle securely +- Implement HTTPS in production -```bash -ng e2e -``` +## Browser Support -Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) -## Additional Resources +## License -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. +Private - 4Pay + +## Contact + +For questions or issues, contact: sdarbinyan@4pay.ru diff --git a/FastCheck/package-lock.json b/FastCheck/package-lock.json index 3ab02a8..829a05c 100644 --- a/FastCheck/package-lock.json +++ b/FastCheck/package-lock.json @@ -14,6 +14,7 @@ "@angular/forms": "^21.0.0", "@angular/platform-browser": "^21.0.0", "@angular/router": "^21.0.0", + "angularx-qrcode": "^21.0.4", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -4148,6 +4149,20 @@ "node": ">= 14.0.0" } }, + "node_modules/angularx-qrcode": { + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/angularx-qrcode/-/angularx-qrcode-21.0.4.tgz", + "integrity": "sha512-GZFa/X/3rHx/4peA4zNROkK6UaYqxJX0dgkEMk7dCcoYNwJM8/UkOkEUfcx+Btjr7iT4UEhf9twWhFjFp58wfw==", + "license": "MIT", + "dependencies": { + "qrcode": "1.5.4", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^21.0.0", + "@angular/core": "^21.0.0" + } + }, "node_modules/ansi-escapes": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", @@ -4387,6 +4402,15 @@ "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.30001765", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", @@ -4575,7 +4599,6 @@ "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" @@ -4588,7 +4611,6 @@ "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": { @@ -4780,6 +4802,15 @@ } } }, + "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", @@ -4808,6 +4839,12 @@ "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", @@ -5271,6 +5308,19 @@ "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", @@ -5343,7 +5393,6 @@ "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.*" @@ -6029,6 +6078,18 @@ "@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", @@ -6802,6 +6863,33 @@ "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", @@ -6815,6 +6903,15 @@ "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", @@ -6924,6 +7021,15 @@ "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", @@ -7029,6 +7135,15 @@ "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", @@ -7113,6 +7228,125 @@ "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", @@ -7176,6 +7410,15 @@ "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", @@ -7186,6 +7429,12 @@ "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", @@ -7475,6 +7724,12 @@ "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", @@ -8380,6 +8635,12 @@ "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", @@ -8401,7 +8662,6 @@ "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", @@ -8416,7 +8676,6 @@ "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" @@ -8426,7 +8685,6 @@ "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" @@ -8442,14 +8700,12 @@ "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" @@ -8459,7 +8715,6 @@ "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", @@ -8474,7 +8729,6 @@ "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/FastCheck/package.json b/FastCheck/package.json index 0beb41d..cb816f6 100644 --- a/FastCheck/package.json +++ b/FastCheck/package.json @@ -29,6 +29,7 @@ "@angular/forms": "^21.0.0", "@angular/platform-browser": "^21.0.0", "@angular/router": "^21.0.0", + "angularx-qrcode": "^21.0.4", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -40,4 +41,4 @@ "typescript": "~5.9.2", "vitest": "^4.0.8" } -} \ No newline at end of file +} diff --git a/FastCheck/public/missing-apis.txt b/FastCheck/public/missing-apis.txt new file mode 100644 index 0000000..d320c48 --- /dev/null +++ b/FastCheck/public/missing-apis.txt @@ -0,0 +1,124 @@ +MISSING APIs - TO BE IMPLEMENTED BY BACKEND +============================================== + +Get User Balance +---------------- +Get the current balance of the authenticated user. +Protocol: https +Root Path: api.Fastcheck.store +Type: GET +Path: /balance +HEADER: Authorization - {"sessionID": "1AF3781BF6B94604B771AEA1D44FA63A"} +Request Parameters: +{ +} +Response (200-OK): +{ + "balance": 150000, + "currency": "RUB" +} +Response (401-ERROR): +{ + "message": "not authorized" +} + + +Get Active FastChecks +--------------------- +Get all active (unused) FastChecks created by the current user. +Protocol: https +Root Path: api.Fastcheck.store +Type: GET +Path: /fastcheck/active +HEADER: Authorization - {"sessionID": "1AF3781BF6B94604B771AEA1D44FA63A"} +Request Parameters: +{ +} +Response (200-OK): +{ + "checks": [ + { + "fastcheck": "1234-5678-0001", + "amount": 15000, + "currency": "RUB", + "code": "5864", + "createdAt": "2026-01-19T09:08:18Z", + "expiration": "2026-01-26T09:08:18Z", + "status": "active" + }, + { + "fastcheck": "1234-5678-0002", + "amount": 25000, + "currency": "RUB", + "code": "1234", + "createdAt": "2026-01-19T10:15:30Z", + "expiration": "2026-01-26T10:15:30Z", + "status": "active" + } + ] +} +Response (401-ERROR): +{ + "message": "not authorized" +} + + +Get FastCheck History +--------------------- +Get all used/expired FastChecks (both created and accepted by user). +Protocol: https +Root Path: api.Fastcheck.store +Type: GET +Path: /fastcheck/history +HEADER: Authorization - {"sessionID": "1AF3781BF6B94604B771AEA1D44FA63A"} +Request Parameters: +{ +} +Response (200-OK): +{ + "checks": [ + { + "fastcheck": "1234-5678-0003", + "amount": 5000, + "currency": "RUB", + "type": "created", + "createdAt": "2026-01-15T09:08:18Z", + "usedAt": "2026-01-15T10:20:00Z", + "status": "used" + }, + { + "fastcheck": "9876-5432-0100", + "amount": 10000, + "currency": "RUB", + "type": "accepted", + "acceptedAt": "2026-01-14T14:30:00Z", + "status": "used" + } + ] +} +Response (401-ERROR): +{ + "message": "not authorized" +} + + +Bank Top-Up Integration (To be provided by bank) +------------------------------------------------- +WHAT WE NEED FROM BANK: +1. Payment page URL or API endpoint to initialize payment +2. Required parameters: + - Amount + - Currency + - Return URL (redirect after payment) + - Callback URL (for payment confirmation webhook) +3. Payment confirmation webhook format +4. Transaction ID for tracking + +EXPECTED FLOW: +1. User clicks "Top Up Balance" +2. Frontend redirects to bank payment page (or opens popup) +3. User completes card payment on bank side +4. Bank sends webhook to backend with payment confirmation +5. Backend updates user balance +6. Bank redirects user back to our app +7. Frontend refreshes balance diff --git a/FastCheck/src/app/app.config.ts b/FastCheck/src/app/app.config.ts index cb1270e..c51175a 100644 --- a/FastCheck/src/app/app.config.ts +++ b/FastCheck/src/app/app.config.ts @@ -1,11 +1,13 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter } from '@angular/router'; +import { provideHttpClient, withFetch } from '@angular/common/http'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), - provideRouter(routes) + provideRouter(routes), + provideHttpClient(withFetch()) ] }; diff --git a/FastCheck/src/app/app.html b/FastCheck/src/app/app.html index e0118a1..0680b43 100644 --- a/FastCheck/src/app/app.html +++ b/FastCheck/src/app/app.html @@ -1,342 +1 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - - + diff --git a/FastCheck/src/app/app.routes.ts b/FastCheck/src/app/app.routes.ts index dc39edb..3187b73 100644 --- a/FastCheck/src/app/app.routes.ts +++ b/FastCheck/src/app/app.routes.ts @@ -1,3 +1,38 @@ import { Routes } from '@angular/router'; +import { authGuard, loginGuard } from './guards/auth.guard'; +import { LoginComponent } from './components/login/login.component'; +import { DashboardComponent } from './components/dashboard/dashboard.component'; +import { ActiveChecksComponent } from './components/active-checks/active-checks.component'; +import { HistoryComponent } from './components/history/history.component'; -export const routes: Routes = []; +export const routes: Routes = [ + { + path: '', + redirectTo: '/login', + pathMatch: 'full' + }, + { + path: 'login', + component: LoginComponent, + canActivate: [loginGuard] + }, + { + path: 'dashboard', + component: DashboardComponent, + canActivate: [authGuard] + }, + { + path: 'active-checks', + component: ActiveChecksComponent, + canActivate: [authGuard] + }, + { + path: 'history', + component: HistoryComponent, + canActivate: [authGuard] + }, + { + path: '**', + redirectTo: '/login' + } +]; diff --git a/FastCheck/src/app/app.scss b/FastCheck/src/app/app.scss index e69de29..92faef9 100644 --- a/FastCheck/src/app/app.scss +++ b/FastCheck/src/app/app.scss @@ -0,0 +1,18 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + width: 100%; + overflow-x: hidden; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + margin: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/FastCheck/src/app/components/active-checks/active-checks.component.html b/FastCheck/src/app/components/active-checks/active-checks.component.html new file mode 100644 index 0000000..36a445a --- /dev/null +++ b/FastCheck/src/app/components/active-checks/active-checks.component.html @@ -0,0 +1,95 @@ +
+
+ + +
+ +
+ + + @if (isLoading()) { +
+
+

Loading active checks...

+
+ } + + @if (error()) { +
+

{{ error() }}

+ +
+ } + + @if (!isLoading() && !error()) { + @if (checks().length === 0) { +
+
📭
+

No Active Checks

+

You don't have any active FastChecks at the moment.

+ Create FastCheck +
+ } @else { +
+ @for (check of checks(); track check.fastcheck) { +
+
+ Active + {{ formatAmount(check.amount) }} ₽ +
+ +
+
+ FastCheck Number +
+ {{ check.fastcheck }} + +
+
+ +
+ Code +
+ {{ check.code }} + +
+
+ +
+ Created + {{ check.createdAt | date:'short' }} +
+ +
+ Expires + {{ check.expiration | date:'short' }} +
+
+ +
+ ⚠️ Keep this information secure. Anyone with these credentials can claim the money. +
+
+ } +
+ } + } +
+
diff --git a/FastCheck/src/app/components/active-checks/active-checks.component.scss b/FastCheck/src/app/components/active-checks/active-checks.component.scss new file mode 100644 index 0000000..f1139c1 --- /dev/null +++ b/FastCheck/src/app/components/active-checks/active-checks.component.scss @@ -0,0 +1,280 @@ +.page-container { + min-height: 100vh; + background: #f5f7fa; +} + +.header { + background: white; + padding: 20px 40px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo { + font-size: 24px; + font-weight: 700; + color: #667eea; +} + +.nav { + display: flex; + gap: 30px; +} + +.nav-link { + text-decoration: none; + color: #666; + font-weight: 500; + padding: 8px 16px; + border-radius: 8px; + transition: all 0.3s; + + &:hover { + color: #667eea; + background: #f0f0f0; + } + + &.active { + color: #667eea; + background: #e8ebff; + } +} + +.content { + padding: 40px; + max-width: 1200px; + margin: 0 auto; + + @media (max-width: 768px) { + padding: 20px; + } +} + +.page-header { + margin-bottom: 40px; + + h1 { + font-size: 32px; + color: #333; + margin-bottom: 10px; + } + + p { + color: #666; + font-size: 16px; + } +} + +.loading { + text-align: center; + padding: 60px 20px; + + .spinner { + width: 50px; + height: 50px; + border: 4px solid #f3f3f3; + border-top: 4px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; + } + + p { + color: #666; + } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.error-card { + background: white; + border-radius: 15px; + padding: 40px; + text-align: center; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); + + p { + color: #c33; + margin-bottom: 20px; + } +} + +.btn-retry { + background: #667eea; + color: white; + border: none; + padding: 12px 30px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s; + + &:hover { + background: #764ba2; + } +} + +.empty-state { + background: white; + border-radius: 20px; + padding: 60px 40px; + text-align: center; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); + + .empty-icon { + font-size: 64px; + margin-bottom: 20px; + } + + h3 { + font-size: 24px; + color: #333; + margin-bottom: 10px; + } + + p { + color: #666; + margin-bottom: 30px; + } +} + +.btn-primary { + display: inline-block; + background: #667eea; + color: white; + text-decoration: none; + padding: 14px 30px; + border-radius: 8px; + font-weight: 600; + transition: all 0.3s; + + &:hover { + background: #764ba2; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); + } +} + +.checks-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 30px; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: 20px; + } +} + +.check-card { + background: white; + border-radius: 15px; + padding: 25px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); + border: 2px solid #e8ebff; + transition: all 0.3s; + + @media (max-width: 768px) { + padding: 20px; + } + + &:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15); + } +} + +.check-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 2px solid #f0f0f0; +} + +.check-badge { + background: #52c41a; + color: white; + padding: 6px 14px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; +} + +.check-amount { + font-size: 24px; + font-weight: 700; + color: #667eea; +} + +.check-details { + margin-bottom: 15px; +} + +.detail-item { + margin-bottom: 15px; + + .label { + display: block; + font-size: 12px; + color: #999; + text-transform: uppercase; + font-weight: 600; + margin-bottom: 5px; + } + + .value { + font-size: 16px; + color: #333; + font-weight: 500; + + &.code { + font-size: 20px; + color: #667eea; + font-weight: 700; + } + } +} + +.value-copy { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; +} + +.btn-copy { + background: #f0f0f0; + border: none; + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 16px; + transition: all 0.2s; + + &:hover { + background: #e0e0e0; + transform: scale(1.1); + } + + &:active { + transform: scale(0.95); + } +} + +.check-warning { + background: #fffbe6; + border-left: 4px solid #faad14; + padding: 12px; + border-radius: 6px; + font-size: 12px; + color: #666; + line-height: 1.5; +} diff --git a/FastCheck/src/app/components/active-checks/active-checks.component.ts b/FastCheck/src/app/components/active-checks/active-checks.component.ts new file mode 100644 index 0000000..4a0d7f4 --- /dev/null +++ b/FastCheck/src/app/components/active-checks/active-checks.component.ts @@ -0,0 +1,51 @@ +import { Component, OnInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { FastCheckService } from '../../services/fastcheck.service'; +import { FastCheck } from '../../models/fastcheck.model'; + +@Component({ + selector: 'app-active-checks', + standalone: true, + imports: [CommonModule, RouterLink], + templateUrl: './active-checks.component.html', + styleUrls: ['./active-checks.component.scss'] +}) +export class ActiveChecksComponent implements OnInit { + checks = signal([]); + isLoading = signal(true); + error = signal(''); + + constructor(private fastCheckService: FastCheckService) {} + + ngOnInit(): void { + this.loadActiveChecks(); + } + + loadActiveChecks(): void { + this.isLoading.set(true); + this.error.set(''); + + this.fastCheckService.getActiveFastChecks().subscribe({ + next: (response) => { + this.checks.set(response.checks); + this.isLoading.set(false); + }, + error: (err) => { + this.error.set('Failed to load active checks'); + this.isLoading.set(false); + console.error('Load error:', err); + } + }); + } + + formatAmount(amount: number): string { + return new Intl.NumberFormat('ru-RU').format(amount); + } + + copyToClipboard(text: string, type: string): void { + navigator.clipboard.writeText(text).then(() => { + alert(`${type} copied to clipboard!`); + }); + } +} diff --git a/FastCheck/src/app/components/dashboard/dashboard.component.html b/FastCheck/src/app/components/dashboard/dashboard.component.html new file mode 100644 index 0000000..74e3fd2 --- /dev/null +++ b/FastCheck/src/app/components/dashboard/dashboard.component.html @@ -0,0 +1,142 @@ +
+ +
+ + +
+ +
+ +
+ @if (isLoadingBalance()) { +
+
+
+ } @else if (balance()) { +
+ Current Balance +

{{ formatAmount(balance()!.balance) }} ₽

+ +
+ } +
+ +
+ +
+

Create New FastCheck

+ +
+ + +
+ + @if (createError()) { +
{{ createError() }}
+ } + + +
+ + +
+

Accept FastCheck

+ +
+ + +
+ +
+ + +
+ + @if (acceptError()) { +
{{ acceptError() }}
+ } + + @if (acceptSuccess()) { +
FastCheck accepted successfully!
+ } + + +
+
+
+
+ + +@if (createdCheck()) { + +} diff --git a/FastCheck/src/app/components/dashboard/dashboard.component.scss b/FastCheck/src/app/components/dashboard/dashboard.component.scss new file mode 100644 index 0000000..c1b6ecb --- /dev/null +++ b/FastCheck/src/app/components/dashboard/dashboard.component.scss @@ -0,0 +1,363 @@ +.dashboard-container { + min-height: 100vh; + background: #f5f7fa; +} + +.header { + background: white; + padding: 20px 40px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + display: flex; + justify-content: space-between; + align-items: center; + + @media (max-width: 768px) { + padding: 15px 20px; + flex-direction: column; + gap: 15px; + } +} + +.logo { + font-size: 24px; + font-weight: 700; + color: #667eea; +} + +.nav { + display: flex; + gap: 30px; + align-items: center; + + @media (max-width: 768px) { + gap: 10px; + flex-wrap: wrap; + justify-content: center; + } +} + +.nav-link { + text-decoration: none; + color: #666; + font-weight: 500; + padding: 8px 16px; + border-radius: 8px; + transition: all 0.3s; + + &:hover { + color: #667eea; + background: #f0f0f0; + } + + &.active { + color: #667eea; + background: #e8ebff; + } +} + +.btn-logout { + background: #ff4d4f; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s; + + &:hover { + background: #ff7875; + transform: translateY(-2px); + } +} + +.content { + padding: 40px; + max-width: 1200px; + margin: 0 auto; + + @media (max-width: 768px) { + padding: 20px; + } +} + +.balance-card { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 20px; + padding: 40px; + color: white; + margin-bottom: 40px; + box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3); + + @media (max-width: 768px) { + padding: 30px 20px; + margin-bottom: 20px; + } +} + +.balance-info { + text-align: center; +} + +.balance-label { + font-size: 16px; + opacity: 0.9; + display: block; + margin-bottom: 10px; +} + +.balance-amount { + font-size: 48px; + font-weight: 700; + margin: 10px 0 30px; + + @media (max-width: 768px) { + font-size: 36px; + margin: 10px 0 20px; + } +} + +.btn-topup { + background: rgba(255, 255, 255, 0.2); + color: white; + border: 2px solid white; + padding: 12px 30px; + border-radius: 10px; + cursor: pointer; + font-size: 16px; + font-weight: 600; + transition: all 0.3s; + + &:hover { + background: white; + color: #667eea; + } +} + +.actions-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 30px; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: 20px; + } +} + +.card { + background: white; + border-radius: 15px; + padding: 30px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); +} + +.card-title { + font-size: 20px; + font-weight: 600; + margin-bottom: 25px; + color: #333; +} + +.form-group { + margin-bottom: 20px; + + label { + display: block; + font-size: 14px; + font-weight: 500; + color: #666; + margin-bottom: 8px; + } +} + +.input { + width: 100%; + padding: 12px 16px; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 16px; + transition: all 0.3s; + box-sizing: border-box; + + &:focus { + outline: none; + border-color: #667eea; + } + + &:disabled { + background: #f5f5f5; + cursor: not-allowed; + } +} + +.btn-primary { + width: 100%; + background: #667eea; + color: white; + border: none; + padding: 14px; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + font-weight: 600; + transition: all 0.3s; + margin-top: 10px; + + &:hover:not(:disabled) { + background: #764ba2; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); + } + + &:disabled { + background: #ccc; + cursor: not-allowed; + transform: none; + } +} + +.error-message { + background: #fee; + color: #c33; + padding: 12px; + border-radius: 8px; + font-size: 14px; + margin: 15px 0; +} + +.success-message { + background: #efe; + color: #3c3; + padding: 12px; + border-radius: 8px; + font-size: 14px; + margin: 15px 0; +} + +.loading-small { + text-align: center; + padding: 20px; + + .spinner-small { + width: 30px; + height: 30px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-top: 3px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto; + } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +// Modal +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal { + background: white; + border-radius: 20px; + max-width: 500px; + width: 90%; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); +} + +.modal-header { + padding: 30px 30px 20px; + border-bottom: 1px solid #eee; + display: flex; + justify-content: space-between; + align-items: center; + + h3 { + margin: 0; + color: #333; + } +} + +.close-btn { + background: none; + border: none; + font-size: 32px; + cursor: pointer; + color: #999; + line-height: 1; + padding: 0; + width: 32px; + height: 32px; + + &:hover { + color: #333; + } +} + +.modal-body { + padding: 30px; +} + +.check-details { + background: #f9f9f9; + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; +} + +.detail-row { + display: flex; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid #e0e0e0; + + &:last-child { + border-bottom: none; + } +} + +.detail-label { + color: #666; + font-size: 14px; +} + +.detail-value { + font-weight: 600; + color: #333; + + &.code { + font-size: 20px; + color: #667eea; + } +} + +.modal-note { + background: #fffbe6; + border-left: 4px solid #faad14; + padding: 15px; + border-radius: 8px; + + p { + margin: 0; + font-size: 14px; + color: #666; + line-height: 1.6; + } +} + +.modal-footer { + padding: 20px 30px 30px; + text-align: center; +} diff --git a/FastCheck/src/app/components/dashboard/dashboard.component.ts b/FastCheck/src/app/components/dashboard/dashboard.component.ts new file mode 100644 index 0000000..3034ee5 --- /dev/null +++ b/FastCheck/src/app/components/dashboard/dashboard.component.ts @@ -0,0 +1,169 @@ +import { Component, OnInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { FastCheckService } from '../../services/fastcheck.service'; +import { AuthService } from '../../services/auth.service'; +import { Balance, CreateFastCheckResponse } from '../../models/fastcheck.model'; + +@Component({ + selector: 'app-dashboard', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink], + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'] +}) +export class DashboardComponent implements OnInit { + balance = signal(null); + isLoadingBalance = signal(true); + + // Create FastCheck + createAmount = signal(0); + isCreating = signal(false); + createdCheck = signal(null); + createError = signal(''); + + // Accept FastCheck + acceptNumber = signal(''); + acceptCode = signal(''); + isAccepting = signal(false); + acceptSuccess = signal(false); + acceptError = signal(''); + + constructor( + private fastCheckService: FastCheckService, + private authService: AuthService, + private router: Router + ) {} + + ngOnInit(): void { + this.loadBalance(); + } + + loadBalance(): void { + this.isLoadingBalance.set(true); + this.fastCheckService.getBalance().subscribe({ + next: (balance) => { + this.balance.set(balance); + this.isLoadingBalance.set(false); + }, + error: (err) => { + console.error('Failed to load balance:', err); + this.isLoadingBalance.set(false); + } + }); + } + + createFastCheck(): void { + const amount = this.createAmount(); + + if (!amount || amount <= 0) { + this.createError.set('Please enter a valid amount'); + return; + } + + const currentBalance = this.balance(); + if (currentBalance && amount > currentBalance.balance) { + this.createError.set('Insufficient balance'); + return; + } + + this.isCreating.set(true); + this.createError.set(''); + this.createdCheck.set(null); + + this.fastCheckService.createFastCheck({ + amount: amount, + currency: 'RUB' + }).subscribe({ + next: (response) => { + this.createdCheck.set(response); + this.isCreating.set(false); + this.createAmount.set(0); + this.loadBalance(); // Refresh balance + }, + error: (err) => { + this.createError.set('Failed to create FastCheck. Please try again.'); + this.isCreating.set(false); + console.error('Create error:', err); + } + }); + } + + acceptFastCheck(): void { + const number = this.acceptNumber().trim(); + const code = this.acceptCode().trim(); + + if (!number || !code) { + this.acceptError.set('Please enter both FastCheck number and code'); + return; + } + + this.isAccepting.set(true); + this.acceptError.set(''); + this.acceptSuccess.set(false); + + this.fastCheckService.acceptFastCheck({ + fastcheck: number, + code: code + }).subscribe({ + next: () => { + this.acceptSuccess.set(true); + this.isAccepting.set(false); + this.acceptNumber.set(''); + this.acceptCode.set(''); + this.loadBalance(); // Refresh balance + + setTimeout(() => { + this.acceptSuccess.set(false); + }, 3000); + }, + error: (err) => { + this.acceptError.set('Failed to accept FastCheck. Check your credentials.'); + this.isAccepting.set(false); + console.error('Accept error:', err); + } + }); + } + + formatAmount(amount: number): string { + return new Intl.NumberFormat('ru-RU').format(amount); + } + + formatFastCheckNumber(input: string): string { + const cleaned = input.replace(/\D/g, ''); + const formatted = cleaned.match(/.{1,4}/g)?.join('-') || ''; + return formatted.slice(0, 14); // xxxx-xxxx-xxxx + } + + onFastCheckNumberInput(event: Event): void { + const input = event.target as HTMLInputElement; + const formatted = this.formatFastCheckNumber(input.value); + this.acceptNumber.set(formatted); + } + + closeCreatedCheckModal(): void { + this.createdCheck.set(null); + } + + logout(): void { + const sessionId = this.authService.getSessionId(); + if (sessionId) { + this.authService.deleteWebSession(sessionId).subscribe({ + next: () => { + this.router.navigate(['/login']); + }, + error: (err) => { + console.error('Logout error:', err); + this.authService.clearAuthentication(); + this.router.navigate(['/login']); + } + }); + } + } + + topUpBalance(): void { + // TODO: Implement bank integration + alert('Bank integration will be implemented. You will be redirected to bank payment page.'); + } +} diff --git a/FastCheck/src/app/components/history/history.component.html b/FastCheck/src/app/components/history/history.component.html new file mode 100644 index 0000000..df13677 --- /dev/null +++ b/FastCheck/src/app/components/history/history.component.html @@ -0,0 +1,86 @@ +
+
+ + +
+ +
+ + + @if (isLoading()) { +
+
+

Loading history...

+
+ } + + @if (error()) { +
+

{{ error() }}

+ +
+ } + + @if (!isLoading() && !error()) { + @if (checks().length === 0) { +
+
📜
+

No History

+

Your transaction history will appear here.

+ Go to Dashboard +
+ } @else { +
+ @for (check of checks(); track check.fastcheck) { +
+
+
+ + {{ getTypeLabel(check.type) }} + + {{ check.fastcheck }} +
+ {{ formatAmount(check.amount) }} ₽ +
+ +
+ @if (check.createdAt) { +
+ Created: + {{ check.createdAt | date:'short' }} +
+ } + + @if (check.usedAt) { +
+ Used: + {{ check.usedAt | date:'short' }} +
+ } + + @if (check.acceptedAt) { +
+ Accepted: + {{ check.acceptedAt | date:'short' }} +
+ } + +
+ Status: + {{ check.status }} +
+
+
+ } +
+ } + } +
+
diff --git a/FastCheck/src/app/components/history/history.component.scss b/FastCheck/src/app/components/history/history.component.scss new file mode 100644 index 0000000..f3836df --- /dev/null +++ b/FastCheck/src/app/components/history/history.component.scss @@ -0,0 +1,270 @@ +.page-container { + min-height: 100vh; + background: #f5f7fa; +} + +.header { + background: white; + padding: 20px 40px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo { + font-size: 24px; + font-weight: 700; + color: #667eea; +} + +.nav { + display: flex; + gap: 30px; +} + +.nav-link { + text-decoration: none; + color: #666; + font-weight: 500; + padding: 8px 16px; + border-radius: 8px; + transition: all 0.3s; + + &:hover { + color: #667eea; + background: #f0f0f0; + } + + &.active { + color: #667eea; + background: #e8ebff; + } +} + +.content { + padding: 40px; + max-width: 1000px; + margin: 0 auto; + + @media (max-width: 768px) { + padding: 20px; + } +} + +.page-header { + margin-bottom: 40px; + + h1 { + font-size: 32px; + color: #333; + margin-bottom: 10px; + } + + p { + color: #666; + font-size: 16px; + } +} + +.loading { + text-align: center; + padding: 60px 20px; + + .spinner { + width: 50px; + height: 50px; + border: 4px solid #f3f3f3; + border-top: 4px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; + } + + p { + color: #666; + } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.error-card { + background: white; + border-radius: 15px; + padding: 40px; + text-align: center; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); + + p { + color: #c33; + margin-bottom: 20px; + } +} + +.btn-retry { + background: #667eea; + color: white; + border: none; + padding: 12px 30px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s; + + &:hover { + background: #764ba2; + } +} + +.empty-state { + background: white; + border-radius: 20px; + padding: 60px 40px; + text-align: center; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); + + .empty-icon { + font-size: 64px; + margin-bottom: 20px; + } + + h3 { + font-size: 24px; + color: #333; + margin-bottom: 10px; + } + + p { + color: #666; + margin-bottom: 30px; + } +} + +.btn-primary { + display: inline-block; + background: #667eea; + color: white; + text-decoration: none; + padding: 14px 30px; + border-radius: 8px; + font-weight: 600; + transition: all 0.3s; + + &:hover { + background: #764ba2; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); + } +} + +.history-list { + display: flex; + flex-direction: column; + gap: 20px; +} + +.history-item { + background: white; + border-radius: 12px; + padding: 25px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + transition: all 0.3s; + + @media (max-width: 768px) { + padding: 20px; + } + + &:hover { + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + } +} + +.item-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #f0f0f0; +} + +.item-info { + display: flex; + align-items: center; + gap: 15px; +} + +.type-badge { + padding: 6px 14px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + + &.type-created { + background: #e8ebff; + color: #667eea; + } + + &.type-accepted { + background: #e6f7ff; + color: #1890ff; + } +} + +.item-number { + font-family: monospace; + font-size: 16px; + color: #666; + font-weight: 500; +} + +.item-amount { + font-size: 24px; + font-weight: 700; + color: #333; +} + +.item-details { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: 10px; + } +} + +.detail { + display: flex; + flex-direction: column; + gap: 5px; +} + +.detail-label { + font-size: 12px; + color: #999; + text-transform: uppercase; + font-weight: 600; +} + +.detail-value { + font-size: 14px; + color: #333; + font-weight: 500; +} + +.status-badge { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + background: #f5f5f5; + color: #999; +} diff --git a/FastCheck/src/app/components/history/history.component.ts b/FastCheck/src/app/components/history/history.component.ts new file mode 100644 index 0000000..4a98297 --- /dev/null +++ b/FastCheck/src/app/components/history/history.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { FastCheckService } from '../../services/fastcheck.service'; +import { FastCheck } from '../../models/fastcheck.model'; + +@Component({ + selector: 'app-history', + standalone: true, + imports: [CommonModule, RouterLink], + templateUrl: './history.component.html', + styleUrls: ['./history.component.scss'] +}) +export class HistoryComponent implements OnInit { + checks = signal([]); + isLoading = signal(true); + error = signal(''); + + constructor(private fastCheckService: FastCheckService) {} + + ngOnInit(): void { + this.loadHistory(); + } + + loadHistory(): void { + this.isLoading.set(true); + this.error.set(''); + + this.fastCheckService.getFastCheckHistory().subscribe({ + next: (response) => { + this.checks.set(response.checks); + this.isLoading.set(false); + }, + error: (err) => { + this.error.set('Failed to load history'); + this.isLoading.set(false); + console.error('Load error:', err); + } + }); + } + + formatAmount(amount: number): string { + return new Intl.NumberFormat('ru-RU').format(amount); + } + + getTypeLabel(type?: string): string { + return type === 'created' ? 'Created' : 'Accepted'; + } + + getTypeClass(type?: string): string { + return type === 'created' ? 'type-created' : 'type-accepted'; + } +} diff --git a/FastCheck/src/app/components/login/login.component.html b/FastCheck/src/app/components/login/login.component.html new file mode 100644 index 0000000..14e7f75 --- /dev/null +++ b/FastCheck/src/app/components/login/login.component.html @@ -0,0 +1,39 @@ + diff --git a/FastCheck/src/app/components/login/login.component.scss b/FastCheck/src/app/components/login/login.component.scss new file mode 100644 index 0000000..d6c17ce --- /dev/null +++ b/FastCheck/src/app/components/login/login.component.scss @@ -0,0 +1,177 @@ +.login-container { + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; + overflow: hidden; +} + +.login-card { + background: white; + border-radius: 20px; + padding: 40px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 400px; + width: 100%; + text-align: center; + + @media (max-width: 768px) { + padding: 30px 20px; + border-radius: 15px; + max-width: 100%; + } +} + +.title { + font-size: 32px; + font-weight: 700; + color: #333; + margin-bottom: 10px; + + @media (max-width: 768px) { + font-size: 28px; + } +} + +.subtitle { + font-size: 16px; + color: #666; + margin-bottom: 30px; + + @media (max-width: 768px) { + font-size: 14px; + margin-bottom: 20px; + } +} + +.loading { + padding: 40px 0; + + .spinner { + width: 50px; + height: 50px; + border: 4px solid #f3f3f3; + border-top: 4px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; + } + + p { + color: #666; + } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.error-message { + padding: 20px; + background: #fee; + border-radius: 10px; + color: #c33; + margin: 20px 0; + + p { + margin-bottom: 15px; + } +} + +.qr-section { + margin: 30px 0; +} + +.qr-wrapper { + display: inline-block; + padding: 20px; + background: white; + border-radius: 15px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; + + @media (max-width: 768px) { + padding: 15px; + } + + ::ng-deep canvas { + max-width: 100%; + height: auto !important; + } +} + +.status-indicator { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + color: #667eea; + font-weight: 500; + margin: 20px 0; + + .pulse { + width: 10px; + height: 10px; + background: #667eea; + border-radius: 50%; + animation: pulse 1.5s ease-in-out infinite; + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.5; + transform: scale(1.2); + } +} + +.btn-link { + background: none; + border: none; + color: #667eea; + cursor: pointer; + text-decoration: underline; + font-size: 14px; + padding: 10px; + + &:hover { + color: #764ba2; + } +} + +.btn-secondary { + background: #667eea; + color: white; + border: none; + padding: 12px 30px; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + font-weight: 500; + transition: all 0.3s; + + &:hover { + background: #764ba2; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); + } +} + +.info { + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid #eee; + + p { + font-size: 14px; + color: #999; + line-height: 1.6; + } +} diff --git a/FastCheck/src/app/components/login/login.component.ts b/FastCheck/src/app/components/login/login.component.ts new file mode 100644 index 0000000..8081a2e --- /dev/null +++ b/FastCheck/src/app/components/login/login.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit, OnDestroy, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { Subscription } from 'rxjs'; +import { QRCodeComponent } from 'angularx-qrcode'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [CommonModule, QRCodeComponent], + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) +export class LoginComponent implements OnInit, OnDestroy { + qrData = signal(''); + sessionId = signal(''); + isLoading = signal(true); + error = signal(''); + + private pollSubscription?: Subscription; + + constructor( + private authService: AuthService, + private router: Router + ) {} + + ngOnInit(): void { + this.createSession(); + } + + ngOnDestroy(): void { + this.pollSubscription?.unsubscribe(); + } + + createSession(): void { + this.isLoading.set(true); + this.error.set(''); + + this.authService.createWebSession().subscribe({ + next: (session) => { + this.sessionId.set(session.sessionId); + this.qrData.set(`fastcheck://login?session=${session.sessionId}`); + this.isLoading.set(false); + this.startPolling(session.sessionId); + }, + error: (err) => { + this.error.set('Failed to create session. Please try again.'); + this.isLoading.set(false); + console.error('Session creation error:', err); + } + }); + } + + private startPolling(sessionId: string): void { + this.pollSubscription = this.authService.startPolling(sessionId).subscribe({ + next: (session) => { + if (session.Status) { + this.router.navigate(['/dashboard']); + } + }, + error: (err) => { + this.error.set('Authentication failed. Please try again.'); + console.error('Polling error:', err); + } + }); + } + + refreshQR(): void { + this.pollSubscription?.unsubscribe(); + this.createSession(); + } +} diff --git a/FastCheck/src/app/guards/auth.guard.ts b/FastCheck/src/app/guards/auth.guard.ts new file mode 100644 index 0000000..4a75d1d --- /dev/null +++ b/FastCheck/src/app/guards/auth.guard.ts @@ -0,0 +1,27 @@ +import { inject } from '@angular/core'; +import { Router, CanActivateFn } from '@angular/router'; +import { AuthService } from '../services/auth.service'; + +export const authGuard: CanActivateFn = () => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isAuthenticated().isAuthenticated) { + return true; + } + + router.navigate(['/login']); + return false; +}; + +export const loginGuard: CanActivateFn = () => { + const authService = inject(AuthService); + const router = inject(Router); + + if (!authService.isAuthenticated().isAuthenticated) { + return true; + } + + router.navigate(['/dashboard']); + return false; +}; diff --git a/FastCheck/src/app/models/api.model.ts b/FastCheck/src/app/models/api.model.ts new file mode 100644 index 0000000..76afc7a --- /dev/null +++ b/FastCheck/src/app/models/api.model.ts @@ -0,0 +1,9 @@ +export interface ApiResponse { + data?: T; + message?: string; + error?: string; +} + +export interface PingResponse { + message: string; +} diff --git a/FastCheck/src/app/models/fastcheck.model.ts b/FastCheck/src/app/models/fastcheck.model.ts new file mode 100644 index 0000000..9cd7e02 --- /dev/null +++ b/FastCheck/src/app/models/fastcheck.model.ts @@ -0,0 +1,44 @@ +export interface FastCheck { + fastcheck: string; + amount: number; + currency: string; + code?: string; + expiration: string; + status: 'active' | 'used' | 'expired'; + createdAt?: string; + usedAt?: string; + acceptedAt?: string; + type?: 'created' | 'accepted'; +} + +export interface CreateFastCheckRequest { + amount: number; + currency: string; +} + +export interface CreateFastCheckResponse { + fastcheck: string; + expiration: string; + code: string; + Status: boolean; +} + +export interface AcceptFastCheckRequest { + fastcheck: string; + code: string; +} + +export interface CheckStatusResponse { + fastcheck: string; + expiration: string; + Status: boolean; +} + +export interface Balance { + balance: number; + currency: string; +} + +export interface FastCheckListResponse { + checks: FastCheck[]; +} diff --git a/FastCheck/src/app/models/session.model.ts b/FastCheck/src/app/models/session.model.ts new file mode 100644 index 0000000..e94c5bf --- /dev/null +++ b/FastCheck/src/app/models/session.model.ts @@ -0,0 +1,13 @@ +export interface WebSession { + sessionId: string; + userId: string; + expires: string; + userSessionId: string; + Status: boolean; +} + +export interface AuthState { + isAuthenticated: boolean; + sessionId: string | null; + userSessionId: string | null; +} diff --git a/FastCheck/src/app/services/api.service.ts b/FastCheck/src/app/services/api.service.ts new file mode 100644 index 0000000..10540c3 --- /dev/null +++ b/FastCheck/src/app/services/api.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class ApiService { + private readonly API_URL = 'https://api.fastcheck.store'; + + constructor(private http: HttpClient) {} + + ping(): Observable<{ message: string }> { + return this.http.get<{ message: string }>(`${this.API_URL}/ping`); + } + + get(path: string, sessionId?: string): Observable { + const headers = sessionId ? this.createAuthHeaders(sessionId) : undefined; + return this.http.get(`${this.API_URL}${path}`, { headers }); + } + + post(path: string, body: any, sessionId?: string): Observable { + const headers = sessionId ? this.createAuthHeaders(sessionId) : undefined; + return this.http.post(`${this.API_URL}${path}`, body, { headers }); + } + + delete(path: string, sessionId?: string): Observable { + const headers = sessionId ? this.createAuthHeaders(sessionId) : undefined; + return this.http.delete(`${this.API_URL}${path}`, { headers }); + } + + private createAuthHeaders(sessionId: string): HttpHeaders { + return new HttpHeaders({ + 'Authorization': JSON.stringify({ sessionID: sessionId }), + 'Content-Type': 'application/json' + }); + } +} diff --git a/FastCheck/src/app/services/auth.service.ts b/FastCheck/src/app/services/auth.service.ts new file mode 100644 index 0000000..0fd8c05 --- /dev/null +++ b/FastCheck/src/app/services/auth.service.ts @@ -0,0 +1,77 @@ +import { Injectable, signal } from '@angular/core'; +import { Observable, interval, switchMap, takeWhile, tap } from 'rxjs'; +import { ApiService } from './api.service'; +import { WebSession, AuthState } from '../models/session.model'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private authState = signal({ + isAuthenticated: false, + sessionId: null, + userSessionId: null + }); + + readonly isAuthenticated = this.authState.asReadonly(); + + constructor(private apiService: ApiService) { + this.loadSessionFromStorage(); + } + + createWebSession(): Observable { + return this.apiService.get('/websession'); + } + + checkWebSessionStatus(sessionId: string): Observable { + return this.apiService.get(`/websession/${sessionId}`); + } + + startPolling(sessionId: string): Observable { + return interval(2000).pipe( + switchMap(() => this.checkWebSessionStatus(sessionId)), + tap(session => { + if (session.Status) { + this.setAuthenticated(session); + } + }), + takeWhile(session => !session.Status, true) + ); + } + + deleteWebSession(sessionId: string): Observable { + return this.apiService.delete(`/websession/${sessionId}`, sessionId).pipe( + tap(() => this.clearAuthentication()) + ); + } + + private setAuthenticated(session: WebSession): void { + const state = { + isAuthenticated: true, + sessionId: session.sessionId, + userSessionId: session.userSessionId + }; + this.authState.set(state); + sessionStorage.setItem('authState', JSON.stringify(state)); + } + + private loadSessionFromStorage(): void { + const stored = sessionStorage.getItem('authState'); + if (stored) { + this.authState.set(JSON.parse(stored)); + } + } + + clearAuthentication(): void { + this.authState.set({ + isAuthenticated: false, + sessionId: null, + userSessionId: null + }); + sessionStorage.removeItem('authState'); + } + + getSessionId(): string | null { + return this.authState().sessionId; + } +} diff --git a/FastCheck/src/app/services/fastcheck.service.ts b/FastCheck/src/app/services/fastcheck.service.ts new file mode 100644 index 0000000..78ce70b --- /dev/null +++ b/FastCheck/src/app/services/fastcheck.service.ts @@ -0,0 +1,142 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { ApiService } from './api.service'; +import { AuthService } from './auth.service'; +import { + FastCheck, + CreateFastCheckRequest, + CreateFastCheckResponse, + AcceptFastCheckRequest, + CheckStatusResponse, + Balance, + FastCheckListResponse +} from '../models/fastcheck.model'; + +@Injectable({ + providedIn: 'root' +}) +export class FastCheckService { + constructor( + private apiService: ApiService, + private authService: AuthService + ) {} + + checkStatus(fastcheckNumber: string): Observable { + return this.apiService.post( + '/fastcheck', + { fastcheck: fastcheckNumber } + ); + } + + createFastCheck(request: CreateFastCheckRequest): Observable { + const sessionId = this.authService.getSessionId(); + if (!sessionId) { + throw new Error('Not authenticated'); + } + return this.apiService.post( + '/fastcheck', + request, + sessionId + ); + } + + acceptFastCheck(request: AcceptFastCheckRequest): Observable<{ message: string }> { + const sessionId = this.authService.getSessionId(); + if (!sessionId) { + throw new Error('Not authenticated'); + } + return this.apiService.post<{ message: string }>( + '/fastcheck', + request, + sessionId + ); + } + + // MOCKED - Backend needs to implement + getBalance(): Observable { + const sessionId = this.authService.getSessionId(); + if (!sessionId) { + throw new Error('Not authenticated'); + } + + // TODO: Replace with real API call + // return this.apiService.get('/balance', sessionId); + + // MOCK DATA + return of({ + balance: 150000, + currency: 'RUB' + }); + } + + // MOCKED - Backend needs to implement + getActiveFastChecks(): Observable { + const sessionId = this.authService.getSessionId(); + if (!sessionId) { + throw new Error('Not authenticated'); + } + + // TODO: Replace with real API call + // return this.apiService.get('/fastcheck/active', sessionId); + + // MOCK DATA + return of({ + checks: [ + { + fastcheck: '4568-1109-3402', + amount: 15000, + currency: 'RUB', + code: '5568', + expiration: '2026-01-26T09:08:18Z', + status: 'active', + createdAt: '2026-01-19T09:08:18Z' + }, + { + fastcheck: '7890-2234-5566', + amount: 25000, + currency: 'RUB', + code: '1234', + expiration: '2026-01-26T10:15:30Z', + status: 'active', + createdAt: '2026-01-19T10:15:30Z' + } + ] + }); + } + + // MOCKED - Backend needs to implement + getFastCheckHistory(): Observable { + const sessionId = this.authService.getSessionId(); + if (!sessionId) { + throw new Error('Not authenticated'); + } + + // TODO: Replace with real API call + // return this.apiService.get('/fastcheck/history', sessionId); + + // MOCK DATA + return of({ + checks: [ + { + fastcheck: '1234-5678-0003', + amount: 5000, + currency: 'RUB', + type: 'created', + createdAt: '2026-01-15T09:08:18Z', + usedAt: '2026-01-15T10:20:00Z', + status: 'used', + expiration: '2026-01-22T09:08:18Z' + }, + { + fastcheck: '9876-5432-0100', + amount: 10000, + currency: 'RUB', + type: 'accepted', + acceptedAt: '2026-01-14T14:30:00Z', + status: 'used', + expiration: '2026-01-21T14:30:00Z' + } + ] + }); + } +} diff --git a/FastCheck/src/environments/environment.ts b/FastCheck/src/environments/environment.ts new file mode 100644 index 0000000..12d5e44 --- /dev/null +++ b/FastCheck/src/environments/environment.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + apiUrl: 'https://api.fastcheck.store' +}; diff --git a/FastCheck/src/styles.scss b/FastCheck/src/styles.scss index 90d4ee0..87a73d5 100644 --- a/FastCheck/src/styles.scss +++ b/FastCheck/src/styles.scss @@ -1 +1,21 @@ /* You can add global styles to this file, and also import other style files */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + width: 100%; + margin: 0; + padding: 0; + overflow-x: hidden; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/FastCheck/tsconfig.app.json b/FastCheck/tsconfig.app.json index 264f459..fb792a9 100644 --- a/FastCheck/tsconfig.app.json +++ b/FastCheck/tsconfig.app.json @@ -7,7 +7,8 @@ "types": [] }, "include": [ - "src/**/*.ts" + "src/**/*.ts", + "src/**/*.html" ], "exclude": [ "src/**/*.spec.ts"