Reverse Engineering Lidl Fun Trips
Technos
-
Apache Cordova
-
AngularJS
-
Ionic
L'appli est une simple webapp qui fait une serie d'appels a un backend rest.
Url du site: https://lidlfuntrips.be/
Architecture
Config Postman
Je suis une fénéant et j'aime postman. Voici un fichier qui contient toutes les query interessantes a charger dans postman.
J'utilise l'env var { {bearer} }. Il suffit de faire un call a login pour recup le bearer et mettre a jour la var d'env, j'ai la flemme de le faire pour vous. Je le ferai un jour peut être.
Et vu que j'ai pas de manière d'heberger le fichier de manière sure fiable sur la longeur je vous le file ici, copiez collez et ROTFL sur mon sujet
{ "id": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6", "name": "Lidle", "description": "", "order": [ "7f95e59e-5c21-9ce7-361e-b12f1c9bd0f4", "89d86de1-4ef4-5af1-cad4-429e3ca18c67", "e46ed2b5-7747-bf0d-5cd1-2498397e470e", "e525b5d4-6ce9-3442-bb37-f02b9c95d50b", "a7eb7921-622c-491c-b08f-26b6e8f098e4" ], "folders": [], "folders_order": [], "timestamp": 1526791518755, "owner": 0, "public": false, "requests": [ { "id": "7f95e59e-5c21-9ce7-361e-b12f1c9bd0f4", "headers": "Content-Type: application/json\nAuthorization: Basic Og==\n", "headerData": [ { "key": "Content-Type", "value": "application/json", "description": "", "enabled": true }, { "key": "Authorization", "value": "Basic Og==", "description": "", "enabled": true } ], "url": "https://wallet-service.qup.eu/v1/users/login", "queryParams": [], "pathVariables": {}, "pathVariableData": [], "preRequestScript": null, "method": "POST", "collectionId": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6", "data": [], "dataMode": "raw", "name": "Login", "description": "", "descriptionFormat": "html", "time": 1526791551026, "version": 2, "responses": [], "tests": null, "currentHelper": "normal", "helperAttributes": {}, "rawModeData": "{\n\t\"device_id\": 1234567890,\n\t\"plateform\": \"android\",\n\t\"grant_type\": \"password\",\n\t\"client_id\": \"lidl-fr_FR-android@ws.apps.queueup.eu\",\n\t\"client_secret\": \"elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs\",\n\t\"username\": \"chamaloriz@gmail.com\",\n\t\"password\": \"Radislav250399\"\n}" }, { "id": "89d86de1-4ef4-5af1-cad4-429e3ca18c67", "headers": "Authorization: Bearer NSx5kamSYNYCBsmgCipO49wqzzQ2ZRwHmZvukKMC\n", "headerData": [ { "key": "Authorization", "value": "Bearer NSx5kamSYNYCBsmgCipO49wqzzQ2ZRwHmZvukKMC", "description": "", "enabled": true } ], "url": "https://wallet-service.qup.eu/v1/me/messages", "queryParams": [], "pathVariables": {}, "pathVariableData": [], "preRequestScript": null, "method": "GET", "collectionId": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6", "data": null, "dataMode": "params", "name": "Get Message", "description": "", "descriptionFormat": "html", "time": 1526791538316, "version": 2, "responses": [], "tests": null, "currentHelper": "normal", "helperAttributes": {} }, { "id": "a7eb7921-622c-491c-b08f-26b6e8f098e4", "headers": "Authorization: Bearer {{bearer}}\nContent-Type: application/json\n", "headerData": [ { "key": "Authorization", "value": "Bearer {{bearer}}", "description": "", "enabled": true }, { "key": "Content-Type", "value": "application/json", "description": "", "enabled": true } ], "url": "https://wallet-service.qup.eu/v1/me/vouchers/add", "queryParams": [], "pathVariables": {}, "pathVariableData": [], "preRequestScript": null, "method": "POST", "collectionId": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6", "data": [], "dataMode": "raw", "name": "Post Vaucher", "description": "", "descriptionFormat": "html", "time": 1526792030344, "version": 2, "responses": [], "tests": null, "currentHelper": "normal", "helperAttributes": {}, "rawModeData": "{\r\n\"code\":\"LIA25636QZ\"\r\n}" }, { "id": "e46ed2b5-7747-bf0d-5cd1-2498397e470e", "headers": "Authorization: Bearer MqyGajuFyVAUfJgNTfdLK0AOsWqBTCc11CGLP1yH\n", "headerData": [ { "key": "Authorization", "value": "Bearer MqyGajuFyVAUfJgNTfdLK0AOsWqBTCc11CGLP1yH", "description": "", "enabled": true } ], "url": "https://wallet-service.qup.eu/v1/me", "queryParams": [], "pathVariables": {}, "pathVariableData": [], "preRequestScript": null, "method": "GET", "collectionId": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6", "data": null, "dataMode": "params", "name": "Get Profile", "description": "", "descriptionFormat": "html", "time": 1526791618943, "version": 2, "responses": [], "tests": null, "currentHelper": "normal", "helperAttributes": {} }, { "id": "e525b5d4-6ce9-3442-bb37-f02b9c95d50b", "headers": "Authorization: Bearer {{bearer}}\n", "headerData": [ { "key": "Authorization", "value": "Bearer {{bearer}}", "description": "", "enabled": true } ], "url": "https://wallet-service.qup.eu/v1/me/vouchers", "queryParams": [], "pathVariables": {}, "pathVariableData": [], "preRequestScript": null, "method": "GET", "collectionId": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6", "data": null, "dataMode": "params", "name": "Get encoded voucher", "description": "", "descriptionFormat": "html", "time": 1526791836364, "version": 2, "responses": [], "tests": null, "currentHelper": "normal", "helperAttributes": {} } ] }
Authentification / Recup un token
Authentification des requêtes via le header HTTP Authorization Bearer. Chaque requête doit contenir le token.
Le token peut être obtenu en faisait un POST sur /users/login (UserProvider.Login())
Query
curl -X POST \ https://wallet-service.qup.eu/v1/users/login \ -H 'content-type: application/json' \ -d '{ "device_id": 1234567890, "plateform": "android", "grant_type": "password", "client_id": "lidl-fr_FR-android@ws.apps.queueup.eu", "client_secret": "elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs", "username": "toto", "password": "1234" }'
Réponse
{ "access_token": "zlsTbxIbDpdUr27vmrJQSgaUPj4NcdcU7d3XwdgI", "token_type": "Bearer", "expires_in": 30758400 }
A noter que le ContentPageProvider utilise un autre mécanisme d'authentification, cfr doc du provider.
Creation d'un compte
-
Le mot de passe doit faire minimum 6 caractères, contenir une majuscule, minuscule et nombre
-
L'adresse mail doit répondre au format (désolé pour le regex du pauvre) [A-Zaz0-9]@[A-Zaz0-9].[A-Zaz0-9]
Query
curl -X POST \ https://wallet-service.qup.eu/v1/users/register \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ -d '{ "title": "f", "firs_name": "AZERTYU", "suffix": "AZERTY", "last_name": "AZERTYU2", "name": "AZERTYU AZERTYU", "postalcode": "4000", "email": "jeanhkjhkjhkjh@hotmail.com", "password": "azerty1234$A", "device_id": 1234567890, "plateform": "android", "grant_type": "password", "client_id": "lidl-fr_FR-android@ws.apps.queueup.eu", "client_secret": "elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs" }'
Response
{ "access_token": "kVSU7NPBrYP3meb4XtvQfq3JwbfH7P0htnmGph4J", "token_type": "Bearer", "expires_in": 30758400 }
Envoyer un voucher
Query
curl -X POST \ https://wallet-service.qup.eu/v1/me/vouchers/add \ -H 'authorization: Bearer kVSU7NPBrYP3meb4XtvQfq3JwbfH7P0htnmGph4J' \ -H 'content-type: application/json' \ -d '{ "code":"LIA25636QZ" }'
Response (erreur - inexistant)
{ "internal_code": 105, "success": false, "errors": [ "" ] }
Response (erreur - déja encodé)
{ "internal_code": 101, "success": false, "errors": [ "" ] }
Responses (succes)
{ "internal_code": 0, "success": true, "data": { "voucher": "XKN5GWWDCE", "order_id": null, "used": false } }
Récuperer les vouchers déja encodé
Query
curl -X GET \ https://wallet-service.qup.eu/v1/me/vouchers \ -H 'authorization: Bearer Ad1GI7gG39DKJiijewuNUn019RZclLewSTFVolJt'
Response
{ "internal_code": 0, "success": true, "data": [ { "voucher": "D6G279P2KB", "used": false, "order_id": null }, { "voucher": "3Z9LAXDM12", "used": false, "order_id": null }, // Une liste quoi... ] }
Récupérer le profile
Query
curl -X GET \ https://wallet-service.qup.eu/v1/me/ \ -H 'authorization: Bearer Ad1GI7gG39DKJiijewuNUn019RZclLewSTFVolJt'
Response
{ "internal_code": 0, "success": true, "data": { "name": "AZERTYU AZERTYU", "email": "jeanhkjhkjhkjh@hotmail.com", "postalcode": "4000", "created_at": "2018-05-18 16:04:56GMT+0000", "temp_password": null, "locale": "en_US", "first_name": "", "suffix": "AZERTY", "last_name": "AZERTYU2", "title": "f", "telephone": null } }
Recuperer les messages
Query
curl -X GET \ https://wallet-service.qup.eu/v1/me/messages \ -H 'authorization: Bearer NSx5kamSYNYCBsmgCipO49wqzzQ2ZRwHmZvukKMC'
Response
{ "internal_code": 0, "success": true, "data": [ { "code": "cVR4ckSGLNc:APA91bFqUUafMNVfIphHlBaisTVBQQCtM8TSck5jOhaS8LZanKaxj-8tKM7B-AH2pZj_RqgqFw2CzlHnxkGKEH3vFoxdhyh9nF4uKMqUWaxAcBI4HTgG_8rdi5FZZT4ZKsA9KnxIX5Tx", "title": "Namens Lidl: Fijne Pinksteren!", "message": "De la part de Lidl Funtrips: joyeuse fête de la Pentecôte!", "read": false, "created_at": "2018-05-19 10:55:59GMT+0000" }, { "code": "cVR4ckSGLNc:APA91bFqUUafMNVfIphHlBaisTVBQQCtM8TSck5jOhaS8LZanKaxj-8tKM7B-AH2pZj_RqgqFw2CzlHnxkGKEH3vFoxdhyh9nF4uKMqUWaxAcBI4HTgG_8rdi5FZZT4ZKsA9KnxIX5Tx", "title": "€ 10 korting bij Bobbejaanland met Pinksteren!", "message": "Une réduction de 10 € chez Bobbejaanland à la Pentecôte! ", "read": false, "created_at": "2018-05-16 13:09:23GMT+0000" }, { "code": "cVR4ckSGLNc:APA91bFqUUafMNVfIphHlBaisTVBQQCtM8TSck5jOhaS8LZanKaxj-8tKM7B-AH2pZj_RqgqFw2CzlHnxkGKEH3vFoxdhyh9nF4uKMqUWaxAcBI4HTgG_8rdi5FZZT4ZKsA9KnxIX5Tx", "title": "Ga met Hemelvaart de baby-olifantjes bezoeken in Planckendael!", "message": "Allez visiter les bébés éléphants à Planckendael pendant l'Ascension!", "read": false, "created_at": "2018-05-09 14:55:05GMT+0000" } ] }
REST API Endpoints
Endpoint | Method |
---|---|
/me | GET, PUT, DELETE |
/me/messages | GET, PUT |
/me/vouchers | GET |
/me/vouchers/add | POST |
/me/messages | GET, PUT |
/users/login | POST |
/users/register | POST |
/users/forgot | POST |
/content | GET |
/content/{key} | GET |
Fichiers décompilé
Tout se trouve dans /assets/www/build/
Fichier | Role |
---|---|
vendor.js | OSEF, angular js |
polyfills.js | OSEF aussi |
swtoolbox.js | OSEF toujours |
main.js | Toute la logique est ici |
0.js | ScanPageModule |
1.js | ProfilePageModule |
2.js | PassForgotPageModule |
3.js | ManualCodePageModule |
4.js | LoginPageModule |
5.js | PrivacyPageModule |
6.js | PassForgotFeedbackPageModule |
7.js | MessagesPageModule |
8.js | IntroPageModule |
9.js | DeleteProfileModalModule |
10.js | ContactPageModule |
Quelques remarques:
-
Les modules ne contiennent pas de logique autre que UI, ils utilisent des objet provider defini dans main qui eux contiennent la logique, servent de facade, repository, etc…
-
Les pages utilisent les forms de AngularJS, il y a un peu de RE a faire sur les pages pour avoir les objet envoyé.
-
AngularJS utilise beaucoup l'injection de de dépendances, tout est injecté via le constructeur.
-
Chaque module ne contient qu'une page.
Main.js
ApiService
Methode | Doc |
---|---|
getLocalizedClientId() | Interface pour AppSettings, utilisé pour build les request |
getClientSecret() | Interface pour AppSettings, utilisé pour build les request |
ApiGateway
Toutes les requêtes REST passent par ce service.
Expose POST, GET, PUT, etc. Toutes les méthodes appellent derrière request(). Cette méthode construit la request rest et set le header Authorization avec le bon token, c'est a peut près tout ce qu'il y a d’intéressant ici.
LocaleService
Retourne nl_NL ou fr_FR, utilise pour build le client_id
ProjectAppSettings
Have Fun, tout est assez clair.
ProjectAppSettings.ENDPOINT = { TEST: { BASE_URL: 'https://wallet-service.test.qup.eu/v1/', CLIENT_ID: { 'IOS': 'lidl-<locale>-android@ws.apps.queueup.eu', 'ANDROID': 'lidl-<locale>-ios@ws.apps.queueup.eu' }, CLIENT_SECRET: { 'IOS': 'elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs', 'ANDROID': 'elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs' }, SHOP_URL: 'https://lidlfuntrips.qup.uat1.aanzee.cc' }, LIVE: { BASE_URL: 'https://wallet-service.qup.eu/v1/', CLIENT_ID: { 'IOS': 'lidl-<locale>-android@ws.apps.queueup.eu', 'ANDROID': 'lidl-<locale>-ios@ws.apps.queueup.eu' }, CLIENT_SECRET: { 'IOS': 'elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs', 'ANDROID': 'elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs' }, SHOP_URL: 'https://lidlfuntrips.be' } };
ProjectAppSettings.SERVER = ProjectAppSettings.ENDPOINT.LIVE; ProjectAppSettings.PROJECT = 'lidl'; ProjectAppSettings.VOUCHER_MASK = [/[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/]; ProjectAppSettings.HAS_NAME_SPLIT = true; ProjectAppSettings.API_ENDPOINT = ProjectAppSettings.SERVER.BASE_URL; ProjectAppSettings.SHOP_URL = ProjectAppSettings.SERVER.SHOP_URL; ProjectAppSettings.ANDROID_SENDER_ID = '430671127661'; ProjectAppSettings.VOUCHER_VALUE = 2; ProjectAppSettings.CLIENT_ID = { 'IOS': ProjectAppSettings.SERVER.CLIENT_ID.IOS, 'ANDROID': ProjectAppSettings.SERVER.CLIENT_ID.ANDROID }; ProjectAppSettings.CLIENT_SECRET = { 'IOS': ProjectAppSettings.SERVER.CLIENT_SECRET.IOS, 'ANDROID': ProjectAppSettings.SERVER.CLIENT_SECRET.ANDROID }; ProjectAppSettings.GA_ID = 'UA-2367280-26'; ProjectAppSettings.VALUTA = 'EUR'; ProjectAppSettings.isContentAccessTokenRequired = true; ProjectAppSettings.scanditLicenceKey = { ios: 'AVwrDYiBHE1gFCB1uhW4ICsgS+avHh6XEnHDO5hQsMErS7UolXsALqohduCGfldXnU/BAxojgEKDfl8zF2pvsxJXazqWVxJW2GIkv4hhqnStawfmiWDcO9xDh8IObnMk8wagkxkXNeTfB1LQ/x0SWoIvRAWtw1RFUbigMVJsbSLIH7ERDf8nMHIiCor+4Mi8KPJ2pj9GtjYklSjesjSk2d9Td3kw/86f16aFSPvL6qmFTsyK04PSo+zeV9rvYIfu2aigvQC0V30mGjnwrB5Cjg6fCVSyBoqKCpl90G+tXo/s6C3T5kPAUpVkAD7V1OZs43W0AJhzG82qNO1Jdh8G8ZmywQhFMWI/i80no4fU+qGNDzvrJr8X59Q3izDbTdCCZ20P1iXQCNXScJn/vpiuAjQyEPSrHybJ/69pDm1oYYP6KxHg+Hyjn2KtukMqJxpXSlcduwB7YrcSnVJSU/YLULUZANDKxt15FCKUpNwg/bBss45yryRh5eAv6LOSGyBZ6FAxwcxSQ+Em5DGqX8dN72dd9m8wHZfco9qIOSl3SiY3GZWivSqK/Wvca+hJ60CtFGuaf34JpKYIELfInznSTGC90VxSMpqRkbch0eCKh/mA4DcdmzuZKDz1jxbnFuWfCSK3y6T42gck+r/KrAixdtbo1inRD+ILKqmc7paZzBa1UUkSx6PvqB1vLa1smqTWMii1hOASKyVj7Nju9TxjqFuidfQcuhvl9f1d5njr62zW0jGB7PqWfKealcvLBReSqgcnghbRQQuSje/xTe8Ou2Ssc/sQNpKafaGjdqC3ucezwzTukj8=', android: 'AXBrj7WBIUINN103cBKCgjQPNEdxOLC/3F3tyddJn1zdaZMasEGF6v0vUVobRJq5zmrnrnpbXF1UbIrgPUMKaoZB628XZHMOhHMoNud1xQtNTrdQD3pauIp28UvaTId/PSOuGGAE6gchxM+MvXFTU+4+OzmZM5kgo8Gx+ob2Ok6TZ7VBmk0XTVCtU6ZbHs9Cax9b/njJLjvDHRK5PErlFGjtSq7/gWN6IyjPGqcYEhPeepz+MpCXluOgjHlU1JUh3laLUjLYnMP5i10gMW5nWwkzv9DZfwbeLh3+2Sg90EPWlyBsqekHRXuze7Ams74cmYh3rsraTCucGTKyCgyqKFzLzb5vR7u7tbsq3vTxLQ87xfjRzx3CNUY37TW9NBNHUYWWfc2BgL7b/jOneWxrkFC1MjLahWn44d6mkEM6bxI6UAW5zjk1T48g7D6ALo15sNrDFJ8aohS+qUSXd6Z73uRJ6F9yBbNCBeMZfrgh8eyMyq0Cb1JaXdMDBq+h+maLZr3ySbi+sCZtoaGbrUd5+7IVk7UmkhKiYQntl+gzIoI089hY0qGYLy6hvbzpS1NbiJn6F9QwjMV4w7FxH242jqv5+kbx/5T80ecKxIHdjXEeZYQ+5+aCfYSfmOucDxGVZJVGjh4J+7EJT0Zl0dts++pwXW2vIT/wKQfM+BM7pt8KAeltegl/+33OgIPDsptJ+XKiOA48Wlhb7tly8j78yAsLdzW39Xt/Btw9UTvJdrUqp8YrzeUGCXup4te6YCB+Vn0W302o/Z4GSSvyoIQNHwlxyc2qW4lFS4+wJHhes8ZX4ZjdJGo=', }; ProjectAppSettings.availableLocales = [{ iso: 'nl', locale: 'nl_NL', }, { iso: 'fr', locale: 'fr_FR', }]; ProjectAppSettings.defaultLanguage = 'fr'; ProjectAppSettings.hasIntroImageBackground = false; ProjectAppSettings.contentPages = { contact: 'contact', privacy: 'privacy', };
AppSettings
Interface sur ProjectAppSettings
Providers
UserProvider
Method | Doc |
---|---|
login(post_data) | POST sur /users/login + LoginPage form data |
register(post_data) | POST sur /users/register + LoginPage register form data |
forgot(post_data) | POST sur /users/forgot + LoginPage form data |
getProfile() | GET sur /user |
updateProfile(post_data) | PUT sur /user + user object |
deleteProfile(post_data) | DELETE sur /user + user object |
saveExpirationDate() |
VoucherProvider
Method | Doc |
---|---|
getVoucher() | GET sur /me/vouchers |
addVoucher(code) | POST sur /me/vouchers/add |
MessageProvider
Method | Doc |
---|---|
getMessage() | GET sur /me/messages |
markAsRead() | PUT sur /me/messages/{messageCode} |
convertServerToPushNotification(); | |
incrementNotificationBadge(); | |
getNotificationBadge(); | |
markNotificationsRead(); |
ContentPageProvider
Récupére du contenu depuis le webservice mais utilise un autre système d'authentification si isContentAccessTokenRequired est a true (ce qui est le cas).
getAccessToken() POST sur /client/access_token avec un grant_type 'client_credentials' pour récupérer un token qui sera ensuite utilisé dans getContentPageForKey(key) qui GET sur /content/{key}
Autres providers dans main.js
Method | Doc |
---|---|
StorageProvider | Abstraction local storage |
PushNotificationProvider | Hum… If you don't get the role from service class you're damn to hell |
GAProvider | Google Analytics |
Trucs moins interessants
Modules dans main.js
Autres classes dans main.js
-
WebPopup
-
QRScanner
-
MyMissingTranslationHandler
-
CustomDate
-
VoucherAmount
-
ScrollShadow
-
SelectSwitchComponent
-
CacheRequest
-
MyApp
No Comments