diff --git a/docs/TELEGRAM_USERAUTH_BACKEND.md b/docs/TELEGRAM_USERAUTH_BACKEND.md new file mode 100644 index 0000000..902464d --- /dev/null +++ b/docs/TELEGRAM_USERAUTH_BACKEND.md @@ -0,0 +1,327 @@ +# Telegram UserAuth Backend Contract + +This document extracts the existing Telegram login flow into a repo-neutral contract for reuse in other projects. + +The UI behavior, payloads, polling cadence, and session model stay the same. Only route names and cookie naming are generalized. + +## Endpoint Renaming + +| Current app contract | Reusable contract | +|---|---| +| `GET /auth/session` | `GET /userauth/session` | +| `POST /auth/qr/create` | `POST /userauth/qr/create` | +| `GET /auth/qr/poll?token=...` | `GET /userauth/qr/poll?token=...` | +| `POST /auth/qr/confirm` | `POST /userauth/qr/confirm` | +| `GET /auth/telegram/callback` | `GET /userauth/telegram/callback` | +| `POST /auth/logout` | `POST /userauth/logout` | +| `POST /websession/{sessionId}` | `POST /usersession/{sessionId}` | +| Cookie `dx_session` | Cookie `userauth_session` | + +## Flow Summary + +There are two supported flows. + +### 1. Direct login from button + +1. Frontend opens `https://t.me/{botUsername}?start=auth_{callbackUrl}`. +2. Telegram bot creates a session and sends the user a login button. +3. The button points to `GET /userauth/telegram/callback?token={sessionId}`. +4. Backend sets `userauth_session` cookie and redirects back to the storefront. +5. Frontend calls `GET /userauth/session` and becomes authenticated. + +### 2. QR login from desktop + +1. Frontend opens dialog. +2. Frontend calls `POST /userauth/qr/create`. +3. Backend returns `{ token, url }` where `url` is a Telegram deep link. +4. Frontend renders a QR from that URL. +5. User scans QR and bot calls `POST /userauth/qr/confirm`. +6. Frontend polls `GET /userauth/qr/poll?token=...` every 3 seconds. +7. When status becomes `confirmed`, backend returns session payload and sets the cookie. +8. Frontend syncs local cart using `POST /usersession/{sessionId}`. + +## Session Shape + +The frontend expects this exact response shape for the authenticated session. + +```json +{ + "sessionId": "550e8400-e29b-41d4-a716-446655440000", + "telegramUserId": 123456789, + "username": "ivan_petrov", + "displayName": "Ivan Petrov", + "active": true, + "expiresAt": "2026-05-21T14:30:00Z" +} +``` + +| Field | Type | Required | Notes | +|---|---|---|---| +| `sessionId` | string | yes | Session identifier used in cart sync | +| `telegramUserId` | number | yes | Telegram user ID | +| `username` | string or null | no | Telegram username | +| `displayName` | string | yes | User-facing full name | +| `active` | boolean | yes | `false` means expired session | +| `expiresAt` | ISO 8601 string | yes | Used by frontend refresh scheduling | + +Recommended TTL: + +- Session TTL: 24 hours +- QR token TTL: 5 minutes + +## HTTP Contract + +### `POST /userauth/qr/create` + +Creates a one-time QR login token when the dialog opens. + +Request body: + +```json +{} +``` + +Response `200`: + +```json +{ + "token": "dG9rZW4tYWJjMTIz", + "url": "https://t.me/userauth_bot?start=login_dG9rZW4tYWJjMTIz" +} +``` + +Requirements: + +- Generate a cryptographically secure token. +- Save token with status `pending`. +- Return a Telegram deep link in `url`. +- Rate limit to 5 requests per minute per IP. + +### `GET /userauth/qr/poll?token={token}` + +Called every 3 seconds until confirmation or expiration. + +Possible responses: + +Pending: + +```json +{ "status": "pending" } +``` + +Confirmed: + +```json +{ + "status": "confirmed", + "session": { + "sessionId": "550e8400-e29b-41d4-a716-446655440000", + "telegramUserId": 123456789, + "username": "ivan_petrov", + "displayName": "Ivan Petrov", + "active": true, + "expiresAt": "2026-05-21T14:30:00Z" + } +} +``` + +Expired: + +```json +{ "status": "expired" } +``` + +Behavior: + +- If confirmed, set cookie `userauth_session` in the response. +- Delete or invalidate the QR token after the first successful confirmed poll. +- If token is unknown or expired, return `status: "expired"`. + +### `POST /userauth/qr/confirm` + +Internal endpoint called by the Telegram bot after the user scans the QR code. + +Required header: + +```text +X-Bot-Secret: +``` + +Request body: + +```json +{ + "token": "dG9rZW4tYWJjMTIz", + "telegram_user": { + "id": 123456789, + "first_name": "Ivan", + "last_name": "Petrov", + "username": "ivan_petrov" + } +} +``` + +Response `200`: + +```json +{ "status": "ok" } +``` + +Behavior: + +- Validate `X-Bot-Secret`. +- Validate token exists and is still `pending`. +- Create a user session. +- Store session ID on the QR token. +- Mark QR token as `confirmed`. + +### `GET /userauth/session` + +Returns the currently active session based on the cookie. + +Frontend behavior depends on this endpoint in two places: + +- initial auth check on app startup +- fallback polling if QR token creation fails + +Response `200`: + +```json +{ + "sessionId": "550e8400-e29b-41d4-a716-446655440000", + "telegramUserId": 123456789, + "username": "ivan_petrov", + "displayName": "Ivan Petrov", + "active": true, + "expiresAt": "2026-05-21T14:30:00Z" +} +``` + +Error handling: + +- Any non-200 response is treated by the frontend as unauthenticated. + +### `GET /userauth/telegram/callback?token={sessionId}` + +Used for direct Telegram login from the primary button flow. + +Behavior: + +- Read the `token` query param. +- Resolve it to a valid active session. +- Set cookie `userauth_session`. +- Redirect user to the storefront URL. + +### `POST /userauth/logout` + +Clears the backend session and expires the cookie. + +Request body: + +```json +{} +``` + +Response `200`: + +```json +{ "message": "ok" } +``` + +### `POST /usersession/{sessionId}` + +Synchronizes local cart immediately after successful login. + +Request body: + +```json +[ + { + "itemID": 123, + "quantity": 2, + "colour": "#ff0000", + "size": "XL", + "price": 1500 + } +] +``` + +Notes: + +- This payload is unchanged from the existing implementation. +- `price` is already discounted on the frontend side. +- The frontend skips the call if cart is empty. + +## Telegram Deep Link Format + +Direct login link format: + +```text +https://t.me/{botUsername}?start=auth_{urlEncodedCallbackUrl} +``` + +QR login link format: + +```text +https://t.me/{botUsername}?start=login_{qrToken} +``` + +Important limit: + +- Telegram limits the `start` payload to 64 characters. +- A base64url encoding of 32 random bytes plus `login_` fits safely. + +## Cookie Requirements + +Use these cookie settings for the frontend to work correctly across site and API origins. + +| Property | Value | +|---|---| +| Name | `userauth_session` | +| Path | `/` | +| HttpOnly | `true` | +| Secure | `true` | +| SameSite | `None` | +| MaxAge | `86400` | +| Domain | your shared parent domain, for example `.example.com` | + +## CORS Requirements + +Because the frontend sends credentials, backend must return an explicit origin. + +Required headers: + +```text +Access-Control-Allow-Origin: https://your-frontend.example +Access-Control-Allow-Credentials: true +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type +``` + +Do not use `*` for `Access-Control-Allow-Origin` together with credentials. + +## Frontend Runtime Expectations + +The current dialog behavior is fixed and should be preserved by backend responses. + +- QR polling interval: every 3 seconds +- QR expiration on frontend: after 100 checks +- If QR creation fails, frontend falls back to direct login URL and session polling +- After login, frontend closes the dialog and re-checks session + +## Minimal Backend Checklist + +- Implement all six `userauth` endpoints and the `usersession` sync endpoint. +- Store sessions for 24 hours. +- Store QR tokens for 5 minutes. +- Protect `POST /userauth/qr/confirm` with `X-Bot-Secret`. +- Set `userauth_session` cookie on confirmed QR poll and direct callback. +- Return the exact session JSON shape. +- Support credentialed CORS. + +## Bot Checklist + +- Handle `/start login_{token}` and call `POST /userauth/qr/confirm`. +- Handle `/start auth_{callbackUrl}` and provide a button that opens the callback URL. +- Send success and expiration messages back to the user. +- Share the same `X-Bot-Secret` value with backend. \ No newline at end of file diff --git a/docs/telegram-login-dialog.html b/docs/telegram-login-dialog.html new file mode 100644 index 0000000..62c3666 --- /dev/null +++ b/docs/telegram-login-dialog.html @@ -0,0 +1,551 @@ + + + + + + Telegram Login Dialog + + + +
+
+

Telegram Login Dialog

+

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

+ +
+ + + + + +
+ +
+
+ Start QR session + POST /userauth/qr/create +

Returns a one-time token and Telegram deeplink for the QR image.

+
+
+ Poll QR confirmation + GET /userauth/qr/poll?token=... +

Returns pending, confirmed, or expired. On confirmed, also returns the user session.

+
+
+ Read current session + GET /userauth/session +

Cookie-based session lookup used for initial auth check and fallback polling.

+
+
+ Sync cart after login + POST /usersession/{sessionId} +

Existing cart payload is preserved. Only the namespace is generalized for reuse.

+
+
+ + +
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/files/changes.txt b/files/changes.txt index d3ffc5c..8d55943 100644 --- a/files/changes.txt +++ b/files/changes.txt @@ -1,144 +1,24 @@ - bro we need to add another project, under another port. it must be the same as dexar - here are infos: - colors: #FD7300 #3AAA35 #E24B00 - 1. lavero.store - 2. info@lavero.storre - 3. logo - public\assets\images\lavero\lavero-logo.png +bro please read carefully. +this must be for all projects. +At first here is the API for only auth process: +https://users.vitanova.network:456/ping + +and here are other stuff regarding it: + //Logout user by sessionID + r.DELETE("/users/sessions/:webSessionID", Logout) + + //creates new session for user and send code for activation + r.POST("/users/sessions", newWebSession) + + r.GET("/users/sessions/:webSessionID", getWebSession) + +As you got all the info, keep all the structure of api above. +Now when the user clicks on login, we must show the QR and the link of the telegram bot, which we already have (btw sho me, so i see wheter it is true or not) +and add a query param "?start=GUID" and generate a guid there. +after we post it ad a websession, we have to get it like this " r.GET("/users/sessions/:webSessionID", getWebSession)" every 5 secs untill we get a status true +if we will be loged in, we have to keep that webSessionID in the cookies for an hour +1. if we open our website, we have to check the cookies and do a request for that websession +2. if we are not loged in, then we will loge in one more time - info for legal: - "ՆՇԱԹԵՐԹ -պետական գրանցման -մասին տեղեկությունների -«ԼԱՎԵՐՈ» -Սահմանափակ -պատասխանատվությամբ ընկերություն -Գրանցված է Հայաստանի Հանրապետության -արդարադատության նախարարության իրավաբանական -անձանց պետական ռեգիստրի գործակալության կողմից -Գրանցված է` -Աշխատակից՝ -ՀՀ ԱՆ իրավաբանական անձանց պետական -ռեգիստրի գործակալություն -Ինքնաշխատ էլեկտրոնային գրանցում -Գրանցման ամսաթիվ՝ -Գրանցման համար՝ -ՀՎՀՀ՝ -18.05.2026 -999.110.1583686 -03590442 -Պետական գրանցման մասին տեղեկությունների նշաթերթը էլեկտրոնային եղանակով ձևավորված է ՀՀ ԱՆ -իրավաբանական անձանց պետական ռեգիստրի տեղեկատվական համակարգի կողմից և հանդիսանում է -«ԼԱՎԵՐՈ» Սահմանափակ պատասխանատվությամբ ընկերություն կանոնադրության անբաժանելի մասը։ -Սույն կանոնադրության գրանցումը հավաստվում է համակարգի կողմից գեներացված RBBE-3D66 -EF54-3DAE ծածկագրով քաղվածքով, որի իսկությունը և արդիականությունը կարող է ստուգվել -փաստաթղթերի վավերականության ստուգման միասնական համակարգում (e-verify.am): -Սույն կանոնադրությունը կազմված է 2026-05-18 և բաղկացած է 4 թերթից: -Հ Ա Ս Տ Ա Տ Վ Ա Ծ Է -«ԼԱՎԵՐՈ» -սահմանափակ -պատասխանատվությամբ -ընկերության հիմնադիրների -2026-05-18 կայացրած -Հիմնադիր ժողովի որոշմամբ -արձանագրություն թիվ` 1 -տնօրեն` ԳԵՎՈՐԳ ՄԱԹԵՎՈՍՅԱՆ -Գ Ր Ա Ն Ց Վ Ա Ծ Է -ՀԱՅԱՍՏԱՆԻ ՀԱՆՐԱՊԵՏՈՒԹՅԱՆ -ԱՐԴԱՐԱԴԱՏՈՒԹՅԱՆ ՆԱԽԱՐԱՐՈՒԹՅԱՆ -ԻՐԱՎԱԲԱՆԱԿԱՆ ԱՆՁԱՆՑ -ՊԵՏԱԿԱՆ ՌԵԳԻՍՏՐԻ ԳՈՐԾԱԿԱԼՈՒԹՅԱՆ -ԿՈՂՄԻՑ -Աշխատակից՝ ՀՀ ԻԱ Պետական ռեգիստրի էլ․ -համակարգ -Գրանցման հ/հ՝ - 999.110.1583686 -Գրանցման ամսաթիվ՝ 18.05.2026 -Հարկ վճարողի հաշվառման -համար (ՀՎՀՀ)` 03590442 -«ԼԱՎԵՐՈ» -ՍԱՀՄԱՆԱՓԱԿ ՊԱՏԱՍԽԱՆԱՏՎՈՒԹՅԱՄԲ ԸՆԿԵՐՈՒԹՅԱՆ -ԿԱՆՈՆԱԴՐՈՒԹՅՈՒՆ -2026 -Էջ 1 4-ից -Սույն կանոնադրությունը կազմված է 2026-05-18 և բաղկացած է 4 թերթից: -1. ԸՆԴՀԱՆՈՒՐ ԴՐՈՒՅԹՆԵՐ -1.1. «ԼԱՎԵՐՈ» սահմանափակ պատասխանատվությամբ ընկերությունը (հետագայում` Ընկերություն) -համարվում է շահույթ ստանալու նպատակով հիմնադրված առևտրային կազմակերպություն -հանդիսացող իրավաբանական անձ, որի կանոնադրական կապիտալը բաժանված է սույն -կանոնադրությամբ սահմանված չափերով բաժնեմասերի: -1.2. Ընկերությունն իր գործունեության ընթացքում ղեկավարվում է Հայաստանի Հանրապետության -Քաղաքացիական Օրենսդրությամբ, այլ իրավական ակտերով և սույն կանոնադրությամբ: -1.3. Ընկերության ֆիրմային անվանումն է` -հայերեն լրիվ՝ «ԼԱՎԵՐՈ» սահմանափակ պատասխանատվությամբ ընկերություն -կրճատ՝ «ԼԱՎԵՐՈ» ՍՊԸ -ռուսերեն լրիվ՝ «ЛАВЕРО» Общество с Ограничеенной Ответственностью -կրճատ՝ «ЛАВЕРО» ՕՕՕ -անգլերեն լրիվ՝ «LAVERO» LIMITED LIABILITY COMPANY -կրճատ՝ «LAVERO» LLC -1.4. Ընկերության գտնվելու վայրը և իրավաբանական (փոստային) հասցեն է` ԿՈՏԱՅՔ, ԱԲՈՎՅԱՆ, ՎԵՐԻՆ -ՊՏՂՆԻ, 3-րդ փողոց, 28, Տ Փ/Դ՝ 2228: -2. ԸՆԿԵՐՈՒԹՅԱՆ ՄԱՍՆԱԿԻՑՆԵՐԸ, ՆՐԱՆՑ ԻՐԱՎՈՒՆՔՆԵՐՆ ՈՒ ՊԱՐՏԱԿԱՆՈՒԹՅՈՒՆՆԵՐԸ -2.1. Անձը համարվում է ընկերության մասնակից իրավաբանական անձանց պետական գրանցումն -իրականացնող մարմնի կողմից Ընկերության մասնակիցների գրանցամատյանում նրա որպես այդպիսին -գրանցվելու պահից: -2.2. Ընկերության կանոնադրական կապիտալում բաժնեմասը փոխանցելը. -2.2.1.Ընկերության մասնակիցն իրավունք ունի իր բաժնեմասը (դրա մասը) վաճառել կամ այլ` օրենքով -չարգելված ձևով, օտարել Ընկերության մեկ կամ մի քանի մասնակիցների, ինչպես նաև երրորդ անձանց: -Ընդ որում, մյուս մասնակիցներն ունեն այդ բաժնեմասը նույն գնով ձեռքբերման նախապատվության -իրավունք՝ Ընկերության կանոնադրական կապիտալում իրենց բաժնեմասերին համամասնորեն: Եթե -Ընկերության մյուս մասնակիցները չեն օգտագործել բաժնեմասը (դրա մասը) գնելու իրենց -նախապատվության իրավունքը կամ այն օգտագործել են մասամբ, ապա Ընկերությունն ունի այդ -բաժնեմասը նույն գնով ձեռքբերման նախապատվության իրավունք: -2.2.2.Երրորդ անձանց օտարման անհնարինության դեպքում մասնակցի պահանջով Ընկերությունն ինքը -պարտավոր է ձեռք բերել մասնակցի բաժնեմասը, այն իրացնել մեկ տարվա ընթացքում այլ -մասնակիցների կամ երրորդ անձանց կամ որոշում ընդունել բաժնեմասի չբաշխված մասի մարման -ճանապարհով Ընկերության կանոնադրական կապիտալի նվազեցման մասին: -2.2.3.Նախապատվության իրավունքի (այլ մասնակիցների կողմից վաճառվող բաժնեմասերի ձեռքբերման) -իրականացման ժամկետը սահմանվում է մեկ ամիս: -2.3. Ընկերությունից դուրս եկող մասնակցի բաժնեմասը դուրս գալու դիմումը ներկայացնելու պահից -փոխանցվում է ընկերությանը: -2.4. Ընկերության մասնակիցները կարող են տեղեկություններ ստանալ, ընկերության գործունեության մասին -գաղտնի փաստաթղթերից բացի, ծանոթանալ հաշվապահական հաշվառման, հաշվետվության, -ընկերության արտադրատնտեսական գործունեության այլ փաստաթղթերի հետ: -2.5. Ընկերության մասնակիցները պարտավոր են`– Ընկերության մասնակիցների ընդհանուր ժողովի սահմանված կարգի համաձայն կատարել -ներդրումներ Ընկերության կանոնադրական կապիտալում: -3. ԸՆԿԵՐՈՒԹՅԱՆ ԿԱՆՈՆԱԴՐԱԿԱՆ ԿԱՊԻՏԱԼԸ -3.1. Ընկերության կանոնադրական կապիտալը կազմում է 1000 դրամ: -Կանոնադրական կապիտալը սահմանում է պարտատերերի շահերը երաշխավորող ընկերության գույքի -նվազագույն չափը: -Ընկերության 100% բաժնեմասերը տեղաբաշխված են, լրիվ վճարված և պատկանում են սույն -կանոնադրության հավելվածում նշված մասնակցին (մասնակիցներին): -Էջ 2 4-ից -Սույն կանոնադրությունը կազմված է 2026-05-18 և բաղկացած է 4 թերթից: -4. ԸՆԿԵՐՈՒԹՅԱՆ ԿԱՌԱՎԱՐՈՒՄԸ -4.1. Ընկերության կառավարման մարմիններն են Ընկերության մասնակիցների ընդհանուր ժողովը և -Ընկերության գործադիր մարմինը՝ տնօրենը: -4.2. Ընկերության կառավարման բարձրագույն մարմինը Ընկերության մասնակիցների ընդհանուր ժողովն է, -որն ունի Ընկերության կառավարման և գործունեության ցանկացած հարցի վերջնական լուծման -իրավունք: -4.3. Ընկերության մասնակիցների հերթական ընդհանուր ժողովը հրավիրում է Ընկերության տնօրենի կողմից -տարեկան մեկ անգամ ֆինանսական տարվա ավարտից ոչ շուտ, քան երկու ամիս ոչ ուշ, քան չորս ամիս -անց: Ընկերությունը պարտավոր է ամեն տարի գումարել մասնակիցների հերթական ընդհանուր ժողովը` -Ընկերության գործունեության տարեկան արդյունքները հաստատելու համար: -4.4. Ընկերության ընթացիկ գործունեության ղեկավարումն իրականացնում է տնօրենը, որն ընտրվում է -Ընկերության մասնակիցների ընդհանուր ժողովի կողմից: -Տնօրենը իրավունք ունի առանց լիազորության գործարքներ կատարել Ընկերության անունից, եթե -գործարքի գումարը չի գերազանցում ընկերության զուտ ակտիվների մեծության 25%-ից: -Էջ 3 4-ից -Սույն կանոնադրությունը կազմված է 2026-05-18 և բաղկացած է 4 թերթից: -«ԼԱՎԵՐՈ» -ՍԱՀՄԱՆԱՓԱԿ ՊԱՏԱՍԽԱՆԱՏՎՈՒԹՅԱՄԲ ԸՆԿԵՐՈՒԹՅԱՆ -մասնակիցների ցուցակ -Մասնակցի քաղաքացիությունը, անունը, ազգանունը (պետությունը -որտեղ հիմնադրվել է, անվանումը) անձնագրի(գրանցման) տվյալները -հասցեն (գտնվելու վայրը) -Բաժնեմասի -չափը - % -Բաժնեմասը -ՀՀ դրամով -ԳԵՎՈՐԳ ՄԱԹԵՎՈՍՅԱՆ ՀԱՄԼԵՏԻ -Քաղաքացիություն՝ ՀԱՅԱՍՏԱՆ -ՀԾՀ` 2723060837 -ՀԱՅԱՍՏԱՆ ԿՈՏԱՅՔ ԱԲՈՎՅԱՆ ՎԵՐԻՆ ՊՏՂՆԻ 3 Փ. Տ 28 2228 -100.0 % 1000.0 -Էջ 4 4-ից" \ No newline at end of file +any questions? diff --git a/src/app/components/telegram-login/telegram-login.component.html b/src/app/components/telegram-login/telegram-login.component.html index efbc8b4..2127fae 100644 --- a/src/app/components/telegram-login/telegram-login.component.html +++ b/src/app/components/telegram-login/telegram-login.component.html @@ -29,6 +29,12 @@ {{ 'auth.loginWithTelegram' | translate }} + @if (loginUrl()) { + + {{ loginUrl() }} + + } +

{{ 'auth.orScanQr' | translate }}

@@ -57,12 +63,12 @@
} @case ('error') { -
- QR Code +
+ + + + + {{ 'auth.qrError' | translate }}
} } diff --git a/src/app/components/telegram-login/telegram-login.component.scss b/src/app/components/telegram-login/telegram-login.component.scss index 2e7704d..a1302ad 100644 --- a/src/app/components/telegram-login/telegram-login.component.scss +++ b/src/app/components/telegram-login/telegram-login.component.scss @@ -102,6 +102,20 @@ h2 { } } +.bot-link { + display: block; + margin-top: 10px; + color: var(--accent-color, #497671); + font-size: 12px; + line-height: 1.35; + overflow-wrap: anywhere; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + .qr-section { margin-top: 20px; @@ -158,6 +172,26 @@ h2 { font-size: 13px; } } + + &.qr-error { + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + width: 204px; + height: 204px; + cursor: pointer; + color: var(--text-secondary, #999); + transition: color 0.2s ease; + + &:hover { + color: var(--accent-color, #497671); + } + + span { + font-size: 13px; + } + } } } diff --git a/src/app/components/telegram-login/telegram-login.component.ts b/src/app/components/telegram-login/telegram-login.component.ts index e716cdd..b206314 100644 --- a/src/app/components/telegram-login/telegram-login.component.ts +++ b/src/app/components/telegram-login/telegram-login.component.ts @@ -19,10 +19,11 @@ export class TelegramLoginComponent implements OnDestroy { status = this.authService.status; loginUrl = signal(''); - qrToken = signal(''); + webSessionID = signal(''); qrStatus = signal<'loading' | 'ready' | 'expired' | 'error'>('loading'); encodedQrUrl = computed(() => encodeURIComponent(this.loginUrl())); + private readonly pollIntervalMs = 5000; private pollTimer?: ReturnType; constructor() { @@ -45,12 +46,14 @@ export class TelegramLoginComponent implements OnDestroy { } openTelegramLogin(): void { - window.open(this.loginUrl(), '_blank'); + const url = this.loginUrl(); + if (!url) return; + + window.open(url, '_blank'); if (!this.pollTimer) { - if (this.qrToken()) { - this.startPolling(this.qrToken()); - } else { - this.startSessionPolling(); + const webSessionID = this.webSessionID(); + if (webSessionID) { + this.startPolling(webSessionID); } } } @@ -62,43 +65,25 @@ export class TelegramLoginComponent implements OnDestroy { private initQrLogin(): void { this.qrStatus.set('loading'); - this.authService.createQrToken().subscribe({ + this.loginUrl.set(''); + this.webSessionID.set(''); + + this.authService.createWebSession().subscribe({ next: (res) => { this.loginUrl.set(res.url); - this.qrToken.set(res.token); + this.webSessionID.set(res.webSessionID); this.qrStatus.set('ready'); - this.startPolling(res.token); + this.startPolling(res.webSessionID); }, error: () => { - this.loginUrl.set(this.authService.getTelegramLoginUrl()); this.qrStatus.set('error'); - this.startSessionPolling(); } }); } - private startSessionPolling(): void { + private startPolling(webSessionID: string): void { this.stopPolling(); - let checks = 0; - this.pollTimer = setInterval(() => { - checks++; - if (checks > 100) { - this.stopPolling(); - this.qrStatus.set('expired'); - return; - } - this.authService.checkSessionOnce().subscribe(session => { - if (session && session.active) { - this.stopPolling(); - this.syncCartAndComplete(session.sessionId); - } - }); - }, 3000); - } - - private startPolling(token: string): void { - this.stopPolling(); - if (!token) return; + if (!webSessionID) return; let checks = 0; this.pollTimer = setInterval(() => { @@ -109,28 +94,18 @@ export class TelegramLoginComponent implements OnDestroy { return; } - this.authService.pollQrToken(token).subscribe({ - next: (res) => { - switch (res.status) { - case 'confirmed': - this.stopPolling(); - if (res.session) { - this.syncCartAndComplete(res.session.sessionId); - } else { - this.authService.onTelegramLoginComplete(); - } - break; - case 'expired': - this.stopPolling(); - this.qrStatus.set('expired'); - break; + this.authService.checkSessionOnce(webSessionID).subscribe({ + next: (session) => { + if (session?.active) { + this.stopPolling(); + this.syncCartAndComplete(session.sessionId); } }, error: () => { // Network error — keep polling } }); - }, 3000); + }, this.pollIntervalMs); } private syncCartAndComplete(sessionId: string): void { diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index 01a04a9..01a752d 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -206,5 +206,6 @@ export const en: Translations = { orScanQr: 'Or scan the QR code', loginNote: 'You will be redirected back after login', qrExpired: 'QR code expired. Click to refresh', + qrError: 'Could not create login session. Click to retry', }, }; diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 177a283..b39ee40 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -206,5 +206,6 @@ export const hy: Translations = { orScanQr: 'Կամ սքանավորեք QR կոդը', loginNote: 'Մուտքից հետո դուք կվերաուղղվեք', qrExpired: 'QR կոդը հնացել է։ Սեղմեք՝ թարմացնելու համար', + qrError: 'Չհաջողվեց ստեղծել մուտքի սեսիա։ Սեղմեք՝ կրկնելու համար', }, }; diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 44a452c..0fbe8d5 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -206,5 +206,6 @@ export const ru: Translations = { orScanQr: 'Или отсканируйте QR-код', loginNote: 'После входа вы будете перенаправлены обратно', qrExpired: 'QR-код устарел. Нажмите, чтобы обновить', + qrError: 'Не удалось создать сессию входа. Нажмите, чтобы повторить', }, }; diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 4899df8..2fe975b 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -204,5 +204,6 @@ export interface Translations { orScanQr: string; loginNote: string; qrExpired: string; + qrError: string; }; } diff --git a/src/app/interceptors/mock-data.interceptor.ts b/src/app/interceptors/mock-data.interceptor.ts index 83e522d..7ea18ce 100644 --- a/src/app/interceptors/mock-data.interceptor.ts +++ b/src/app/interceptors/mock-data.interceptor.ts @@ -672,7 +672,7 @@ function respond(body: T, delayMs = 150) { } // ─── Mock Auth State ─── -let mockQrPollCount = 0; +const mockWebSessionChecks = new Map(); // ─── The Interceptor ─── @@ -688,38 +688,39 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => { return respond({ message: 'pong (mock)' }); } - // ── GET /auth/session - if (url.includes('/auth/session') && req.method === 'GET') { - return respond({ active: false }, 100); + // ── POST /users/sessions + if (url.endsWith('/users/sessions') && req.method === 'POST') { + const body = req.body as { webSessionID?: string } | null; + const webSessionID = body?.webSessionID || req.headers.get('WebSessionID') || 'mock-web-session'; + mockWebSessionChecks.set(webSessionID, 0); + return respond({ webSessionID, status: false }, 200); } - // ── POST /auth/qr/create - if (url.includes('/auth/qr/create') && req.method === 'POST') { - const token = 'mock-qr-token-' + Date.now(); - const botUsername = (environment as Record)['telegramBot'] as string || 'DexarSupport_bot'; - mockQrPollCount = 0; - return respond({ - token, - url: `https://t.me/${botUsername}?start=qr_${token}` - }, 200); - } + // ── GET /users/sessions/:webSessionID + const userSessionMatch = url.match(/\/users\/sessions\/([^/?]+)$/); + if (userSessionMatch && req.method === 'GET') { + const webSessionID = decodeURIComponent(userSessionMatch[1]); + const checks = (mockWebSessionChecks.get(webSessionID) ?? 0) + 1; + mockWebSessionChecks.set(webSessionID, checks); - // ── GET /auth/qr/poll - if (url.includes('/auth/qr/poll') && req.method === 'GET') { - mockQrPollCount++; - // Simulate confirmed after 3 polls (~9 seconds) - if (mockQrPollCount >= 3) { + if (checks >= 3) { return respond({ - status: 'confirmed', - session: { - sessionId: 'mock-session-' + Date.now(), - active: true, - displayName: 'Telegram User', - expiresAt: new Date(Date.now() + 3600000).toISOString() - } + webSessionID, + status: true, + telegramUserID: 123456, + username: 'telegram_user', + displayName: 'Telegram User', + expiresAt: new Date(Date.now() + 3600000).toISOString() }, 200); } - return respond({ status: 'pending' }, 200); + + return respond({ webSessionID, status: false }, 200); + } + + // ── DELETE /users/sessions/:webSessionID + if (userSessionMatch && req.method === 'DELETE') { + mockWebSessionChecks.delete(decodeURIComponent(userSessionMatch[1])); + return respond({ status: true }, 100); } // ── GET /category (all categories flat list) diff --git a/src/app/models/auth.model.ts b/src/app/models/auth.model.ts index 0f1cc0d..cf5f1cf 100644 --- a/src/app/models/auth.model.ts +++ b/src/app/models/auth.model.ts @@ -7,6 +7,11 @@ export interface AuthSession { expiresAt: string; } +export interface WebSessionStart { + webSessionID: string; + url: string; +} + export interface TelegramAuthData { id: number; first_name: string; @@ -17,9 +22,4 @@ export interface TelegramAuthData { hash: string; } -export interface QrPollResponse { - status: 'pending' | 'confirmed' | 'expired'; - session?: AuthSession; -} - export type AuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated'; diff --git a/src/app/pages/cart/cart.component.html b/src/app/pages/cart/cart.component.html index ad5406e..21e91aa 100644 --- a/src/app/pages/cart/cart.component.html +++ b/src/app/pages/cart/cart.component.html @@ -169,17 +169,6 @@ {{ 'cart.loginWithTelegram' | translate }} - -
} diff --git a/src/app/pages/cart/cart.component.scss b/src/app/pages/cart/cart.component.scss index caa54b3..e34349c 100644 --- a/src/app/pages/cart/cart.component.scss +++ b/src/app/pages/cart/cart.component.scss @@ -802,29 +802,6 @@ box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3); } } - - .login-gate-qr { - margin-top: 14px; - - .qr-hint { - margin: 0 0 8px; - font-size: 0.8rem; - color: #999; - } - - .qr-wrapper { - display: inline-flex; - padding: 10px; - background: #fff; - border-radius: 10px; - border: 1px solid #e5e7eb; - - img { - display: block; - border-radius: 4px; - } - } - } } } @@ -967,29 +944,6 @@ box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3); } } - - .login-gate-qr { - margin-top: 14px; - - .qr-hint { - margin: 0 0 8px; - font-size: 0.8rem; - color: #9ca3af; - } - - .qr-wrapper { - display: inline-flex; - padding: 10px; - background: #fff; - border-radius: 12px; - border: 1px solid #e5e7eb; - - img { - display: block; - border-radius: 4px; - } - } - } } } diff --git a/src/app/pages/cart/cart.component.ts b/src/app/pages/cart/cart.component.ts index c410b87..1d9e0e0 100644 --- a/src/app/pages/cart/cart.component.ts +++ b/src/app/pages/cart/cart.component.ts @@ -33,7 +33,6 @@ export class CartComponent implements OnDestroy { private authService = inject(AuthService); isAuthenticated = this.authService.isAuthenticated; - loginUrl = signal(''); // Swipe state swipedItemId = signal(null); @@ -69,7 +68,6 @@ export class CartComponent implements OnDestroy { this.items = this.cartService.items; this.itemCount = this.cartService.itemCount; this.totalPrice = this.cartService.totalPrice; - this.loginUrl.set(this.authService.getTelegramLoginUrl()); } requestLogin(): void { diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index c9b65f3..a0736a6 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -1,9 +1,12 @@ import { Injectable, signal, computed } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of, catchError, map, tap } from 'rxjs'; -import { AuthSession, AuthStatus, QrPollResponse } from '../models/auth.model'; +import { AuthSession, AuthStatus, WebSessionStart } from '../models/auth.model'; import { environment } from '../../environments/environment'; +const WEB_SESSION_COOKIE = 'webSessionID'; +const WEB_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60; + @Injectable({ providedIn: 'root' }) @@ -24,52 +27,45 @@ export class AuthService { readonly displayName = computed(() => this.sessionSignal()?.displayName ?? null); private readonly apiUrl = environment.apiUrl; - private sessionCheckTimer?: ReturnType; + private readonly authApiUrl = environment.authApiUrl; + private sessionCheckTimer?: ReturnType; constructor(private http: HttpClient) { // On init, check existing session via cookie this.checkSession(); } - /** - * Check current session status with backend. - * The backend reads the session cookie and returns the session info. - */ + /** Check the current webSessionID cookie against the auth backend. */ checkSession(): void { + const webSessionID = this.getStoredWebSessionID(); + + if (!webSessionID) { + this.clearAuthState('unauthenticated'); + return; + } + this.statusSignal.set('checking'); - this.http.get(`${this.apiUrl}/auth/session`, { - withCredentials: true - }).pipe( - catchError(() => { - this.statusSignal.set('unauthenticated'); - this.sessionSignal.set(null); - return of(null); - }) - ).subscribe(session => { - if (session && session.active) { - this.sessionSignal.set(session); - this.statusSignal.set('authenticated'); - this.scheduleSessionRefresh(session.expiresAt); - } else if (session && !session.active) { - this.sessionSignal.set(null); - this.statusSignal.set('expired'); - } else { - this.statusSignal.set('unauthenticated'); + this.checkSessionOnce(webSessionID).subscribe(session => { + if (!session?.active) { + this.clearAuthState('unauthenticated'); } }); } /** Check session without updating internal state (for polling) */ - checkSessionOnce(): Observable { - return this.http.get(`${this.apiUrl}/auth/session`, { - withCredentials: true - }).pipe( + checkSessionOnce(webSessionID = this.getStoredWebSessionID()): Observable { + if (!webSessionID) { + return of(null); + } + + return this.http.get>( + `${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}` + ).pipe( + map(response => this.normalizeWebSession(response, webSessionID)), tap(session => { - if (session && session.active) { - this.sessionSignal.set(session); - this.statusSignal.set('authenticated'); - this.scheduleSessionRefresh(session.expiresAt); + if (session?.active) { + this.activateSession(session); } }), catchError(() => of(null)) @@ -78,19 +74,19 @@ export class AuthService { /** * Called after user completes Telegram login. - * The callback URL from Telegram will hit our backend which sets the cookie. - * Then we re-check the session. */ onTelegramLoginComplete(): void { - this.checkSession(); this.hideLogin(); + + if (!this.isAuthenticated()) { + this.checkSession(); + } } /** Generate the Telegram login URL for bot-based auth */ - getTelegramLoginUrl(): string { + getTelegramLoginUrl(webSessionID = this.generateGuid()): string { const botUsername = (environment as Record)['telegramBot'] as string || 'DexarSupport_bot'; - const callbackUrl = encodeURIComponent(`${this.apiUrl}/auth/telegram/callback`); - return `https://t.me/${botUsername}?start=auth_${callbackUrl}`; + return `https://t.me/${botUsername}?start=${encodeURIComponent(webSessionID)}`; } /** Get QR code data URL for Telegram login */ @@ -98,23 +94,22 @@ export class AuthService { return this.getTelegramLoginUrl(); } - /** Create a one-time QR login token via backend */ - createQrToken(): Observable<{ token: string; url: string }> { - return this.http.post<{ token: string; url: string }>( - `${this.apiUrl}/auth/qr/create`, - {}, - { withCredentials: true } - ); - } + /** Create a backend web session and return the Telegram start link for it. */ + createWebSession(): Observable { + const webSessionID = this.generateGuid(); - /** Poll the QR token status (pending → confirmed / expired) */ - pollQrToken(token: string): Observable { - return this.http.get( - `${this.apiUrl}/auth/qr/poll`, - { - params: { token }, - withCredentials: true, - } + return this.http.post>( + `${this.authApiUrl}/users/sessions`, + { webSessionID }, + { headers: { WebSessionID: webSessionID } } + ).pipe( + map(response => { + const responseWebSessionID = this.extractSessionId(response, webSessionID); + return { + webSessionID: responseWebSessionID, + url: this.getTelegramLoginUrl(responseWebSessionID), + }; + }) ); } @@ -138,17 +133,36 @@ export class AuthService { /** Logout — clears session on backend and locally */ logout(): void { - this.http.post(`${this.apiUrl}/auth/logout`, {}, { - withCredentials: true + const webSessionID = this.sessionSignal()?.sessionId || this.getStoredWebSessionID(); + + if (!webSessionID) { + this.clearAuthState('unauthenticated'); + return; + } + + this.http.delete(`${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}`, { + headers: { WebSessionID: webSessionID } }).pipe( catchError(() => of(null)) ).subscribe(() => { - this.sessionSignal.set(null); - this.statusSignal.set('unauthenticated'); - this.clearSessionRefresh(); + this.clearAuthState('unauthenticated'); }); } + private activateSession(session: AuthSession): void { + this.sessionSignal.set(session); + this.statusSignal.set('authenticated'); + this.setStoredWebSessionID(session.sessionId); + this.scheduleSessionRefresh(session.expiresAt); + } + + private clearAuthState(status: AuthStatus): void { + this.sessionSignal.set(null); + this.statusSignal.set(status); + this.clearStoredWebSessionID(); + this.clearSessionRefresh(); + } + /** Schedule a session re-check before it expires */ private scheduleSessionRefresh(expiresAt: string): void { this.clearSessionRefresh(); @@ -156,7 +170,9 @@ export class AuthService { const expiresMs = new Date(expiresAt).getTime(); const nowMs = Date.now(); // Re-check 60 seconds before expiry, minimum 30s from now - const refreshIn = Math.max(expiresMs - nowMs - 60_000, 30_000); + const refreshIn = Number.isFinite(expiresMs) + ? Math.max(expiresMs - nowMs - 60_000, 30_000) + : WEB_SESSION_COOKIE_MAX_AGE_SECONDS * 1000; this.sessionCheckTimer = setTimeout(() => { this.checkSession(); @@ -169,4 +185,176 @@ export class AuthService { this.sessionCheckTimer = undefined; } } + + private normalizeWebSession(response: Record | null, fallbackSessionId: string): AuthSession | null { + if (!response) { + return null; + } + + const user = this.asRecord(this.readFirst(response, ['user', 'User', 'telegramUser', 'TelegramUser'])) ?? response; + const status = this.readFirst(response, [ + 'status', + 'Status', + 'active', + 'Active', + 'loggedIn', + 'LoggedIn', + 'isLoggedIn', + 'IsLoggedIn', + 'authenticated', + 'Authenticated' + ]); + const active = this.isActiveStatus(status); + const sessionId = this.extractSessionId(response, fallbackSessionId); + const username = this.readString(this.readFirst(user, ['username', 'Username'])) + ?? this.readString(this.readFirst(response, ['username', 'Username'])); + const firstName = this.readString(this.readFirst(user, ['firstName', 'first_name', 'FirstName', 'First_name'])); + const lastName = this.readString(this.readFirst(user, ['lastName', 'last_name', 'LastName', 'Last_name'])); + const fullName = [firstName, lastName].filter(Boolean).join(' '); + const explicitDisplayName = this.readString(this.readFirst(response, ['displayName', 'DisplayName', 'name', 'Name'])) + ?? this.readString(this.readFirst(user, ['displayName', 'DisplayName', 'name', 'Name'])) + const displayName = explicitDisplayName ?? username ?? (fullName || 'Telegram User'); + const telegramUserId = this.readNumber(this.readFirst(user, ['telegramUserId', 'telegramUserID', 'TelegramUserID', 'id', 'ID'])) + ?? this.readNumber(this.readFirst(response, ['telegramUserId', 'telegramUserID', 'TelegramUserID', 'userID', 'UserID'])) + ?? 0; + const expiresAt = this.readString(this.readFirst(response, ['expiresAt', 'ExpiresAt', 'expires', 'Expires'])) + ?? new Date(Date.now() + WEB_SESSION_COOKIE_MAX_AGE_SECONDS * 1000).toISOString(); + + return { + sessionId, + telegramUserId, + username, + displayName, + active, + expiresAt, + }; + } + + private extractSessionId(response: Record | null, fallbackSessionId: string): string { + if (!response) { + return fallbackSessionId; + } + + return this.readString(this.readFirst(response, [ + 'webSessionID', + 'WebSessionID', + 'webSessionId', + 'sessionID', + 'SessionID', + 'sessionId', + 'id', + 'ID' + ])) ?? fallbackSessionId; + } + + private readFirst(source: Record, keys: string[]): unknown { + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + return source[key]; + } + } + + return undefined; + } + + private readString(value: unknown): string | null { + if (typeof value === 'string' && value.trim()) { + return value; + } + + if (typeof value === 'number' || typeof value === 'bigint') { + return value.toString(); + } + + return null; + } + + private readNumber(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + + if (typeof value === 'string') { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + + return null; + } + + private asRecord(value: unknown): Record | null { + return value !== null && typeof value === 'object' && !Array.isArray(value) + ? value as Record + : null; + } + + private isActiveStatus(status: unknown): boolean { + if (status === true || status === 1) { + return true; + } + + if (typeof status !== 'string') { + return false; + } + + return ['true', '1', 'active', 'authenticated', 'confirmed', 'success', 'logged_in'].includes(status.toLowerCase()); + } + + private generateGuid(): string { + if (globalThis.crypto?.randomUUID) { + return globalThis.crypto.randomUUID(); + } + + const bytes = new Uint8Array(16); + if (globalThis.crypto?.getRandomValues) { + globalThis.crypto.getRandomValues(bytes); + } else { + for (let index = 0; index < bytes.length; index++) { + bytes[index] = Math.floor(Math.random() * 256); + } + } + + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')); + return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`; + } + + private getStoredWebSessionID(): string | null { + if (typeof document === 'undefined') { + return null; + } + + const cookie = document.cookie + .split('; ') + .find(row => row.startsWith(`${WEB_SESSION_COOKIE}=`)); + + if (!cookie) { + return null; + } + + try { + return decodeURIComponent(cookie.substring(WEB_SESSION_COOKIE.length + 1)); + } catch { + return null; + } + } + + private setStoredWebSessionID(webSessionID: string): void { + if (typeof document === 'undefined') { + return; + } + + const secure = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : ''; + document.cookie = `${WEB_SESSION_COOKIE}=${encodeURIComponent(webSessionID)}; Max-Age=${WEB_SESSION_COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax${secure}`; + } + + private clearStoredWebSessionID(): void { + if (typeof document === 'undefined') { + return; + } + + document.cookie = `${WEB_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Lax`; + } } diff --git a/src/environments/environment.lavero.production.ts b/src/environments/environment.lavero.production.ts index 3734022..1095d92 100644 --- a/src/environments/environment.lavero.production.ts +++ b/src/environments/environment.lavero.production.ts @@ -5,12 +5,13 @@ export const environment = { brandFullName: 'Lavero Store', theme: 'lavero', apiUrl: '/api', + authApiUrl: 'https://users.vitanova.network:456', logo: '/assets/images/lavero/lavero-logo.png', contactEmail: 'info@lavero.store', supportEmail: 'info@lavero.store', domain: 'lovero.store', telegram: '@laveromarket', - telegramBot: 'laveroSupportbot', + telegramBot: 'myAMLKYCBOT', phones: { russia: '+7 916 109 10 32', support: '+7 916 109 10 32' diff --git a/src/environments/environment.lavero.ts b/src/environments/environment.lavero.ts index cd98fad..2141c50 100644 --- a/src/environments/environment.lavero.ts +++ b/src/environments/environment.lavero.ts @@ -5,12 +5,13 @@ export const environment = { brandFullName: 'Lavero Store', theme: 'lavero', apiUrl: '/api', + authApiUrl: 'https://users.vitanova.network:456', logo: '/assets/images/lavero/lavero-logo.png', contactEmail: 'info@lavero.store', supportEmail: 'info@lavero.store', domain: 'lovero.store', telegram: '@laveromarket', - telegramBot: 'laveroSupportbot', + telegramBot: 'myAMLKYCBOT', phones: { russia: '+7 916 109 10 32', support: '+7 916 109 10 32' diff --git a/src/environments/environment.novo.production.ts b/src/environments/environment.novo.production.ts index fe4222c..631eaee 100644 --- a/src/environments/environment.novo.production.ts +++ b/src/environments/environment.novo.production.ts @@ -5,12 +5,13 @@ export const environment = { brandFullName: 'novo Market', theme: 'novo', apiUrl: '/api', + authApiUrl: 'https://users.vitanova.network:456', logo: '/assets/images/novo-logo.svg', contactEmail: 'info@novo.market', supportEmail: 'info@novo.market', domain: 'novo.market', telegram: '@novomarket', - telegramBot: 'novoSupportbot', + telegramBot: 'myAMLKYCBOT', phones: { russia: '+7 916 109 10 32', support: '+7 916 109 10 32' diff --git a/src/environments/environment.novo.ts b/src/environments/environment.novo.ts index f331124..95440b8 100644 --- a/src/environments/environment.novo.ts +++ b/src/environments/environment.novo.ts @@ -5,12 +5,13 @@ export const environment = { brandFullName: 'novo Market', theme: 'novo', apiUrl: '/api', + authApiUrl: 'https://users.vitanova.network:456', logo: '/assets/images/novo-logo.svg', contactEmail: 'info@novo.market', supportEmail: 'info@novo.market', domain: 'novo.market', telegram: '@novomarket', - telegramBot: 'novoSupportbot', + telegramBot: 'myAMLKYCBOT', phones: { russia: '+7 916 109 10 32', support: '+7 916 109 10 32' diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts index 38c3619..ac4c81a 100644 --- a/src/environments/environment.production.ts +++ b/src/environments/environment.production.ts @@ -5,12 +5,13 @@ export const environment = { brandFullName: 'Dexar Market', theme: 'dexar', apiUrl: 'https://api.dexarmarket.ru:445', + authApiUrl: 'https://users.vitanova.network:456', logo: '/assets/images/dexar-logo.svg', contactEmail: 'info@dexarmarket.ru', supportEmail: 'info@dexarmarket.ru', domain: 'dexarmarket.ru', telegram: '@dexarmarket', - telegramBot: 'DexarSupport_bot', + telegramBot: 'myAMLKYCBOT', phones: { russia: '+7 (926) 459-31-57', armenia: '+374 94 86 18 16', diff --git a/src/environments/environment.ts b/src/environments/environment.ts index b843497..fdd921e 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -6,12 +6,13 @@ export const environment = { brandFullName: 'Dexar Market', theme: 'dexar', apiUrl: '/api', + authApiUrl: 'https://users.vitanova.network:456', logo: '/assets/images/dexar-logo.svg', contactEmail: 'info@dexarmarket.ru', supportEmail: 'info@dexarmarket.ru', domain: 'dexarmarket.ru', telegram: '@dexarmarket', - telegramBot: 'DexarSupport_bot', + telegramBot: 'myAMLKYCBOT', phones: { russia: '+7 (926) 459-31-57', armenia: '+374 94 86 18 16',