first commit

This commit is contained in:
sdarbinyan
2026-01-23 00:52:04 +04:00
parent 975b59c7b9
commit 2e516fa4d3
32 changed files with 3397 additions and 388 deletions

View File

@@ -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
<qrcode [qrdata]="qrData" [width]="250"></qrcode>
```
---
## 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<WebSession> {
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

200
FastCheck/IMPLEMENTATION.md Normal file
View File

@@ -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!

View File

@@ -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

View File

@@ -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"

View File

@@ -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"
},

View File

@@ -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

View File

@@ -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())
]
};

View File

@@ -1,342 +1 @@
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
<!-- * * * * * * * to get started with your project! * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<style>
:host {
--bright-blue: oklch(51.01% 0.274 263.83);
--electric-violet: oklch(53.18% 0.28 296.97);
--french-violet: oklch(47.66% 0.246 305.88);
--vivid-pink: oklch(69.02% 0.277 332.77);
--hot-red: oklch(61.42% 0.238 15.34);
--orange-red: oklch(63.32% 0.24 31.68);
--gray-900: oklch(19.37% 0.006 300.98);
--gray-700: oklch(36.98% 0.014 302.71);
--gray-400: oklch(70.9% 0.015 304.04);
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
180deg,
var(--orange-red) 0%,
var(--vivid-pink) 50%,
var(--electric-violet) 100%
);
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
90deg,
var(--orange-red) 0%,
var(--vivid-pink) 50%,
var(--electric-violet) 100%
);
--pill-accent: var(--bright-blue);
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 {
font-size: 3.125rem;
color: var(--gray-900);
font-weight: 500;
line-height: 100%;
letter-spacing: -0.125rem;
margin: 0;
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
}
p {
margin: 0;
color: var(--gray-700);
}
main {
width: 100%;
min-height: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
box-sizing: inherit;
position: relative;
}
.angular-logo {
max-width: 9.2rem;
}
.content {
display: flex;
justify-content: space-around;
width: 100%;
max-width: 700px;
margin-bottom: 3rem;
}
.content h1 {
margin-top: 1.75rem;
}
.content p {
margin-top: 1.5rem;
}
.divider {
width: 1px;
background: var(--red-to-pink-to-purple-vertical-gradient);
margin-inline: 0.5rem;
}
.pill-group {
display: flex;
flex-direction: column;
align-items: start;
flex-wrap: wrap;
gap: 1.25rem;
}
.pill {
display: flex;
align-items: center;
--pill-accent: var(--bright-blue);
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
color: var(--pill-accent);
padding-inline: 0.75rem;
padding-block: 0.375rem;
border-radius: 2.75rem;
border: 0;
transition: background 0.3s ease;
font-family: var(--inter-font);
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1.4rem;
letter-spacing: -0.00875rem;
text-decoration: none;
white-space: nowrap;
}
.pill:hover {
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
}
.pill-group .pill:nth-child(6n + 1) {
--pill-accent: var(--bright-blue);
}
.pill-group .pill:nth-child(6n + 2) {
--pill-accent: var(--electric-violet);
}
.pill-group .pill:nth-child(6n + 3) {
--pill-accent: var(--french-violet);
}
.pill-group .pill:nth-child(6n + 4),
.pill-group .pill:nth-child(6n + 5),
.pill-group .pill:nth-child(6n + 6) {
--pill-accent: var(--hot-red);
}
.pill-group svg {
margin-inline-start: 0.25rem;
}
.social-links {
display: flex;
align-items: center;
gap: 0.73rem;
margin-top: 1.5rem;
}
.social-links path {
transition: fill 0.3s ease;
fill: var(--gray-400);
}
.social-links a:hover svg path {
fill: var(--gray-900);
}
@media screen and (max-width: 650px) {
.content {
flex-direction: column;
width: max-content;
}
.divider {
height: 1px;
width: 100%;
background: var(--red-to-pink-to-purple-horizontal-gradient);
margin-block: 1.5rem;
}
}
</style>
<main class="main">
<div class="content">
<div class="left-side">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 982 239"
fill="none"
class="angular-logo"
>
<g clip-path="url(#a)">
<path
fill="url(#b)"
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
<path
fill="url(#c)"
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
</g>
<defs>
<radialGradient
id="c"
cx="0"
cy="0"
r="1"
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#FF41F8" />
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5" />
<stop offset="1" stop-color="#FF41F8" stop-opacity="0" />
</radialGradient>
<linearGradient
id="b"
x1="0"
x2="982"
y1="192"
y2="192"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#F0060B" />
<stop offset="0" stop-color="#F0070C" />
<stop offset=".526" stop-color="#CC26D5" />
<stop offset="1" stop-color="#7702FF" />
</linearGradient>
<clipPath id="a"><path fill="#fff" d="M0 0h982v239H0z" /></clipPath>
</defs>
</svg>
<h1>Hello, {{ title() }}</h1>
<p>Congratulations! Your app is running. 🎉</p>
</div>
<div class="divider" role="separator" aria-label="Divider"></div>
<div class="right-side">
<div class="pill-group">
@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) {
<a
class="pill"
[href]="item.link"
target="_blank"
rel="noopener"
>
<span>{{ item.title }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
height="14"
viewBox="0 -960 960 960"
width="14"
fill="currentColor"
>
<path
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
/>
</svg>
</a>
}
</div>
<div class="social-links">
<a
href="https://github.com/angular/angular"
aria-label="Github"
target="_blank"
rel="noopener"
>
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Github"
>
<path
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
/>
</svg>
</a>
<a
href="https://x.com/angular"
aria-label="X"
target="_blank"
rel="noopener"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="X"
>
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
/>
</svg>
</a>
<a
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
aria-label="Youtube"
target="_blank"
rel="noopener"
>
<svg
width="29"
height="20"
viewBox="0 0 29 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Youtube"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
/>
</svg>
</a>
</div>
</div>
</div>
</main>
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<router-outlet />
<router-outlet></router-outlet>

View File

@@ -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'
}
];

View File

@@ -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;
}

View File

@@ -0,0 +1,95 @@
<div class="page-container">
<header class="header">
<div class="logo">FastCheck</div>
<nav class="nav">
<a routerLink="/dashboard" class="nav-link">Dashboard</a>
<a routerLink="/active-checks" class="nav-link active">Active Checks</a>
<a routerLink="/history" class="nav-link">History</a>
</nav>
</header>
<div class="content">
<div class="page-header">
<h1>Active FastChecks</h1>
<p>View all your unused FastChecks</p>
</div>
@if (isLoading()) {
<div class="loading">
<div class="spinner"></div>
<p>Loading active checks...</p>
</div>
}
@if (error()) {
<div class="error-card">
<p>{{ error() }}</p>
<button (click)="loadActiveChecks()" class="btn-retry">Retry</button>
</div>
}
@if (!isLoading() && !error()) {
@if (checks().length === 0) {
<div class="empty-state">
<div class="empty-icon">📭</div>
<h3>No Active Checks</h3>
<p>You don't have any active FastChecks at the moment.</p>
<a routerLink="/dashboard" class="btn-primary">Create FastCheck</a>
</div>
} @else {
<div class="checks-grid">
@for (check of checks(); track check.fastcheck) {
<div class="check-card">
<div class="check-header">
<span class="check-badge">Active</span>
<span class="check-amount">{{ formatAmount(check.amount) }} ₽</span>
</div>
<div class="check-details">
<div class="detail-item">
<span class="label">FastCheck Number</span>
<div class="value-copy">
<span class="value">{{ check.fastcheck }}</span>
<button
(click)="copyToClipboard(check.fastcheck, 'Number')"
class="btn-copy"
title="Copy">
📋
</button>
</div>
</div>
<div class="detail-item">
<span class="label">Code</span>
<div class="value-copy">
<span class="value code">{{ check.code }}</span>
<button
(click)="copyToClipboard(check.code!, 'Code')"
class="btn-copy"
title="Copy">
📋
</button>
</div>
</div>
<div class="detail-item">
<span class="label">Created</span>
<span class="value">{{ check.createdAt | date:'short' }}</span>
</div>
<div class="detail-item">
<span class="label">Expires</span>
<span class="value">{{ check.expiration | date:'short' }}</span>
</div>
</div>
<div class="check-warning">
⚠️ Keep this information secure. Anyone with these credentials can claim the money.
</div>
</div>
}
</div>
}
}
</div>
</div>

View File

@@ -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;
}

View File

@@ -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<FastCheck[]>([]);
isLoading = signal<boolean>(true);
error = signal<string>('');
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!`);
});
}
}

View File

@@ -0,0 +1,142 @@
<div class="dashboard-container">
<!-- Header -->
<header class="header">
<div class="logo">FastCheck</div>
<nav class="nav">
<a routerLink="/dashboard" class="nav-link active">Dashboard</a>
<a routerLink="/active-checks" class="nav-link">Active Checks</a>
<a routerLink="/history" class="nav-link">History</a>
<button (click)="logout()" class="btn-logout">Logout</button>
</nav>
</header>
<div class="content">
<!-- Balance Card -->
<div class="balance-card">
@if (isLoadingBalance()) {
<div class="loading-small">
<div class="spinner-small"></div>
</div>
} @else if (balance()) {
<div class="balance-info">
<span class="balance-label">Current Balance</span>
<h2 class="balance-amount">{{ formatAmount(balance()!.balance) }} ₽</h2>
<button (click)="topUpBalance()" class="btn-topup">+ Top Up Balance</button>
</div>
}
</div>
<div class="actions-grid">
<!-- Create FastCheck -->
<div class="card">
<h3 class="card-title">Create New FastCheck</h3>
<div class="form-group">
<label>Amount (RUB)</label>
<input
type="number"
[(ngModel)]="createAmount"
placeholder="Enter amount"
class="input"
[disabled]="isCreating()">
</div>
@if (createError()) {
<div class="error-message">{{ createError() }}</div>
}
<button
(click)="createFastCheck()"
[disabled]="isCreating() || !createAmount()"
class="btn-primary">
@if (isCreating()) {
<span>Creating...</span>
} @else {
<span>Create FastCheck</span>
}
</button>
</div>
<!-- Accept FastCheck -->
<div class="card">
<h3 class="card-title">Accept FastCheck</h3>
<div class="form-group">
<label>FastCheck Number</label>
<input
type="text"
[value]="acceptNumber()"
(input)="onFastCheckNumberInput($event)"
placeholder="xxxx-xxxx-xxxx"
maxlength="14"
class="input"
[disabled]="isAccepting()">
</div>
<div class="form-group">
<label>Code</label>
<input
type="text"
[(ngModel)]="acceptCode"
placeholder="Enter 4-digit code"
maxlength="4"
class="input"
[disabled]="isAccepting()">
</div>
@if (acceptError()) {
<div class="error-message">{{ acceptError() }}</div>
}
@if (acceptSuccess()) {
<div class="success-message">FastCheck accepted successfully!</div>
}
<button
(click)="acceptFastCheck()"
[disabled]="isAccepting() || !acceptNumber() || !acceptCode()"
class="btn-primary">
@if (isAccepting()) {
<span>Accepting...</span>
} @else {
<span>Accept FastCheck</span>
}
</button>
</div>
</div>
</div>
</div>
<!-- Created Check Modal -->
@if (createdCheck()) {
<div class="modal-overlay" (click)="closeCreatedCheckModal()">
<div class="modal" (click)="$event.stopPropagation()">
<div class="modal-header">
<h3>FastCheck Created!</h3>
<button class="close-btn" (click)="closeCreatedCheckModal()">×</button>
</div>
<div class="modal-body">
<div class="check-details">
<div class="detail-row">
<span class="detail-label">FastCheck Number:</span>
<span class="detail-value">{{ createdCheck()!.fastcheck }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Code:</span>
<span class="detail-value code">{{ createdCheck()!.code }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Expires:</span>
<span class="detail-value">{{ createdCheck()!.expiration | date:'short' }}</span>
</div>
</div>
<div class="modal-note">
<p>⚠️ Save this information securely. Anyone with the number and code can claim this FastCheck.</p>
</div>
</div>
<div class="modal-footer">
<button (click)="closeCreatedCheckModal()" class="btn-primary">Close</button>
</div>
</div>
</div>
}

View File

@@ -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;
}

View File

@@ -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<Balance | null>(null);
isLoadingBalance = signal<boolean>(true);
// Create FastCheck
createAmount = signal<number>(0);
isCreating = signal<boolean>(false);
createdCheck = signal<CreateFastCheckResponse | null>(null);
createError = signal<string>('');
// Accept FastCheck
acceptNumber = signal<string>('');
acceptCode = signal<string>('');
isAccepting = signal<boolean>(false);
acceptSuccess = signal<boolean>(false);
acceptError = signal<string>('');
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.');
}
}

View File

@@ -0,0 +1,86 @@
<div class="page-container">
<header class="header">
<div class="logo">FastCheck</div>
<nav class="nav">
<a routerLink="/dashboard" class="nav-link">Dashboard</a>
<a routerLink="/active-checks" class="nav-link">Active Checks</a>
<a routerLink="/history" class="nav-link active">History</a>
</nav>
</header>
<div class="content">
<div class="page-header">
<h1>Transaction History</h1>
<p>View all used and expired FastChecks</p>
</div>
@if (isLoading()) {
<div class="loading">
<div class="spinner"></div>
<p>Loading history...</p>
</div>
}
@if (error()) {
<div class="error-card">
<p>{{ error() }}</p>
<button (click)="loadHistory()" class="btn-retry">Retry</button>
</div>
}
@if (!isLoading() && !error()) {
@if (checks().length === 0) {
<div class="empty-state">
<div class="empty-icon">📜</div>
<h3>No History</h3>
<p>Your transaction history will appear here.</p>
<a routerLink="/dashboard" class="btn-primary">Go to Dashboard</a>
</div>
} @else {
<div class="history-list">
@for (check of checks(); track check.fastcheck) {
<div class="history-item">
<div class="item-header">
<div class="item-info">
<span [class]="'type-badge ' + getTypeClass(check.type)">
{{ getTypeLabel(check.type) }}
</span>
<span class="item-number">{{ check.fastcheck }}</span>
</div>
<span class="item-amount">{{ formatAmount(check.amount) }} ₽</span>
</div>
<div class="item-details">
@if (check.createdAt) {
<div class="detail">
<span class="detail-label">Created:</span>
<span class="detail-value">{{ check.createdAt | date:'short' }}</span>
</div>
}
@if (check.usedAt) {
<div class="detail">
<span class="detail-label">Used:</span>
<span class="detail-value">{{ check.usedAt | date:'short' }}</span>
</div>
}
@if (check.acceptedAt) {
<div class="detail">
<span class="detail-label">Accepted:</span>
<span class="detail-value">{{ check.acceptedAt | date:'short' }}</span>
</div>
}
<div class="detail">
<span class="detail-label">Status:</span>
<span class="status-badge">{{ check.status }}</span>
</div>
</div>
</div>
}
</div>
}
}
</div>
</div>

View File

@@ -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;
}

View File

@@ -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<FastCheck[]>([]);
isLoading = signal<boolean>(true);
error = signal<string>('');
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';
}
}

View File

@@ -0,0 +1,39 @@
<div class="login-container">
<div class="login-card">
<h1 class="title">FastCheck</h1>
<p class="subtitle">Scan QR code to login</p>
@if (isLoading()) {
<div class="loading">
<div class="spinner"></div>
<p>Generating QR code...</p>
</div>
}
@if (error()) {
<div class="error-message">
<p>{{ error() }}</p>
<button (click)="refreshQR()" class="btn-secondary">Try Again</button>
</div>
}
@if (qrData() && !isLoading()) {
<div class="qr-section">
<div class="qr-wrapper">
<qrcode
[qrdata]="qrData()"
[width]="250"
[errorCorrectionLevel]="'M'">
</qrcode>
</div>
<div class="status-indicator">
<div class="pulse"></div>
<span>Waiting for scan...</span>
</div>
<button (click)="refreshQR()" class="btn-link">Refresh QR Code</button>
</div>
}
</div>
</div>

View File

@@ -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;
}
}

View File

@@ -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<string>('');
sessionId = signal<string>('');
isLoading = signal<boolean>(true);
error = signal<string>('');
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();
}
}

View File

@@ -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;
};

View File

@@ -0,0 +1,9 @@
export interface ApiResponse<T = any> {
data?: T;
message?: string;
error?: string;
}
export interface PingResponse {
message: string;
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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<T>(path: string, sessionId?: string): Observable<T> {
const headers = sessionId ? this.createAuthHeaders(sessionId) : undefined;
return this.http.get<T>(`${this.API_URL}${path}`, { headers });
}
post<T>(path: string, body: any, sessionId?: string): Observable<T> {
const headers = sessionId ? this.createAuthHeaders(sessionId) : undefined;
return this.http.post<T>(`${this.API_URL}${path}`, body, { headers });
}
delete<T>(path: string, sessionId?: string): Observable<T> {
const headers = sessionId ? this.createAuthHeaders(sessionId) : undefined;
return this.http.delete<T>(`${this.API_URL}${path}`, { headers });
}
private createAuthHeaders(sessionId: string): HttpHeaders {
return new HttpHeaders({
'Authorization': JSON.stringify({ sessionID: sessionId }),
'Content-Type': 'application/json'
});
}
}

View File

@@ -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<AuthState>({
isAuthenticated: false,
sessionId: null,
userSessionId: null
});
readonly isAuthenticated = this.authState.asReadonly();
constructor(private apiService: ApiService) {
this.loadSessionFromStorage();
}
createWebSession(): Observable<WebSession> {
return this.apiService.get<WebSession>('/websession');
}
checkWebSessionStatus(sessionId: string): Observable<WebSession> {
return this.apiService.get<WebSession>(`/websession/${sessionId}`);
}
startPolling(sessionId: string): Observable<WebSession> {
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<any> {
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;
}
}

View File

@@ -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<CheckStatusResponse> {
return this.apiService.post<CheckStatusResponse>(
'/fastcheck',
{ fastcheck: fastcheckNumber }
);
}
createFastCheck(request: CreateFastCheckRequest): Observable<CreateFastCheckResponse> {
const sessionId = this.authService.getSessionId();
if (!sessionId) {
throw new Error('Not authenticated');
}
return this.apiService.post<CreateFastCheckResponse>(
'/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<Balance> {
const sessionId = this.authService.getSessionId();
if (!sessionId) {
throw new Error('Not authenticated');
}
// TODO: Replace with real API call
// return this.apiService.get<Balance>('/balance', sessionId);
// MOCK DATA
return of({
balance: 150000,
currency: 'RUB'
});
}
// MOCKED - Backend needs to implement
getActiveFastChecks(): Observable<FastCheckListResponse> {
const sessionId = this.authService.getSessionId();
if (!sessionId) {
throw new Error('Not authenticated');
}
// TODO: Replace with real API call
// return this.apiService.get<FastCheckListResponse>('/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<FastCheckListResponse> {
const sessionId = this.authService.getSessionId();
if (!sessionId) {
throw new Error('Not authenticated');
}
// TODO: Replace with real API call
// return this.apiService.get<FastCheckListResponse>('/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'
}
]
});
}
}

View File

@@ -0,0 +1,4 @@
export const environment = {
production: false,
apiUrl: 'https://api.fastcheck.store'
};

View File

@@ -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;
}

View File

@@ -7,7 +7,8 @@
"types": []
},
"include": [
"src/**/*.ts"
"src/**/*.ts",
"src/**/*.html"
],
"exclude": [
"src/**/*.spec.ts"