mirror of
				https://github.com/optim-enterprises-bv/databunker.git
				synced 2025-10-31 09:57:46 +00:00 
			
		
		
		
	initial project release
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | databunker | ||||||
|  | bql.db | ||||||
							
								
								
									
										768
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										768
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,768 @@ | |||||||
|  | # Paranoid Guy Data Bunker | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **Data Bunker is advanced personal information tokenization and storage service build to comply with GDPR.** | ||||||
|  |  | ||||||
|  | This project, when deployed, can replace all user personal records scattered in the organization's different | ||||||
|  | internal databases with one user token generated and managed by Data Bunker service. | ||||||
|  |  | ||||||
|  | By deploying this project and moving all personal information to one place, you will comply with the following | ||||||
|  | GDPR statement: *Personal data should be processed in a manner that ensures appropriate security and  | ||||||
|  | confidentiality of the  personal data, including for preventing unauthorized access to or use of personal | ||||||
|  | data and the equipment used for the processing.* | ||||||
|  |  | ||||||
|  | **NOTE**: Implementing this project does not make you fully compliant with GDPR requirements and you still | ||||||
|  | need to consult with an attorney specializing in privacy. | ||||||
|  |  | ||||||
|  | #### Diagram of old-style solution. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #### Diagram of Solution with Paranoid Guy Data Bunker | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | # This product stands many GDPR requirements | ||||||
|  |  | ||||||
|  | ## Right to be forgotten / Right to erasure | ||||||
|  |  | ||||||
|  | When your customer asks for his **right to be forgotten** legal right, his private records will be | ||||||
|  | wiped out of the Data Bunker database, giving you the possibility to leave all internal databases unchanged. | ||||||
|  |  | ||||||
|  | **NOTE**: You just need to make sure that you do not have any user identifiable information in your other databases, | ||||||
|  | logs, files, etc... | ||||||
|  |  | ||||||
|  | ## Right of access | ||||||
|  |  | ||||||
|  | We build in passwordless login into the data bunker service. So, your customer/user can log in into his personal account | ||||||
|  | at Data Bunker and view all information collected by Data Bunker in connection to his profile. | ||||||
|  |  | ||||||
|  | ## Right to rectification | ||||||
|  |  | ||||||
|  | Your customer/user can log in to his personal account at Data Bunker and change his records. If needed, Bunker will | ||||||
|  | send you a notification request about the change. | ||||||
|  |  | ||||||
|  | ## Right to restrict processing / Right to object | ||||||
|  |  | ||||||
|  | Data Bunker can work as management for all user consents. User can cancel specific consent in his personal account at | ||||||
|  | Bunker, for example, to block sending him emails. Your backend can work with Data Bunker using API to add, or cancel | ||||||
|  | consents and we will send you a notification about user actions. | ||||||
|  |  | ||||||
|  | ## Right to data portability (partial) | ||||||
|  |  | ||||||
|  | Your customer/user can log in to his personal account at Data Bunker and view and extract all his records stored at | ||||||
|  | Data Bunker. | ||||||
|  |  | ||||||
|  | **NOTE**: You need to provide your customers with a way to extract data from your internal databases. | ||||||
|  |  | ||||||
|  | ## Data minimisation | ||||||
|  |  | ||||||
|  | Basically, when you clean up your databases from personal records and use Data Bunker token instead, you | ||||||
|  | are already minimizing the personal information you store in different systems. In addition, when sending | ||||||
|  | you customer data to 3rd party systems Data Bunker provides you with purposely build *shareable identity* | ||||||
|  | that is time-bound. | ||||||
|  |  | ||||||
|  | ## Data Accuracy | ||||||
|  |  | ||||||
|  | We allow the customer to change the records that are stored in Data Bunker. This way we achieve data accuracy. | ||||||
|  |  | ||||||
|  | ## Transparency | ||||||
|  |  | ||||||
|  | All operations with personal records are saved in the audit log. Your customer can log in to his account at Data Bunker | ||||||
|  | and view the audit trail. | ||||||
|  |  | ||||||
|  | ## Integrity and confidentiality | ||||||
|  |  | ||||||
|  | All personal data is encrypted. Only relevant personnel can access the data. We audit all operations with personal records. | ||||||
|  | All-access to Data Bunker API is done using an HTTPS SSL certificate. Enterprise version supports Shamir's Secret Sharing | ||||||
|  | algorithm to split the master key to a number of keys. A number of keys (that can be saved in different hands in the | ||||||
|  | organization) are required to bring up the system. | ||||||
|  |  | ||||||
|  | ## Accountability principle | ||||||
|  |  | ||||||
|  | Each one, connected to Data Bunker must provide an access token to do any operation in Data Bunker or the user needs to | ||||||
|  | login to access his own account. All operations are saved in the audit log. | ||||||
|  |  | ||||||
|  | ## Privacy by design | ||||||
|  |  | ||||||
|  | This product, from the architecture level was build to comply with strick privacy laws. Deploying this or similar | ||||||
|  | architecture, can make your company privacy by design compliant. | ||||||
|  |  | ||||||
|  | ## NOTE | ||||||
|  |  | ||||||
|  | Implementing this project does not make you fully compliant with GDPR requirements and you still need to | ||||||
|  | consult with an attorney specializing in privacy. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | # Data Bunker usecases | ||||||
|  |  | ||||||
|  | ## Personal Information tokenization and storage | ||||||
|  |  | ||||||
|  | This is already covered deeply above. Here I can add that Data Bunker has a layer of application | ||||||
|  | level personal information storage and each user in our database can be linked to a number of | ||||||
|  | application records (saved in Data Bunker). | ||||||
|  |  | ||||||
|  | ## Audit of all operations with personal records | ||||||
|  |  | ||||||
|  | This is already covered above. | ||||||
|  |  | ||||||
|  | ## GDPR compliant logging | ||||||
|  |  | ||||||
|  | Data Bunker supports a number of API that can help you to store user information in logs in | ||||||
|  | GDPR compliant way and work with cloud logging companies. | ||||||
|  |  | ||||||
|  | ## Consent management, i.e. withdawal | ||||||
|  |  | ||||||
|  | According to GDPR, if you want to send your customer SMS using 3rd party gateway, | ||||||
|  | you must show to your customer a detailed notification message that you will send | ||||||
|  | his phone number to a specific SMS gateway company and the user needs to confirm that. | ||||||
|  |  | ||||||
|  | You need to store these confirmations and Data Bunker can help you with that. | ||||||
|  |  | ||||||
|  | Consent must be freely given, specific, informed and unambiguous. From GDPR, Article 7, item 3: | ||||||
|  |  | ||||||
|  | * **The data subject shall have the right to withdraw his or her consent at any time.** | ||||||
|  | * **It shall be as easy to withdraw as to give consent.** | ||||||
|  |  | ||||||
|  | In Data Bunker: | ||||||
|  |  | ||||||
|  | * Your customers can log in to his Data Bunker account and view all consents he gave. | ||||||
|  | * Users can also discharge consents and we will send you a notification message. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## User signup and sign-in | ||||||
|  |  | ||||||
|  | When implementing signup and sign-in in your customer-facing applications, we recommend you to | ||||||
|  | store all signup records in the Data Bunker database. We support 3 types of indexes, index | ||||||
|  | by login, index by email and index by phone. So you can easily implement login logic with | ||||||
|  | our service. | ||||||
|  |  | ||||||
|  | Index by email and index by phone allow us to give your customers passwordless access to their | ||||||
|  | personal profile at Data Bunker. We send your user a one-time login code by SMS or email to | ||||||
|  | give him access to his account at Data Bunker. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | # Questions | ||||||
|  |  | ||||||
|  | ## Why Open Source? | ||||||
|  |  | ||||||
|  | I am a big fan of the open-source movement. After a lot of thoughts and consultations, | ||||||
|  | the main Data Bunker product will be open source. | ||||||
|  |  | ||||||
|  | We are doing this to give our customers a steady base to continue using this solution in case | ||||||
|  | the company is closed. | ||||||
|  |  | ||||||
|  | Enterprise version will be closed source. | ||||||
|  |  | ||||||
|  | ## What is considered PII? | ||||||
|  |  | ||||||
|  | Following it a partial list. | ||||||
|  |  | ||||||
|  | * Name | ||||||
|  | * Address | ||||||
|  | * IP address | ||||||
|  | * Browsing history | ||||||
|  | * Political opinion | ||||||
|  | * Sexual orientation | ||||||
|  | * Social Security Number | ||||||
|  | * Financial data | ||||||
|  | * Banking data | ||||||
|  | * Cookie data | ||||||
|  | * Contacts | ||||||
|  | * Mobile device ID | ||||||
|  | * Passport data | ||||||
|  | * Driving license | ||||||
|  | * ID number | ||||||
|  | * Health / medical data | ||||||
|  | * RFID | ||||||
|  | * Genetic info | ||||||
|  | * Ethnic and racial information | ||||||
|  |  | ||||||
|  | ## Technology stack? | ||||||
|  |  | ||||||
|  | I am a big fan of go language and I use it extensively. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Project technical features: | ||||||
|  |  | ||||||
|  | * [Encrypted storage for personal information](#personal-information-tokanization) | ||||||
|  | * [Application data separation](#application-data-separation) | ||||||
|  | * [Time-limited passwordless access to personal information](#time-limited-passwordless-access-to-personal-information) | ||||||
|  | * [Web and mobile app session data storage](#web-and-mobile-app-session-data-storage) | ||||||
|  | * [Time-limited passwordless access to web and app session data](#web-and-mobile-app-session-data-storage) | ||||||
|  | * [Share user identiy with 3rd party services](#share-user-identity-with-3rd-parties) | ||||||
|  | * [User consent management, storage & withdrawal](#user-consent-management) | ||||||
|  | * [Audit of all operations](#audit) | ||||||
|  | * [Customer UI](#user-ui) | ||||||
|  | * [User passwordless authentication](#custom-user-index) | ||||||
|  |  | ||||||
|  | ## Enterprise features | ||||||
|  | * [Split master key with Shamir's Secret Sharing algo](#master-key-split-in-enterprise-version) | ||||||
|  | * [Advaned role management, ACL](#advanced-acl) | ||||||
|  | * [Support Hashicorp Vault](#hashicorp-vault-integration) | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Encryption in motion and encryption in storage | ||||||
|  |  | ||||||
|  | All access to Data Bunker API is done using HTTPS SSL certificate. All records that have user personal information | ||||||
|  | are encrypted or securely hashed in the databases. All user records are encrypted with a 32 byte key comprizing of | ||||||
|  | System Master key (24 bytes, stored in memory, not on disk) and user record key (8 bytes, stored on disk). | ||||||
|  |  | ||||||
|  | ### Master key split in Enterprise version | ||||||
|  |  | ||||||
|  | Upon initial start, the **Enterprise version** generates a secret master key and 5 keys out of it. | ||||||
|  | These 5 keys are generated using Shamir's Secret Sharing algorithm. Combining 3 of any of the keys, | ||||||
|  | ejects original master key and that can be used to decrypt all records. | ||||||
|  |  | ||||||
|  | The Master key is kept in RAM and is never stored to disk. You will need to provide 3 kits to unlock the application. | ||||||
|  | It is possible to save these keys in the AWS secret store and other vault services. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Data Bunker internal tables | ||||||
|  |  | ||||||
|  | Information inside Data Bunker is saved in multiple tables in encrypted format. Here is a diagram of tables. | ||||||
|  |  | ||||||
|  | Detailed usecase for each table is covered bellow. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Personal information tokanization | ||||||
|  |  | ||||||
|  | User information, or PII, received in HTML POST key/value format of or JSON format is serialized, encrypted | ||||||
|  | with a 32 byte key and saved in database. You will get a user token to use in internal databases. Afterwords, | ||||||
|  | you can query the Data Bunker service to receive personal information, saving audit trail. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Application data separation | ||||||
|  |  | ||||||
|  | When creating application, I suppose you do not want to mix your customer data with data from other applications. | ||||||
|  | In addition to personal information record, Data Bunker provides you a way to store your app user information in a | ||||||
|  | specific type of record for that. So, you can retreave only your app' user personal information. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Web and mobile app session data storage | ||||||
|  |  | ||||||
|  | Web or mobile application session data is very similar. They contain customer IP address, browser information, | ||||||
|  | web server headers, logged-in user info, etc... Many systems, including popular webservers, like Nginx, Apache | ||||||
|  | simply store this information in logs. This information, according to GDPR is considered personal identifiable | ||||||
|  | information and must be secured and controlled. | ||||||
|  |  | ||||||
|  | So, you can not save user ip or browser information in logs now. Insead, Data Bunker will generate you a token to | ||||||
|  | save in logs. Data Bunker provides you an API to retreave this info out of Data Bunker without additional password | ||||||
|  | for a limited time as in GDPR. For example one month. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Time-limited passwordless access to personal information | ||||||
|  |  | ||||||
|  | Sometimes you want to share user, app or session private information in less trusted systems without providing | ||||||
|  | access to system root token. | ||||||
|  |  | ||||||
|  | Data Bunker has an API that allows you to generate temprorary access token to access specific fields in the | ||||||
|  | user personal record or application level data or a session record for a limited time only. | ||||||
|  |  | ||||||
|  | Your partner can retrieve this information and only specific fields during this specific timeframe. | ||||||
|  | Afterward, access will be blocked. | ||||||
|  |  | ||||||
|  | **IMAGE** | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Shareable user identity for 3rd parties | ||||||
|  |  | ||||||
|  | When sharing data with 3rd party services like web analytics, logging, intelligence, etc... sometimes we need to | ||||||
|  | share user id, for example, customer original IP address or email address. All these pieces of information | ||||||
|  | are considred user identifiable information and must be minimized when sending to 3rd paty systems. | ||||||
|  |  | ||||||
|  | ***Do not share your customer user name, IP, emails, etc... because they look nice in reports!*** | ||||||
|  |  | ||||||
|  | According to GDPR: *The personal data should be adequate, relevant and **limited to what is necessary** for the | ||||||
|  | purposes for which they are processed.* | ||||||
|  |  | ||||||
|  | Our system can generate you time-limited shareable identity token that you can share with 3rd parties as an identity. | ||||||
|  | This identity, can link bacck to the user personal record or user app record or to specific user session. | ||||||
|  |  | ||||||
|  | Optionally, Data Bunker can incorporate partner name in identity so, you track this identity usage. | ||||||
|  |  | ||||||
|  | **IMAGE** | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## User consent management | ||||||
|  |  | ||||||
|  | Consent in GDPR terms is clear approval for example to share user information with 3rd party, for example with SMS | ||||||
|  | gateway company to send him urgent notifications. | ||||||
|  |  | ||||||
|  | Consent must be freely given, specific, informed and unambiguous. From GDPR, Article 7, item 3: | ||||||
|  |  | ||||||
|  | * **The data subject shall have the right to withdraw his or her consent at any time.** | ||||||
|  | * **It shall be as easy to withdraw as to give consent.** | ||||||
|  |  | ||||||
|  | To comply with this requirement, we support storage and management of user consent by API level and in user UI. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Audit | ||||||
|  |  | ||||||
|  | Data Bunker saves audit events on all API operation. For example, new personal record added or changed; personal information | ||||||
|  | record retreaved, etc... | ||||||
|  |  | ||||||
|  | By providing Audit of events, in relation to personal data, provides response to GDRP Article 15 requirement: | ||||||
|  | *Right of access by the data subject*. | ||||||
|  |  | ||||||
|  | Special features: | ||||||
|  |  | ||||||
|  | * Personal information in audit event is encrypted. | ||||||
|  | * User can view his own records only. | ||||||
|  |  | ||||||
|  | Each audit record consists of: | ||||||
|  |  | ||||||
|  | * Date and time | ||||||
|  | * Operation title | ||||||
|  | * Operation status | ||||||
|  | * Operation description | ||||||
|  | * Change before and after if applicable | ||||||
|  | * User session info if available: IP address, headers, etc... | ||||||
|  |  | ||||||
|  | **IMAGE** | ||||||
|  |  | ||||||
|  | Example from google: https://console.cloud.google.com/home/activity | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## User UI | ||||||
|  |  | ||||||
|  | Internal UI is build to allow users to login with their email and access all data collected by this system. | ||||||
|  | You can easily change it to your requirements. | ||||||
|  |  | ||||||
|  | According to GDPR, controller must provide Data subject with: | ||||||
|  |  | ||||||
|  | * Right of access by the data subject (Article 15) | ||||||
|  | * Right to rectification (Article 16) | ||||||
|  | * Right to be forgotten (Article 17) | ||||||
|  | * Right to restriction of processing (Article 18) | ||||||
|  | * Notification obligation regarding rectification or erasure of personal data or restriction of processing (Article 19) | ||||||
|  | * Right to data portability (Article 20) | ||||||
|  | * Right to object (Article 21) | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | # Enterprise features | ||||||
|  |  | ||||||
|  | ## Advanced role management, ACL | ||||||
|  |  | ||||||
|  | By default, all access to Data Bunker is done with one root token or with **Time-limited passwordless access tokens** | ||||||
|  | that allow to read data from specific user record only. | ||||||
|  |  | ||||||
|  | For more granular control, Data Bunker supports the notion of custom roles. For example, you can create a role | ||||||
|  | to view all records or another role to add and change any user records; view sessions, view all audit events, etc... | ||||||
|  |  | ||||||
|  | After you define a role, the system allow you to generate access token for this role (you will need to have root token | ||||||
|  | for all these operations). | ||||||
|  |  | ||||||
|  | Data Bunker have an API for all these operations. | ||||||
|  |  | ||||||
|  | ## Support Hashicorp Vault | ||||||
|  |  | ||||||
|  | Hashicorp Vault, is a great piece of new generation of security product, has a notion of session accounts/passwords. | ||||||
|  | Hashicorp Vault can store root access token to Paranoid Guy Data Bunker, and when your application wants to open | ||||||
|  | session and access Data Bunker, it will talk with Bunker to issue a temp token with specified role. | ||||||
|  | When your application session is closed with Data Bunker, Hashicorp Vault will connect to Data Bunker and revoke access token. | ||||||
|  |  | ||||||
|  | This architecture is done to minimize the chance that if the attacker breakes into your application server, | ||||||
|  | he will not get a full controll over the Data Bunker service as root token will not be saved in your | ||||||
|  | application server. | ||||||
|  |  | ||||||
|  | This is all done with the help of custom plugin we build for Hashicorp Vault. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## User Api | ||||||
|  |  | ||||||
|  |  | ||||||
|  | | Resource / HTTP method | POST (create)    | GET (read)    | PUT (update)     | DELETE (delete)  | | ||||||
|  | | ---------------------- | ---------------- | ------------- | ---------------- | ---------------- | | ||||||
|  | | /v1/user               | Create new user  | Error         | Error            | Error            | | ||||||
|  | | /v1/user/uuid/{token}  | Error            | Get user      | Update user      | Delete user PII  | | ||||||
|  | | /v1/user/login/{login} | Error            | Get user      | Update user      | Delete user PII  | | ||||||
|  | | /v1/user/email/{email} | Error            | Get user      | Update user      | Delete user PII  | | ||||||
|  | | /v1/user/phone/{phone} | Error            | Get user      | Update user      | Delete user PII  | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Create user record | ||||||
|  | ### `POST /v1/user` | ||||||
|  |  | ||||||
|  | ### Explanation | ||||||
|  |  | ||||||
|  | This API is used to create new user record and if the request is successful it returns new `{token}`. | ||||||
|  | On the database level, each records is encrypted with it's own key. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### POST Body Format | ||||||
|  |  | ||||||
|  | POST Body can contain regular form data or JSON. Data Bunker extracts `{login}`, `{phoen}` and `{email}` out of | ||||||
|  | POST data or from JSON first level and builds additional hashed index for user object. These fields, if | ||||||
|  | provided must be unique, otherwise you will ge an error. So, you can not create additional user object | ||||||
|  | with duplicate email. | ||||||
|  |  | ||||||
|  | The following content type supported: | ||||||
|  |  | ||||||
|  | * **application/json** | ||||||
|  | * **application/x-www-form-urlencoded** | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Example: | ||||||
|  |  | ||||||
|  | Create used by posting JSON: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl -s http://localhost:3000/v1/user -XPOST \ | ||||||
|  |   -H "X-Bunker-Token: cb2537f9-14e2-7019-503f-b36a1a8f6e7f" \ | ||||||
|  |   -H "Content-Type: application/json" \ | ||||||
|  |   -d '{"firstName": "John","lastName":"Doe","email":"user@gmail.com"}' | ||||||
|  | {"status":"ok","uuid":"db80789b-0ad7-0690-035a-fd2c42531e87"} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Create user by POSTing user key/value fiels as post parameters: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl -s http://localhost:3000/v1/user -XPOST \ | ||||||
|  |   -H "X-Bunker-Token: $TOKEN" \ | ||||||
|  |   -d 'firstName=John' \ | ||||||
|  |   -d 'lastName=Doe' \ | ||||||
|  |   -d 'email=user2@gmail.com' | ||||||
|  | {"status":"ok","uuid":"db80789b-0ad7-0690-035a-fd2c42531e87"} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **NOTE**: Keep this user token privately as it provides user private information in your system. | ||||||
|  | For semi-trusted environments or 3rd party companies, use **shareable identity** instead. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Get user record | ||||||
|  | ### `GET /v1/user/{uuid,login,email,phone}/{indexValue}` | ||||||
|  |  | ||||||
|  | ### Explanation | ||||||
|  | This API is used to get user PII records. You can lookup user token by **uuid** (token), **email**, **phone** or **login**. | ||||||
|  |  | ||||||
|  | ### Example: | ||||||
|  |  | ||||||
|  | Fetch by user token: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl --header "X-Bunker-Token: $TOKEN" -XGET \ | ||||||
|  |    https://localhost:3000/v1/user/uuid/DAD2474A-E9A7-4BA7-BFC2-C4506880198E | ||||||
|  | {"uuid":"DAD2474A-E9A7-4BA7-BFC2-C4506880198E","data":{"k1":[1,10,20], | ||||||
|  | "k2":{"f1":"t1","f3":{"a":"b"}},"login":"user1","name":"tom"}} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Fetch by "login" name: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl --header "X-Bunker-Token: $TOKEN" -XGET \ | ||||||
|  |    https://localhost:3000/v1/user/login/user1 | ||||||
|  | {"uuid":"DAD2474A-E9A7-4BA7-BFC2-C4506880198E","data":{"k1":[1,10,20], | ||||||
|  | "k2":{"f1":"t1","f3":{"a":"b"}},"login":"user1","name":"tom"}} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Update user record | ||||||
|  | ### `PUT /v1/user/{uuid,login,email,phone}/{indexValue}` | ||||||
|  |  | ||||||
|  | ### Explanation | ||||||
|  |  | ||||||
|  | This API is used to update user record. You can update user by **uuid** (token), **email**, **phone** or **login**. | ||||||
|  | This call returns update status on success or error message on error. | ||||||
|  |  | ||||||
|  | ### POST Body Format | ||||||
|  |  | ||||||
|  | POST Body can contain regular form POST data or JSON. When using JSON, you can remove the record by setting it's value to null. | ||||||
|  | For example {"key-to-delete":null}. | ||||||
|  |  | ||||||
|  | The following content type supported: | ||||||
|  |  | ||||||
|  | * **application/json** | ||||||
|  | * **application/x-www-form-urlencoded** | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Example: | ||||||
|  |  | ||||||
|  | The following command will change user name to "Alex". An audit event will be generated showing previous and new value. | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl --header "X-Bunker-Token: $TOKEN" -d 'name=Alex' -XPUT \ | ||||||
|  |    https://localhost:3000/v1/user/uuid/DAD2474A-E9A7-4BA7-BFC2-C4506880198E | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Delete user by record | ||||||
|  | ### `DELETE /v1/user/{uuid,login,email,phone}/{indexValue}` | ||||||
|  |  | ||||||
|  | This command will remove all user records from the database, leaving only user token id. | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl -header "X-Bunker-Token: $TOKEN" -XDELETE \ | ||||||
|  |   https://localhost:3000/v1/user/uuid/DAD2474A-E9A7-4BA7-BFC2-C4506880198E | ||||||
|  | {"status":"ok","result":"done"} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## User App Api | ||||||
|  |  | ||||||
|  |  | ||||||
|  | | Resource / HTTP method          | POST (create)       | GET (read)            | PUT (update)  | DELETE | | ||||||
|  | | ------------------------------- | ------------------- | --------------------- | ------------- | ------ | | ||||||
|  | | /v1/userapp/uuid/:uuid/:appname | Create new user app | Get record            | Change record | Delete | | ||||||
|  | | /v1/userapp/uuid/:uuid          | Error               | Get all user app list | Error         | Error  | | ||||||
|  | | /v1/userapp/list                | Error               | Get all app list      | Error         | Error  | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Create user app record | ||||||
|  | ### `POST /v1/userapp/uuid/:uuid/:appname` | ||||||
|  |  | ||||||
|  | ### Explanation | ||||||
|  |  | ||||||
|  | This API is used to create new user app record and if the request is successful it returns new `{token}`. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## User Session Api | ||||||
|  |  | ||||||
|  | | Resource / HTTP method    | POST (create)      | GET (read)     | PUT (update)   | DELETE (delete) | | ||||||
|  | | ------------------------- | ------------------ | -------------- | -------------- | --------------- | | ||||||
|  | | /v1/session/uuid/:uuid    | Create new session | Get sessions   | Error          | Error           | | ||||||
|  | | /v1/session/session/:uuid | Error              | Get session    | Error??        | Error??         | | ||||||
|  | | /v1/session/clientip/:ip  | Error              | Get sessions   | Error          | Error           | | ||||||
|  |  | ||||||
|  | ## Create user session record | ||||||
|  | ### `POST /v1/session/uuid/:uuid` | ||||||
|  |  | ||||||
|  | ### Explanation | ||||||
|  |  | ||||||
|  | This API is used to create new user session and if the request is successful it returns new `{session}`. | ||||||
|  | You can now use this id in your logs instead of user IP and browser user-agent info, etc... | ||||||
|  |  | ||||||
|  | Our API supports generation of session tokens based on the following information: | ||||||
|  | user ip, mobile device info, user agent, etc... | ||||||
|  |  | ||||||
|  | You can send the data as JSON POST or as regular POST parameters when working with this API. | ||||||
|  |  | ||||||
|  | ## Get user session record | ||||||
|  | ### `GET /v1/session/session/:session` | ||||||
|  |  | ||||||
|  | ### Explanation | ||||||
|  |  | ||||||
|  | This API returns session data. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Get session records by user token. | ||||||
|  | ### `GET /v1/session/uuid/:session` | ||||||
|  |  | ||||||
|  | ### Explanation | ||||||
|  |  | ||||||
|  | This API returns an array of sessions of the same user. | ||||||
|  |  | ||||||
|  | ## Get session records by ip address. | ||||||
|  | ### `GET /v1/session/clientip/:session` | ||||||
|  |  | ||||||
|  | ### Explanation | ||||||
|  |  | ||||||
|  | This API returns an array of user sessions by IP address. These sessions can be of different people. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Passwordless tokens API | ||||||
|  |  | ||||||
|  | | Resource / HTTP method | POST (create)     | GET (read)    | PUT (update)     | DELETE (delete)  | | ||||||
|  | | ---------------------- | ----------------- | ------------- | ---------------- | ---------------- | | ||||||
|  | | /v1/token/:uuid        | Create new record | Error         | Error            | Error            | | ||||||
|  | | /v1/token/:token       | Error             | Get data      | Error            | Error            | | ||||||
|  |  | ||||||
|  | 	router.POST("/v1/token/:token", e.userNewToken) | ||||||
|  | 	router.GET("/v1/token/:xtoken", e.userCheckToken) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Shareable token API | ||||||
|  |  | ||||||
|  | | Resource / HTTP method | POST (create)     | GET (read)    | PUT (update)     | DELETE (delete)  | | ||||||
|  | | ---------------------- | ----------------- | ------------- | ---------------- | ---------------- | | ||||||
|  | | /v1/shareable/:uuid    | Create new record | Error         | Error            | Error            | | ||||||
|  | | /v1/shareable/:token   | Error             | Get data      | Error            | Error            | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | **TODO-FINISH** | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Temporary user access tokens | ||||||
|  |  | ||||||
|  | Sometimes, for example, when working with 3rd party partners or semi-trusted environments, you might | ||||||
|  | need to generate a user access token with a specific expiration time. Your partner can retrieve user | ||||||
|  | information during this specific time only. | ||||||
|  | Afterward, access will be blocked. | ||||||
|  |  | ||||||
|  | The following command will generate a token to access user email and name for 7 days: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl --header "X-Bunker-Token: $TOKEN" -d 'fields=email,name' -d 'expiration=7d' -d 'partner=sms' \ | ||||||
|  |    https://bunker.company.com/gentokens/DAD2474A-E9A7-4BA7-BFC2-C4506880198E | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Output: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | 476E41E7-72AD-448A-BB43-7ACDB8C53735 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### 3rd party logging | ||||||
|  |  | ||||||
|  | Instead of maintaining internal logs, a lot of companies are using 3rd party logging facility like logz or coralogix or something else. | ||||||
|  | To improve adherence to GDPR, we build a special feature - generate specific session id for such 3rd party service. | ||||||
|  |  | ||||||
|  | When using these uuids in external systems, you basically **pseudonymise personal data**. In addition, in accordance with GDPR Article 5: | ||||||
|  | **Principles relating to processing of personal data**. Personal data shall be: (c)  | ||||||
|  | adequate, relevant and limited to what is necessary in relation to the purposes for which they are processed (‘**data minimisation**’); | ||||||
|  |  | ||||||
|  | Here is a command to do it: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl -d 'ip=user@example.com' \ | ||||||
|  |      -d 'user-agent=mozila' \ | ||||||
|  |      -d 'partner=coralogix' \ | ||||||
|  |      -d 'expiration=7d'\ | ||||||
|  |    https://bunker.company.com/gensession/DAD2474A-E9A7-4BA7-BFC2-C4506880198E | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | It will generate a new uuid, that you can now pass to 3rd party system as a user id. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## User consent management | ||||||
|  |  | ||||||
|  | One of the GDPR requirements is the storage of user consent. For example, your customer must approve to receive email marketing information. | ||||||
|  |  | ||||||
|  | Using the GDPR language, your customer must give explicit consent to receive marketing information. | ||||||
|  |  | ||||||
|  | Consent must be freely given, specific, informed and unambiguous. From GDPR, Article 7, item 3: | ||||||
|  |  | ||||||
|  | * **The data subject shall have the right to withdraw his or her consent at any time.** | ||||||
|  | * **It shall be as easy to withdraw as to give consent.** | ||||||
|  |  | ||||||
|  | To comply with this requirement, we added support to manage user consent. We support the following APIs: | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### List granted | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl --header "X-Bunker-Token: $TOKEN" \ | ||||||
|  |    https://bunker.company.com/consent/DAD2474A-E9A7-4BA7-BFC2-C4506880198E | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### List all | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl --header "X-Bunker-Token: $TOKEN" \ | ||||||
|  |    https://bunker.company.com/consent/DAD2474A-E9A7-4BA7-BFC2-C4506880198E?all | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Cancel consent | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | curl --header "X-Bunker-Token: $TOKEN" -XDELETE \ | ||||||
|  |    https://bunker.company.com/consent/DAD2474A-E9A7-4BA7-BFC2-C4506880198E/<consent-id> | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### User gives consent | ||||||
|  |  | ||||||
|  | **TODO** | ||||||
|  |  | ||||||
|  | ### Easily cancel consent for email marketing | ||||||
|  |  | ||||||
|  | For example, for email marketing, users got distracted, when they need to login in order to unsubscribe from the newsletter. | ||||||
|  | To simplify this operation, users will be allowed to unsubscribe only using email address without full login operation. | ||||||
|  |  | ||||||
|  | ### Unlock bunker | ||||||
|  |  | ||||||
|  | Run the following command with different keys: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | bunker unlock **key** | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Or you can provide multiple keys at once: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | bunker unlock key1 key2 key3 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### View lock status | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | bunker status | jq .lock | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Result: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | locked | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Audit API | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | It is not compliant, unless you have a real reason to share this specific personal sub-record. For example, | ||||||
|  | sending customer phone when notifying customer using 3rd party SMS gateway. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # SECTION IS NOT UPDATED BELLOW | ||||||
|  |  | ||||||
|  | ## Data Bunker init | ||||||
|  |  | ||||||
|  | Upon initial init, the Data Bunker service will check if the system is initialized for the first time, and if yes, | ||||||
|  | it will generate root password, master key and derived keys out of it. Otherwise, an error will be printed. | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | bunker init | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Output: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | Root password: 123456 | ||||||
|  | Key1: abcdefg | ||||||
|  | Key2: abcdefg | ||||||
|  | key3: abcdefg | ||||||
|  | Key4: abcdefg | ||||||
|  | Key5: abcdefg | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **TODO**: Secret keys printed to output can be easily extracted in cloud environments for example in Kubernetes logs! | ||||||
							
								
								
									
										10
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										10
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | # build without debug | ||||||
|  | go build -ldflags "-w" -o databunker ./src/bunker.go ./src/qldb.go ./src/audit_db.go ./src/audit_api.go \ | ||||||
|  |   ./src/utils.go ./src/cryptor.go \ | ||||||
|  |   ./src/sms.go ./src/email.go \ | ||||||
|  |   ./src/users_db.go ./src/users_api.go \ | ||||||
|  |   ./src/userapps_db.go ./src/userapps_api.go \ | ||||||
|  |   ./src/sessions_db.go \ | ||||||
|  |   ./src/consent_db.go ./src/consent_api.go \ | ||||||
|  |   ./src/xtokens_db.go ./src/xtokens_api.go | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								databunker.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								databunker.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | # Server configurations | ||||||
|  | generic: | ||||||
|  |   # allow to create user object without login | ||||||
|  |   create_user_without_token: true | ||||||
|  |   #notification_url: "http://localhost/" | ||||||
|  | sms: | ||||||
|  |   # default country when sending out SMSM | ||||||
|  |   twilio_account: "AC" | ||||||
|  |   twilio_token: "af" | ||||||
|  |   twilio_from: "+180" | ||||||
|  |   default_country: "UK" | ||||||
|  | server: | ||||||
|  |   host: "localhost" | ||||||
|  |   port: 3000 | ||||||
|  | smtp: | ||||||
|  |   server: "smtp.eu.mailgun.org" | ||||||
|  |   port: 587 | ||||||
|  |   user: "postmaster@mg.your-company.com" | ||||||
|  |   pass: "b6" | ||||||
|  |   sender: "botdatabunker.your-company.com" | ||||||
							
								
								
									
										
											BIN
										
									
								
								images/create-user-app-record.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								images/create-user-app-record.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 146 KiB | 
							
								
								
									
										
											BIN
										
									
								
								images/create-user-session-flow.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								images/create-user-session-flow.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 122 KiB | 
							
								
								
									
										
											BIN
										
									
								
								images/create-user-token-flow.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								images/create-user-token-flow.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 195 KiB | 
							
								
								
									
										
											BIN
										
									
								
								images/data-bunker-tables.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								images/data-bunker-tables.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 50 KiB | 
							
								
								
									
										
											BIN
										
									
								
								images/new-style-solution.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								images/new-style-solution.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 85 KiB | 
							
								
								
									
										
											BIN
										
									
								
								images/old-style-solution.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								images/old-style-solution.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 37 KiB | 
							
								
								
									
										42
									
								
								src/audit_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/audit_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/julienschmidt/httprouter" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (e mainEnv) getAuditEvents(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	userTOKEN := ps.ByName("token") | ||||||
|  | 	event := audit("view audit events", userTOKEN) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  | 	//fmt.Println("error code") | ||||||
|  | 	if enforceUUID(w, userTOKEN, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	var offset int32 | ||||||
|  | 	var limit int32 = 10 | ||||||
|  | 	args := r.URL.Query() | ||||||
|  | 	if value, ok := args["offset"]; ok { | ||||||
|  | 		offset = atoi(value[0]) | ||||||
|  | 	} | ||||||
|  | 	if value, ok := args["limit"]; ok { | ||||||
|  | 		limit = atoi(value[0]) | ||||||
|  | 	} | ||||||
|  | 	resultJSON, counter, err := e.db.getAuditEvents(userTOKEN, offset, limit) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("Total count of events: %d\n", counter) | ||||||
|  | 	//fmt.Fprintf(w, "<html><head><title>title</title></head>") | ||||||
|  | 	w.Header().Set("Access-Control-Allow-Origin", "*") | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	str := fmt.Sprintf(`{"total":%d,"rows":%s}`, counter, resultJSON) | ||||||
|  | 	w.Write([]byte(str)) | ||||||
|  | } | ||||||
							
								
								
									
										98
									
								
								src/audit_db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/audit_db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type auditEvent struct { | ||||||
|  | 	When   int32  `json:"when"` | ||||||
|  | 	Who    string `json:"who"` | ||||||
|  | 	Record string `json:"record"` | ||||||
|  | 	App    string `json:"app"` | ||||||
|  | 	Title  string `json:"title"` | ||||||
|  | 	Status string `json:"status"` | ||||||
|  | 	Msg    string `json:"msg"` | ||||||
|  | 	Debug  string `json:"debug"` | ||||||
|  | 	Before string `json:"before"` | ||||||
|  | 	After  string `json:"after"` | ||||||
|  | 	Meta   string `json:"meta"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func audit(title string, record string) *auditEvent { | ||||||
|  | 	fmt.Printf("/%s : %s\n", title, record) | ||||||
|  | 	return &auditEvent{Title: title, Record: record, Status: "ok", When: int32(time.Now().Unix())} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func auditApp(title string, record string, app string) *auditEvent { | ||||||
|  | 	fmt.Printf("/%s : %s : %s\n", title, app, record) | ||||||
|  | 	return &auditEvent{Title: title, Record: record, Status: "ok", When: int32(time.Now().Unix())} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (event auditEvent) submit(db dbcon) { | ||||||
|  | 	//fmt.Println("submit event to audit!!!!!!!!!!") | ||||||
|  | 	/* | ||||||
|  | 		bdoc, err := bson.Marshal(event) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Printf("failed to marshal audit event: %s\n", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		var bdoc2 bson.M | ||||||
|  | 		err = bson.Unmarshal(bdoc, &bdoc2) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Printf("failed to marshal audit event2: %s\n", err) | ||||||
|  | 			return | ||||||
|  | 		}*/ | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	bdoc["when"] = event.When | ||||||
|  | 	if len(event.Who) > 0 { | ||||||
|  | 		bdoc["who"] = event.Who | ||||||
|  | 	} | ||||||
|  | 	if len(event.Record) > 0 { | ||||||
|  | 		bdoc["record"] = event.Record | ||||||
|  | 	} | ||||||
|  | 	if len(event.App) > 0 { | ||||||
|  | 		bdoc["app"] = event.App | ||||||
|  | 	} | ||||||
|  | 	if len(event.Title) > 0 { | ||||||
|  | 		bdoc["title"] = event.Title | ||||||
|  | 	} | ||||||
|  | 	bdoc["status"] = event.Status | ||||||
|  | 	if len(event.Msg) > 0 { | ||||||
|  | 		bdoc["msg"] = event.Msg | ||||||
|  | 	} | ||||||
|  | 	if len(event.Debug) > 0 { | ||||||
|  | 		bdoc["debug"] = event.Debug | ||||||
|  | 	} | ||||||
|  | 	if len(event.Before) > 0 { | ||||||
|  | 		bdoc["before"] = event.Before | ||||||
|  | 	} | ||||||
|  | 	if len(event.After) > 0 { | ||||||
|  | 		bdoc["after"] = event.After | ||||||
|  | 	} | ||||||
|  | 	if len(event.Meta) > 0 { | ||||||
|  | 		bdoc["meta"] = event.Meta | ||||||
|  | 	} | ||||||
|  | 	_, err := db.createRecord(TblName.Audit, &bdoc) | ||||||
|  | 	//_, err := db.audit.InsertOne(context.TODO(), &bdoc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("failed to marshal audit event: %s\n", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	//fmt.Printf("done!!!") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getAuditEvents(userTOKEN string, offset int32, limit int32) ([]byte, int64, error) { | ||||||
|  | 	//var results []*auditEvent | ||||||
|  | 	count, err := dbobj.countRecords(TblName.Audit, "record", userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, 0, err | ||||||
|  | 	} | ||||||
|  | 	records, err := dbobj.getList(TblName.Audit, "record", userTOKEN, offset, limit) | ||||||
|  | 	resultJSON, err := json.Marshal(records) | ||||||
|  | 	//fmt.Printf("Found multiple documents (array of pointers): %+v\n", results) | ||||||
|  | 	return resultJSON, count, nil | ||||||
|  | } | ||||||
							
								
								
									
										255
									
								
								src/bunker.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								src/bunker.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,255 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"flag" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
|  | 	"math/rand" | ||||||
|  | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/gobuffalo/packr" | ||||||
|  | 	"github.com/julienschmidt/httprouter" | ||||||
|  | 	"github.com/kelseyhightower/envconfig" | ||||||
|  | 	"github.com/prometheus/client_golang/prometheus" | ||||||
|  | 	"github.com/prometheus/client_golang/prometheus/promhttp" | ||||||
|  | 	yaml "gopkg.in/yaml.v2" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Tbl = int | ||||||
|  |  | ||||||
|  | type listTbls struct { | ||||||
|  | 	Users    Tbl | ||||||
|  | 	Audit    Tbl | ||||||
|  | 	Xtokens  Tbl | ||||||
|  | 	Consent  Tbl | ||||||
|  | 	Sessions Tbl | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Enum for public use | ||||||
|  | var TblName = &listTbls{ | ||||||
|  | 	Users:    0, | ||||||
|  | 	Audit:    1, | ||||||
|  | 	Xtokens:  2, | ||||||
|  | 	Consent:  3, | ||||||
|  | 	Sessions: 4, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Config struct { | ||||||
|  | 	Generic struct { | ||||||
|  | 		Create_user_without_token bool `yaml:"create_user_without_token"` | ||||||
|  | 	} | ||||||
|  | 	Sms struct { | ||||||
|  | 		Default_country string `yaml:"default_country"` | ||||||
|  | 		Twilio_account  string `yaml:"twilio_account"` | ||||||
|  | 		Twilio_token    string `yaml:"twilio_token"` | ||||||
|  | 		Twilio_from     string `yaml:"twilio_from"` | ||||||
|  | 	} | ||||||
|  | 	Server struct { | ||||||
|  | 		Port string `yaml:"port", envconfig:"BUNKER_PORT"` | ||||||
|  | 		Host string `yaml:"host", envconfig:"BUNKER_HOST"` | ||||||
|  | 	} `yaml:"server"` | ||||||
|  | 	Smtp struct { | ||||||
|  | 		Server string `yaml:"server", envconfig:"SMTP_SERVER"` | ||||||
|  | 		Port   string `yaml:"port", envconfig:"SMTP_PORT"` | ||||||
|  | 		User   string `yaml:"user", envconfig:"SMTP_USER"` | ||||||
|  | 		Pass   string `yaml:"pass", envconfig:"SMTP_PASS"` | ||||||
|  | 		Sender string `yaml:"sender", envconfig:"SMTP_SENDER"` | ||||||
|  | 	} `yaml:"smtp"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type mainEnv struct { | ||||||
|  | 	db   dbcon | ||||||
|  | 	conf Config | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type userJSON struct { | ||||||
|  | 	jsonData []byte | ||||||
|  | 	loginIdx string | ||||||
|  | 	emailIdx string | ||||||
|  | 	phoneIdx string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type tokenAuthResult struct { | ||||||
|  | 	ttype   string | ||||||
|  | 	name    string | ||||||
|  | 	token   string | ||||||
|  | 	fields  string | ||||||
|  | 	appName string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func prometheusHandler() http.Handler { | ||||||
|  | 	handlerOptions := promhttp.HandlerOpts{ | ||||||
|  | 		ErrorHandling:      promhttp.ContinueOnError, | ||||||
|  | 		DisableCompression: true, | ||||||
|  | 	} | ||||||
|  | 	promHandler := promhttp.HandlerFor(prometheus.DefaultGatherer, handlerOptions) | ||||||
|  | 	return promHandler | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) metrics(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	fmt.Printf("/metrics\n") | ||||||
|  | 	//w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	//fmt.Fprintf(w, `{"status":"ok","apps":%q}`, result) | ||||||
|  | 	//fmt.Fprintf(w, "hello") | ||||||
|  | 	//promhttp.Handler().ServeHTTP(w, r) | ||||||
|  | 	prometheusHandler().ServeHTTP(w, r) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) index(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	fmt.Printf("Index access\n") | ||||||
|  | 	/* | ||||||
|  | 		if r.Method != "GET" { | ||||||
|  | 			http.Error(w, http.StatusText(405), 405) | ||||||
|  | 			log.Panic("Method %s", r.Method) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	*/ | ||||||
|  | 	fmt.Fprintf(w, "<html><head><title>title</title></head></html>") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) setupRouter() *httprouter.Router { | ||||||
|  |  | ||||||
|  | 	box := packr.NewBox("../ui") | ||||||
|  |  | ||||||
|  | 	router := httprouter.New() | ||||||
|  | 	router.POST("/v1/user", e.userNew) | ||||||
|  | 	router.GET("/v1/user/:index/:code", e.userGet) | ||||||
|  | 	router.DELETE("/v1/user/:index/:code", e.userDelete) | ||||||
|  | 	router.PUT("/v1/user/:index/:code", e.userChange) | ||||||
|  |  | ||||||
|  | 	router.GET("/v1/login/:index/:code", e.userLogin) | ||||||
|  | 	router.GET("/v1/enter/:index/:code/:tmp", e.userLoginEnter) | ||||||
|  |  | ||||||
|  | 	router.POST("/v1/xtoken/:token", e.userNewToken) | ||||||
|  | 	router.GET("/v1/xtoken/:xtoken", e.userCheckToken) | ||||||
|  |  | ||||||
|  | 	router.GET("/v1/consent/:index/:code", e.consentList) | ||||||
|  | 	router.POST("/v1/consent/:index/:code", e.consentAccept) | ||||||
|  | 	//router.PATCH("/v1/consent/:index/:code", e.consentCancel) | ||||||
|  | 	router.DELETE("/v1/consent/:index/:code", e.consentCancel) | ||||||
|  |  | ||||||
|  | 	router.POST("/v1/userapp/token/:token/:appname", e.userappNew) | ||||||
|  | 	router.GET("/v1/userapp/token/:token/:appname", e.userappGet) | ||||||
|  | 	router.PUT("/v1/userapp/token/:token/:appname", e.userappChange) | ||||||
|  | 	router.GET("/v1/userapp/token/:token", e.userappList) | ||||||
|  | 	router.GET("/v1/userapp/list", e.appList) | ||||||
|  |  | ||||||
|  | 	router.GET("/v1/metrics", e.metrics) | ||||||
|  |  | ||||||
|  | 	router.GET("/v1/audit/list/:token", e.getAuditEvents) | ||||||
|  |  | ||||||
|  | 	router.GET("/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 		data, err := box.Find("index.html") | ||||||
|  | 		if err != nil { | ||||||
|  | 			//log.Panic("error %s", err.Error()) | ||||||
|  | 			fmt.Printf("404 %s, error: %s\n", r.URL.Path, err.Error()) | ||||||
|  | 			w.WriteHeader(404) | ||||||
|  | 		} else { | ||||||
|  | 			//fmt.Printf("return static file: %s\n", data) | ||||||
|  | 			fmt.Printf("200 %s\n", r.URL.Path) | ||||||
|  | 			w.WriteHeader(200) | ||||||
|  | 			w.Write([]byte(data)) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 	router.GET("/site/*filepath", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 		data, err := box.Find(r.URL.Path) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Printf("404 GET %s\n", r.URL.Path) | ||||||
|  | 			w.WriteHeader(404) | ||||||
|  | 		} else { | ||||||
|  | 			//w.Header().Set("Access-Control-Allow-Origin", "*") | ||||||
|  | 			if strings.HasSuffix(r.URL.Path, ".css") { | ||||||
|  | 				w.Header().Set("Content-Type", "text/css") | ||||||
|  | 			} else if strings.HasSuffix(r.URL.Path, ".js") { | ||||||
|  | 				w.Header().Set("Content-Type", "text/javascript") | ||||||
|  | 			} | ||||||
|  | 			// text/plain | ||||||
|  | 			fmt.Printf("200 %s\n", r.URL.Path) | ||||||
|  | 			w.WriteHeader(200) | ||||||
|  | 			w.Write([]byte(data)) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 	router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		w.Header().Set("Content-Type", "text/plain; charset=utf-8") | ||||||
|  | 		w.WriteHeader(http.StatusNotFound) | ||||||
|  | 		w.Write([]byte("url not found")) | ||||||
|  | 		fmt.Printf("404 %s %s\n", r.Method, r.URL.Path) | ||||||
|  | 	}) | ||||||
|  | 	return router | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func readFile(cfg *Config) error { | ||||||
|  | 	f, err := os.Open("databunker.yaml") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	decoder := yaml.NewDecoder(f) | ||||||
|  | 	err = decoder.Decode(cfg) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | func readEnv(cfg *Config) error { | ||||||
|  | 	err := envconfig.Process("", cfg) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  | 	rand.Seed(time.Now().UnixNano()) | ||||||
|  | 	fmt.Println("***MAIN***") | ||||||
|  | 	lockMemory() | ||||||
|  | 	var cfg Config | ||||||
|  | 	readFile(&cfg) | ||||||
|  | 	readEnv(&cfg) | ||||||
|  | 	fmt.Printf("%+v\n", cfg) | ||||||
|  | 	initPtr := flag.Bool("init", false, "a bool") | ||||||
|  | 	masterKeyPtr := flag.String("masterkey", "", "master key") | ||||||
|  | 	flag.Parse() | ||||||
|  | 	var err error | ||||||
|  | 	var masterKey []byte | ||||||
|  | 	if *initPtr { | ||||||
|  | 		fmt.Println("Init") | ||||||
|  | 		masterKey, err = generateMasterKey() | ||||||
|  | 		fmt.Printf("Master key: %x\n", masterKey) | ||||||
|  | 	} else if masterKeyPtr != nil && len(*masterKeyPtr) > 0 { | ||||||
|  | 		masterKey, err = hex.DecodeString(*masterKeyPtr) | ||||||
|  | 	} else { | ||||||
|  | 		fmt.Println("Run ./databunker -init for the firts time.") | ||||||
|  | 		log.Fatal("Masterkey is missing. Run ./databunker -masterkey key") | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		//log.Panic("error %s", err.Error()) | ||||||
|  | 		fmt.Printf("error %s", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	db, _ := newDB(masterKey, nil) | ||||||
|  | 	if *initPtr { | ||||||
|  | 		fmt.Println("Init") | ||||||
|  | 		db.initDB() | ||||||
|  | 		rootToken, err := db.createRootToken() | ||||||
|  | 		if err != nil { | ||||||
|  | 			//log.Panic("error %s", err.Error()) | ||||||
|  | 			fmt.Printf("error %s", err.Error()) | ||||||
|  | 		} | ||||||
|  | 		fmt.Printf("Root token: %s\n", rootToken) | ||||||
|  | 	} | ||||||
|  | 	db.initUserApps() | ||||||
|  | 	e := mainEnv{db, cfg} | ||||||
|  | 	fmt.Printf("host %s\n", cfg.Server.Host+":"+cfg.Server.Port) | ||||||
|  | 	router := e.setupRouter() | ||||||
|  | 	if _, err := os.Stat("./server.key"); !os.IsNotExist(err) { | ||||||
|  | 		//TODO | ||||||
|  | 		fmt.Printf("Loading ssl\n") | ||||||
|  | 		err := http.ListenAndServeTLS(":443", "server.ctr", "server.key", router) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatal("ListenAndServe: ", err) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		log.Fatal(http.ListenAndServe(cfg.Server.Host+":"+cfg.Server.Port, router)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										115
									
								
								src/bunker_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/bunker_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/julienschmidt/httprouter" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestCreateAPIUser(t *testing.T) { | ||||||
|  | 	masterKey, _ := hex.DecodeString("71c65924336c5e6f41129b6f0540ad03d2a8bf7e9b10db72") | ||||||
|  | 	db, _ := newDB(masterKey, nil) | ||||||
|  | 	var cfg Config | ||||||
|  | 	e := mainEnv{db, cfg} | ||||||
|  |  | ||||||
|  | 	rootToken, err := e.db.getRootToken() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to retreave root token: %s\n", err) | ||||||
|  | 	} | ||||||
|  | 	userJSON := `{"login":"abcdefg","name":"tom","pass":"mylittlepony","k1":[1,10,20],"k2":{"f1":"t1","f3":{"a":"b"}},"admin":true}` | ||||||
|  |  | ||||||
|  | 	request := httptest.NewRequest("POST", "/user", strings.NewReader(userJSON)) | ||||||
|  | 	request.Header.Set("Content-Type", "application/json") | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  | 	//var resp http.ResponseWriter | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	var ps httprouter.Params | ||||||
|  | 	e.userNew(rr, request, ps) | ||||||
|  |  | ||||||
|  | 	//fmt.Printf("After create user------------------\n%s\n\n\n", rr.Body) | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	err = json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to parse json response on user create: %s\n", err) | ||||||
|  | 	} | ||||||
|  | 	var userTOKEN string | ||||||
|  | 	if status, ok := raw["status"]; ok { | ||||||
|  | 		if status == "error" { | ||||||
|  | 			if strings.HasPrefix(raw["message"].(string), "duplicate") { | ||||||
|  | 				_, userTOKEN, _ = e.db.getUserIndex("abcdefg", "login") | ||||||
|  | 				fmt.Printf("user already exists: %s\n", userTOKEN) | ||||||
|  | 			} else { | ||||||
|  | 				t.Fatalf("Failed to create user: %s\n", raw["message"]) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} else if status == "ok" { | ||||||
|  | 			userTOKEN = raw["token"].(string) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(userTOKEN) == 0 { | ||||||
|  | 		t.Fatalf("Failed to parse user UUID") | ||||||
|  | 	} | ||||||
|  | 	p2 := httprouter.Param{"token", userTOKEN} | ||||||
|  | 	ps2 := []httprouter.Param{p2} | ||||||
|  |  | ||||||
|  | 	pars := `{"expiration":"1d","fields":"uuid,name,pass,k1,k2.f3"}` | ||||||
|  | 	request = httptest.NewRequest("POST", "/user", strings.NewReader(pars)) | ||||||
|  | 	request.Header.Set("Content-Type", "application/json") | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  | 	//var resp http.ResponseWriter | ||||||
|  | 	rr = httptest.NewRecorder() | ||||||
|  | 	e.userNewToken(rr, request, ps2) | ||||||
|  | 	//fmt.Printf("after create token------------------\n%s\n\n\n", rr.Body) | ||||||
|  | 	err = json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("Failed to parse json response on user create: %s\n", err) | ||||||
|  | 	} | ||||||
|  | 	tokenUUID := "" | ||||||
|  | 	if status, ok := raw["status"]; ok { | ||||||
|  | 		if status == "error" { | ||||||
|  | 			t.Fatalf("Failed to create user token: %s\n", raw["message"]) | ||||||
|  | 			return | ||||||
|  | 		} else if status == "ok" { | ||||||
|  | 			tokenUUID = raw["xtoken"].(string) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(tokenUUID) == 0 { | ||||||
|  | 		t.Fatalf("Failed to retreave user token: %s\n", rr.Body) | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("User token: %s\n", tokenUUID) | ||||||
|  |  | ||||||
|  | 	request = httptest.NewRequest("GET", "/user", nil) | ||||||
|  | 	request.Header.Set("Content-Type", "application/json") | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  | 	//var resp http.ResponseWriter | ||||||
|  | 	rr = httptest.NewRecorder() | ||||||
|  |  | ||||||
|  | 	p3 := httprouter.Param{"xtoken", tokenUUID} | ||||||
|  | 	ps3 := []httprouter.Param{p3} | ||||||
|  | 	e.userCheckToken(rr, request, ps3) | ||||||
|  | 	fmt.Printf("get by token------------------\n%s\n\n\n", rr.Body) | ||||||
|  | 	err = json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("Failed to parse json response on user create: %s\n", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	request = httptest.NewRequest("DELETE", "/user", nil) | ||||||
|  | 	request.Header.Set("Content-Type", "application/json") | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  | 	//var resp http.ResponseWriter | ||||||
|  | 	rr = httptest.NewRecorder() | ||||||
|  | 	p4 := httprouter.Param{"code", userTOKEN} | ||||||
|  | 	p5 := httprouter.Param{"index", "token"} | ||||||
|  | 	ps4 := []httprouter.Param{p4, p5} | ||||||
|  | 	e.userDelete(rr, request, ps4) | ||||||
|  | 	fmt.Printf("after userDelete------------------\n%s\n\n\n", rr.Body) | ||||||
|  | 	err = json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("Failed to parse json response on user create: %s\n", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										132
									
								
								src/consent_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/consent_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"reflect" | ||||||
|  |  | ||||||
|  | 	"github.com/julienschmidt/httprouter" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (e mainEnv) consentAccept(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	var err error | ||||||
|  | 	code := ps.ByName("code") | ||||||
|  | 	index := ps.ByName("index") | ||||||
|  | 	event := audit("consent accept by "+index, code) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	userTOKEN := "" | ||||||
|  | 	if index == "token" { | ||||||
|  | 		if enforceUUID(w, code, event) == false { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		userBson, _ := e.db.lookupUserRecord(code) | ||||||
|  | 		if userBson != nil { | ||||||
|  | 			userTOKEN = code | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		// TODO: decode url in code! | ||||||
|  | 		userBson, _ := e.db.lookupUserRecordByIndex(index, code) | ||||||
|  | 		if userBson != nil { | ||||||
|  | 			userTOKEN = userBson["token"].(string) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	defer func() { | ||||||
|  | 		w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 		w.WriteHeader(200) | ||||||
|  | 		w.Write([]byte(`{"status":"ok"}`)) | ||||||
|  | 	}() | ||||||
|  | 	records, err := getJSONPostData(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		//returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	brief := "" | ||||||
|  | 	message := "" | ||||||
|  | 	status := "accept" | ||||||
|  | 	if value, ok := records["brief"]; ok { | ||||||
|  | 		if reflect.TypeOf(value) == reflect.TypeOf("string") { | ||||||
|  | 			brief = value.(string) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if value, ok := records["message"]; ok { | ||||||
|  | 		if reflect.TypeOf(value) == reflect.TypeOf("string") { | ||||||
|  | 			message = value.(string) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if value, ok := records["status"]; ok { | ||||||
|  | 		if reflect.TypeOf(value) == reflect.TypeOf("string") { | ||||||
|  | 			status = value.(string) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(brief) == 0 { | ||||||
|  | 		//returnError(w, r, "internal error", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if len(message) == 0 { | ||||||
|  | 		message = brief | ||||||
|  | 	} | ||||||
|  | 	e.db.createConsentRecord(userTOKEN, index, code, brief, message, status) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) consentCancel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	code := ps.ByName("code") | ||||||
|  | 	index := ps.ByName("index") | ||||||
|  | 	event := audit("consent cancel by "+index, code) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  | 	userTOKEN := code | ||||||
|  | 	if enforceUUID(w, userTOKEN, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	// make sure that user is logged in here, unless he wants to cancel emails | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	records, err := getJSONPostData(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	brief := "" | ||||||
|  | 	if value, ok := records["brief"]; ok { | ||||||
|  | 		if reflect.TypeOf(value) == reflect.TypeOf("string") { | ||||||
|  | 			brief = value.(string) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(brief) == 0 { | ||||||
|  | 		returnError(w, r, "consent brief code is missing", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	e.db.cancelConsentRecord(userTOKEN, brief) | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	w.Write([]byte(`{"status":"ok"}`)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) consentList(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	code := ps.ByName("code") | ||||||
|  | 	index := ps.ByName("index") | ||||||
|  | 	event := audit("consent list of events by "+index, code) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  | 	userTOKEN := code | ||||||
|  | 	if enforceUUID(w, userTOKEN, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	// make sure that user is logged in here, unless he wants to cancel emails | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resultJSON, numRecords, err := e.db.listConsentRecords(userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("Total count of rows: %d\n", numRecords) | ||||||
|  | 	//fmt.Fprintf(w, "<html><head><title>title</title></head>") | ||||||
|  | 	w.Header().Set("Access-Control-Allow-Origin", "*") | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	str := fmt.Sprintf(`{"status":"ok","total":%d,"rows":%s}`, numRecords, resultJSON) | ||||||
|  | 	w.Write([]byte(str)) | ||||||
|  | } | ||||||
							
								
								
									
										86
									
								
								src/consent_db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/consent_db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/fatih/structs" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type consentEvent struct { | ||||||
|  | 	When    int32  `json:"when,omitempty" structs:"when"` | ||||||
|  | 	Who     string `json:"who,omitempty" structs:"who"` | ||||||
|  | 	Type    string `json:"type,omitempty" structs:"type"` | ||||||
|  | 	Token   string `json:"token,omitempty" structs:"token"` | ||||||
|  | 	Brief   string `json:"brief,omitempty" structs:"brief"` | ||||||
|  | 	Message string `json:"message,omitempty" structs:"message"` | ||||||
|  | 	Status  string `json:"status,omitempty" structs:"status"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) createConsentRecord(userTOKEN string, usertype string, usercode string, brief string, message string, status string) { | ||||||
|  | 	now := int32(time.Now().Unix()) | ||||||
|  | 	// brief can not be too long, may be hash it ? | ||||||
|  | 	if len(brief) > 64 { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if len(userTOKEN) > 0 { | ||||||
|  | 		// first check if this consent exists, then update | ||||||
|  | 		raw, err := dbobj.getRecord2(TblName.Consent, "token", userTOKEN, "brief", brief) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Printf("error to find:%s", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if raw != nil { | ||||||
|  | 			fmt.Println("update rec") | ||||||
|  | 			// update date, status | ||||||
|  | 			bdoc := bson.M{} | ||||||
|  | 			bdoc["when"] = now | ||||||
|  | 			bdoc["status"] = status | ||||||
|  | 			dbobj.updateRecord2(TblName.Consent, "token", userTOKEN, "brief", brief, &bdoc, nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	ev := consentEvent{ | ||||||
|  | 		When:    now, | ||||||
|  | 		Who:     usercode, | ||||||
|  | 		Token:   userTOKEN, | ||||||
|  | 		Type:    usertype, | ||||||
|  | 		Brief:   brief, | ||||||
|  | 		Message: message, | ||||||
|  | 		Status:  status, | ||||||
|  | 	} | ||||||
|  | 	// in any case - insert record | ||||||
|  | 	fmt.Printf("insert consent record\n") | ||||||
|  | 	dbobj.createRecord(TblName.Consent, structs.Map(ev)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) cancelConsentRecord(userTOKEN string, brief string) error { | ||||||
|  | 	// brief can not be too long, may be hash it ? | ||||||
|  | 	if len(brief) > 64 { | ||||||
|  | 		return errors.New("Brief value is too long") | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("%s %s\n", userTOKEN, brief) | ||||||
|  | 	now := int32(time.Now().Unix()) | ||||||
|  | 	// update date, status | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	bdoc["when"] = now | ||||||
|  | 	bdoc["status"] = "cancel" | ||||||
|  | 	dbobj.updateRecord2(TblName.Consent, "token", userTOKEN, "brief", brief, &bdoc, nil) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // link consent to user? | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) listConsentRecords(userTOKEN string) ([]byte, int, error) { | ||||||
|  | 	records, err := dbobj.getList(TblName.Consent, "token", userTOKEN, 0, 0) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, 0, err | ||||||
|  | 	} | ||||||
|  | 	count := len(records) | ||||||
|  | 	resultJSON, err := json.Marshal(records) | ||||||
|  | 	//fmt.Printf("Found multiple documents (array of pointers): %+v\n", results) | ||||||
|  | 	return resultJSON, count, nil | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								src/cryptor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/cryptor.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/aes" | ||||||
|  | 	"crypto/cipher" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"io" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // shamir secret split | ||||||
|  | // https://github.com/hashicorp/vault/tree/master/shamir | ||||||
|  | // https://github.com/kinvolk/go-shamir | ||||||
|  | // go get github.com/hashicorp/vault/shamir | ||||||
|  |  | ||||||
|  | func generateRecordKey() ([]byte, error) { | ||||||
|  | 	key := make([]byte, 8) | ||||||
|  | 	if _, err := io.ReadFull(rand.Reader, key); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return key, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // generate master key - 24 bytes length | ||||||
|  | func generateMasterKey() ([]byte, error) { | ||||||
|  | 	masterKey := make([]byte, 24) | ||||||
|  | 	_, err := io.ReadFull(rand.Reader, masterKey) | ||||||
|  | 	return masterKey, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decrypt(masterKey []byte, userKey []byte, data []byte) ([]byte, error) { | ||||||
|  | 	// Load your secret key from a safe place and reuse it across multiple | ||||||
|  | 	// Seal/Open calls. (Obviously don't use this example key for anything | ||||||
|  | 	// real.) If you want to convert a passphrase to a key, use a suitable | ||||||
|  | 	// package like bcrypt or scrypt. | ||||||
|  | 	// When decoded the key should be 16 bytes (AES-128) or 32 (AES-256). | ||||||
|  | 	key := append(masterKey, userKey...) | ||||||
|  |  | ||||||
|  | 	block, err := aes.NewCipher(key) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	aesgcm, err := cipher.NewGCM(block) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	ciphertext := data[0 : len(data)-12] | ||||||
|  | 	nonce := data[len(data)-12:] | ||||||
|  | 	plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) | ||||||
|  | 	return plaintext, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func encrypt(masterKey []byte, userKey []byte, plaintext []byte) ([]byte, error) { | ||||||
|  | 	// We use 32 byte key (AES-256). | ||||||
|  | 	// comprising 24 master key | ||||||
|  | 	// and 8 bytes record key | ||||||
|  | 	key := append(masterKey, userKey...) | ||||||
|  | 	block, err := aes.NewCipher(key) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	// Never use more than 2^32 random nonces with a given key because of the risk of a repeat. | ||||||
|  | 	nonce := make([]byte, 12) | ||||||
|  | 	if _, err := io.ReadFull(rand.Reader, nonce); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	aesgcm, err := cipher.NewGCM(block) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) | ||||||
|  | 	//fmt.Printf("%x\n", ciphertext) | ||||||
|  | 	// apppend random nonce bvalue to the end | ||||||
|  | 	ciphertext = append(ciphertext, nonce...) | ||||||
|  | 	return ciphertext, nil | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								src/email.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/email.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/smtp" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func sendCodeByEmail(code string, address string, cfg Config) { | ||||||
|  | 	/* | ||||||
|  | 		c, err := smtp.Dial(smtpServer) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 		defer c.Close() | ||||||
|  | 		// Set the sender and recipient. | ||||||
|  | 		c.Mail("bot@paranoidguy.com") | ||||||
|  | 		c.Rcpt(address) | ||||||
|  | 		// Send the email body. | ||||||
|  | 		wc, err := c.Data() | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatal(err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		defer wc.Close() | ||||||
|  | 		buf := bytes.NewBufferString("This is the email body.") | ||||||
|  | 		if _, err = buf.WriteTo(wc); err != nil { | ||||||
|  | 			log.Fatal(err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	*/ | ||||||
|  | 	Dest := []string{"stremovsky@gmail.com", address} | ||||||
|  | 	Subject := "Access Code" | ||||||
|  | 	bodyMessage := "Data bunker access code is " + code | ||||||
|  | 	msg := "From: " + cfg.Smtp.Sender + "\n" + | ||||||
|  | 		"To: " + strings.Join(Dest, ",") + "\n" + | ||||||
|  | 		"Subject: " + Subject + "\n" + bodyMessage | ||||||
|  |  | ||||||
|  | 	err := smtp.SendMail(cfg.Smtp.Server+":"+cfg.Smtp.Port, | ||||||
|  | 		smtp.PlainAuth("", cfg.Smtp.User, cfg.Smtp.Pass, cfg.Smtp.Server), | ||||||
|  | 		cfg.Smtp.User, Dest, []byte(msg)) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("smtp error: %s", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fmt.Println("Mail sent successfully!") | ||||||
|  | } | ||||||
							
								
								
									
										747
									
								
								src/qldb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										747
									
								
								src/qldb.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,747 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | // This project is using the following golang internal database: | ||||||
|  | // https://godoc.org/modernc.org/ql | ||||||
|  |  | ||||||
|  | // go build modernc.org/ql/ql | ||||||
|  | // go install modernc.org/ql/ql | ||||||
|  |  | ||||||
|  | // https://stackoverflow.com/questions/21986780/is-it-possible-to-retrieve-a-column-value-by-name-using-golang-database-sql | ||||||
|  |  | ||||||
|  | // https://stackoverflow.com/questions/21986780/is-it-possible-to-retrieve-a-column-value-by-name-using-golang-database-sql | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/md5" | ||||||
|  | 	"database/sql" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
|  | 	"strconv" | ||||||
|  |  | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson/primitive" | ||||||
|  | 	"modernc.org/ql" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	knownApps []string | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type dbcon struct { | ||||||
|  | 	db        *sql.DB | ||||||
|  | 	masterKey []byte | ||||||
|  | 	hash      []byte | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newDB(masterKey []byte, urlurl *string) (dbcon, error) { | ||||||
|  | 	dbobj := dbcon{nil, nil, nil} | ||||||
|  |  | ||||||
|  | 	/* | ||||||
|  | 		if db, err := ql.OpenFile("./bunker.db", &ql.Options{}); err != nil { | ||||||
|  | 			fmt.Println("open db") | ||||||
|  | 			a, err := db.Info() | ||||||
|  | 			if err != nil { | ||||||
|  | 				fmt.Printf("error in db info: %s\n", err) | ||||||
|  | 			} | ||||||
|  | 			for _, v := range a.Tables { | ||||||
|  | 				fmt.Printf("dbinfo: %s\n", string(v.Name)) | ||||||
|  | 			} | ||||||
|  | 			db.Close() | ||||||
|  | 		} | ||||||
|  | 	*/ | ||||||
|  |  | ||||||
|  | 	ql.RegisterDriver2() | ||||||
|  | 	db, err := sql.Open("ql2", "./bql.db") | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("Failed to open ql db: %s", err) | ||||||
|  | 	} | ||||||
|  | 	hash := md5.Sum(masterKey) | ||||||
|  | 	dbobj = dbcon{db, masterKey, hash[:]} | ||||||
|  | 	return dbobj, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) initDB() error { | ||||||
|  | 	var err error | ||||||
|  | 	fmt.Println("init db *****") | ||||||
|  | 	if err = initUsers(dbobj.db); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = initXTokens(dbobj.db); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = initAudit(dbobj.db); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = initConsent(dbobj.db); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = initSessions(dbobj.db); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) initUserApps() error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decodeFieldsValues(data interface{}) (string, string) { | ||||||
|  | 	fields := "" | ||||||
|  | 	values := "" | ||||||
|  | 	str := "" | ||||||
|  |  | ||||||
|  | 	switch t := data.(type) { | ||||||
|  | 	case primitive.M: | ||||||
|  | 		fmt.Println("format is: primitive.M") | ||||||
|  | 		for idx, val := range data.(primitive.M) { | ||||||
|  | 			if len(fields) == 0 { | ||||||
|  | 				fields = idx | ||||||
|  | 			} else { | ||||||
|  | 				fields = fields + "," + idx | ||||||
|  | 			} | ||||||
|  | 			switch t := val.(type) { | ||||||
|  | 			case string: | ||||||
|  | 				str = "\"" + val.(string) + "\"" | ||||||
|  | 			case int: | ||||||
|  | 				str = strconv.Itoa(val.(int)) | ||||||
|  | 			case int32: | ||||||
|  | 				str = strconv.FormatInt(int64(val.(int32)), 10) | ||||||
|  | 			default: | ||||||
|  | 				fmt.Printf("wrong type: %s\n", t) | ||||||
|  | 			} | ||||||
|  | 			if len(values) == 0 { | ||||||
|  | 				values = str | ||||||
|  | 			} else { | ||||||
|  | 				values = values + "," + str | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	case *primitive.M: | ||||||
|  | 		fmt.Println("format is: *primitive.M") | ||||||
|  | 		for idx, val := range *data.(*primitive.M) { | ||||||
|  | 			if len(fields) == 0 { | ||||||
|  | 				fields = idx | ||||||
|  | 			} else { | ||||||
|  | 				fields = fields + "," + idx | ||||||
|  | 			} | ||||||
|  | 			switch t := val.(type) { | ||||||
|  | 			case string: | ||||||
|  | 				str = "\"" + val.(string) + "\"" | ||||||
|  | 			case int: | ||||||
|  | 				str = strconv.Itoa(val.(int)) | ||||||
|  | 			case int32: | ||||||
|  | 				str = strconv.FormatInt(int64(val.(int32)), 10) | ||||||
|  | 			default: | ||||||
|  | 				fmt.Printf("wrong type: %s\n", t) | ||||||
|  | 			} | ||||||
|  | 			if len(values) == 0 { | ||||||
|  | 				values = str | ||||||
|  | 			} else { | ||||||
|  | 				values = values + "," + str | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	case map[string]interface{}: | ||||||
|  | 		fmt.Println("format is: map[string]interface{}") | ||||||
|  | 		for idx, val := range data.(map[string]interface{}) { | ||||||
|  | 			if len(fields) == 0 { | ||||||
|  | 				fields = idx | ||||||
|  | 			} else { | ||||||
|  | 				fields = fields + "," + idx | ||||||
|  | 			} | ||||||
|  | 			switch t := val.(type) { | ||||||
|  | 			case string: | ||||||
|  | 				str = "\"" + val.(string) + "\"" | ||||||
|  | 			case int: | ||||||
|  | 				str = strconv.Itoa(val.(int)) | ||||||
|  | 			case int32: | ||||||
|  | 				str = strconv.FormatInt(int64(val.(int32)), 10) | ||||||
|  | 			default: | ||||||
|  | 				fmt.Printf("wrong type: %s\n", t) | ||||||
|  | 			} | ||||||
|  | 			if len(values) == 0 { | ||||||
|  | 				values = str | ||||||
|  | 			} else { | ||||||
|  | 				values = values + "," + str | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		fmt.Printf("XXXXXX wrong type: %T\n", t) | ||||||
|  | 	} | ||||||
|  | 	return fields, values | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decodeForCleanup(data interface{}) string { | ||||||
|  | 	fields := "" | ||||||
|  |  | ||||||
|  | 	switch t := data.(type) { | ||||||
|  | 	case primitive.M: | ||||||
|  | 		for idx, _ := range data.(primitive.M) { | ||||||
|  | 			if len(fields) == 0 { | ||||||
|  | 				fields = idx + "=null" | ||||||
|  | 			} else { | ||||||
|  | 				fields = fields + "," + idx + "=null" | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return fields | ||||||
|  | 	case map[string]interface{}: | ||||||
|  | 		for idx, _ := range data.(map[string]interface{}) { | ||||||
|  | 			if len(fields) == 0 { | ||||||
|  | 				fields = idx + "=null" | ||||||
|  | 			} else { | ||||||
|  | 				fields = fields + "," + idx + "=null" | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		fmt.Printf("decodeForCleanup: wrong type: %s\n", t) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fields | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decodeForUpdate(bdoc *bson.M, bdel *bson.M) string { | ||||||
|  | 	fields := "" | ||||||
|  | 	str := "" | ||||||
|  |  | ||||||
|  | 	if bdoc != nil { | ||||||
|  | 		/* | ||||||
|  | 			switch t := *bdoc.(type) { | ||||||
|  | 			default: | ||||||
|  | 				fmt.Printf("Type is %T\n", t) | ||||||
|  | 			} | ||||||
|  | 		*/ | ||||||
|  |  | ||||||
|  | 		for idx, val := range *bdoc { | ||||||
|  | 			switch t := val.(type) { | ||||||
|  | 			case string: | ||||||
|  | 				str = "\"" + val.(string) + "\"" | ||||||
|  | 			case int: | ||||||
|  | 				str = strconv.Itoa(val.(int)) | ||||||
|  | 			case int32: | ||||||
|  | 				str = strconv.FormatInt(int64(val.(int32)), 10) | ||||||
|  | 			default: | ||||||
|  | 				fmt.Printf("wrong type: %s\n", t) | ||||||
|  | 			} | ||||||
|  | 			if len(fields) == 0 { | ||||||
|  | 				fields = idx + "=" + str | ||||||
|  | 			} else { | ||||||
|  | 				fields = fields + "," + idx + "=" + str | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if bdel != nil { | ||||||
|  | 		for idx, _ := range *bdel { | ||||||
|  | 			if len(fields) == 0 { | ||||||
|  | 				fields = idx + "=null" | ||||||
|  | 			} else { | ||||||
|  | 				fields = fields + "," + idx + "=null" | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return fields | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getTable(t Tbl) string { | ||||||
|  | 	switch t { | ||||||
|  | 	case TblName.Users: | ||||||
|  | 		return "users" | ||||||
|  | 	case TblName.Audit: | ||||||
|  | 		return "audit" | ||||||
|  | 	case TblName.Consent: | ||||||
|  | 		return "consent" | ||||||
|  | 	case TblName.Xtokens: | ||||||
|  | 		return "xtokens" | ||||||
|  | 	case TblName.Sessions: | ||||||
|  | 		return "sessions" | ||||||
|  | 	} | ||||||
|  | 	return "users" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) createRecordInTable(tbl string, data interface{}) (int, error) { | ||||||
|  | 	fields, values := decodeFieldsValues(data) | ||||||
|  | 	q := "insert into " + tbl + " (" + fields + ") values (" + values + ");" | ||||||
|  | 	fmt.Printf("q: %s\n", q) | ||||||
|  |  | ||||||
|  | 	tx, err := dbobj.db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	_, err = tx.Exec(q) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	return 1, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) createRecord(t Tbl, data interface{}) (int, error) { | ||||||
|  | 	//if reflect.TypeOf(value) == reflect.TypeOf("string") | ||||||
|  | 	tbl := getTable(t) | ||||||
|  | 	return dbobj.createRecordInTable(tbl, data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) countRecords(t Tbl, keyName string, keyValue string) (int64, error) { | ||||||
|  | 	tbl := getTable(t) | ||||||
|  | 	q := "select count(*) from " + tbl + " WHERE " + keyName + "'" + keyValue + "';" | ||||||
|  | 	fmt.Printf("q: %s\n", q) | ||||||
|  |  | ||||||
|  | 	tx, err := dbobj.db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	row := tx.QueryRow(q) | ||||||
|  | 	// Columns | ||||||
|  | 	var count int | ||||||
|  | 	err = row.Scan(&count) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	return int64(count), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) updateRecord(t Tbl, keyName string, keyValue string, bdoc *bson.M) (int64, error) { | ||||||
|  | 	table := getTable(t) | ||||||
|  | 	filter := keyName + "=\"" + keyValue + "\"" | ||||||
|  | 	return dbobj.updateRecordInTableDo(table, filter, bdoc, nil) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) updateRecordInTable(table string, keyName string, keyValue string, bdoc *bson.M) (int64, error) { | ||||||
|  | 	filter := keyName + "=\"" + keyValue + "\"" | ||||||
|  | 	return dbobj.updateRecordInTableDo(table, filter, bdoc, nil) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) updateRecord2(t Tbl, keyName string, keyValue string, | ||||||
|  | 	keyName2 string, keyValue2 string, bdoc *bson.M, bdel *bson.M) (int64, error) { | ||||||
|  | 	table := getTable(t) | ||||||
|  | 	filter := keyName + "=\"" + keyValue + "\" AND " + keyName2 + "=\"" + keyValue2 + "\"" | ||||||
|  | 	return dbobj.updateRecordInTableDo(table, filter, bdoc, bdel) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) updateRecordInTable2(table string, keyName string, | ||||||
|  | 	keyValue string, keyName2 string, keyValue2 string, bdoc *bson.M, bdel *bson.M) (int64, error) { | ||||||
|  | 	filter := keyName + "=\"" + keyValue + "\" AND " + keyName2 + "=\"" + keyValue2 + "\"" | ||||||
|  | 	return dbobj.updateRecordInTableDo(table, filter, bdoc, bdel) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) updateRecordInTableDo(table string, filter string, bdoc *bson.M, bdel *bson.M) (int64, error) { | ||||||
|  | 	op := decodeForUpdate(bdoc, bdel) | ||||||
|  | 	q := "update " + table + " SET " + op + " WHERE " + filter | ||||||
|  | 	fmt.Printf("q: %s\n", q) | ||||||
|  |  | ||||||
|  | 	tx, err := dbobj.db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	defer tx.Rollback() | ||||||
|  | 	result, err := tx.Exec(q) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	num, err := result.RowsAffected() | ||||||
|  | 	return num, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getRecord(t Tbl, keyName string, keyValue string) (bson.M, error) { | ||||||
|  | 	tbl := getTable(t) | ||||||
|  | 	return dbobj.getRecordInTable(tbl, keyName, keyValue) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getRecordInTable(table string, keyName string, keyValue string) (bson.M, error) { | ||||||
|  | 	q := "select * from " + table + " WHERE " + keyName + "=\"" + keyValue + "\"" | ||||||
|  | 	return dbobj.getRecordInTableDo(q) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getRecord2(t Tbl, keyName string, keyValue string, | ||||||
|  | 	keyName2 string, keyValue2 string) (bson.M, error) { | ||||||
|  | 	tbl := getTable(t) | ||||||
|  | 	return dbobj.getRecordInTable2(tbl, keyName, keyValue, keyName2, keyValue2) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getRecordInTable2(table string, keyName string, keyValue string, | ||||||
|  | 	keyName2 string, keyValue2 string) (bson.M, error) { | ||||||
|  | 	q := "select * from " + table + " WHERE " + keyName + "=\"" + keyValue + "\" AND " + | ||||||
|  | 		keyName2 + "=\"" + keyValue2 + "\"" | ||||||
|  | 	return dbobj.getRecordInTableDo(q) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getRecordInTableDo(q string) (bson.M, error) { | ||||||
|  | 	fmt.Printf("q: %s\n", q) | ||||||
|  | 	tx, err := dbobj.db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	defer tx.Rollback() | ||||||
|  | 	rows, err := tx.Query(q) | ||||||
|  | 	if err == sql.ErrNoRows { | ||||||
|  | 		fmt.Println("nothing found") | ||||||
|  | 		return nil, nil | ||||||
|  | 	} else if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	defer rows.Close() | ||||||
|  |  | ||||||
|  | 	columnNames, err := rows.Columns() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	//fmt.Printf("names: %s\n", columnNames) | ||||||
|  | 	if err := rows.Err(); err != nil { | ||||||
|  | 		log.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	//pointers := make([]interface{}, len(columnNames)) | ||||||
|  | 	recBson := bson.M{} | ||||||
|  | 	rows.Next() | ||||||
|  | 	//for rows.Next() { | ||||||
|  | 	//fmt.Println("parsing result line") | ||||||
|  | 	columnPointers := make([]interface{}, len(columnNames)) | ||||||
|  | 	//for i, _ := range columnNames { | ||||||
|  | 	//		columnPointers[i] = new(interface{}) | ||||||
|  | 	//} | ||||||
|  | 	columns := make([]interface{}, len(columnNames)) | ||||||
|  | 	for i, _ := range columns { | ||||||
|  | 		columnPointers[i] = &columns[i] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = rows.Scan(columnPointers...) | ||||||
|  | 	if err == sql.ErrNoRows { | ||||||
|  | 		fmt.Println("nothing found") | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("nothing found: %s\n", err) | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	for i, colName := range columnNames { | ||||||
|  | 		switch t := columns[i].(type) { | ||||||
|  | 		case string: | ||||||
|  | 			recBson[colName] = columns[i] | ||||||
|  | 		case []uint8: | ||||||
|  | 			recBson[colName] = string(columns[i].([]uint8)) | ||||||
|  | 		case int64: | ||||||
|  | 			recBson[colName] = int32(columns[i].(int64)) | ||||||
|  | 		case nil: | ||||||
|  | 			//fmt.Printf("is nil, not interesting\n") | ||||||
|  | 		default: | ||||||
|  | 			fmt.Printf("field: %s - %s, unknown: %s - %T\n", colName, columns[i], t, t) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	//} | ||||||
|  | 	err = rows.Close() | ||||||
|  | 	if err == sql.ErrNoRows { | ||||||
|  | 		fmt.Println("nothing found2") | ||||||
|  | 		return nil, nil | ||||||
|  | 	} else if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if len(recBson) == 0 { | ||||||
|  | 		fmt.Println("no result!!!") | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return recBson, err | ||||||
|  | 	} | ||||||
|  | 	return recBson, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) deleteRecord(t Tbl, keyName string, keyValue string) (int64, error) { | ||||||
|  | 	tbl := getTable(t) | ||||||
|  | 	return dbobj.deleteRecordInTable(tbl, keyName, keyValue) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) deleteRecordInTable(table string, keyName string, keyValue string) (int64, error) { | ||||||
|  | 	q := "delete from " + table + " WHERE " + keyName + "=\"" + keyValue + "\"" | ||||||
|  | 	fmt.Printf("q: %s\n", q) | ||||||
|  |  | ||||||
|  | 	tx, err := dbobj.db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	defer tx.Rollback() | ||||||
|  | 	result, err := tx.Exec(q) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	num, err := result.RowsAffected() | ||||||
|  | 	return num, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) cleanupRecord(t Tbl, keyName string, keyValue string, data interface{}) (int64, error) { | ||||||
|  | 	tbl := getTable(t) | ||||||
|  | 	cleanup := decodeForCleanup(data) | ||||||
|  | 	q := "update " + tbl + " SET " + cleanup + " WHERE " + keyName + "=\"" + keyValue + "\"" | ||||||
|  | 	fmt.Printf("q: %s\n", q) | ||||||
|  |  | ||||||
|  | 	tx, err := dbobj.db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	defer tx.Rollback() | ||||||
|  | 	result, err := tx.Exec(q) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	num, err := result.RowsAffected() | ||||||
|  | 	return num, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getList(t Tbl, keyName string, keyValue string, start int32, limit int32) ([]bson.M, error) { | ||||||
|  | 	fmt.Println("TODO") | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getAllTables() ([]string, error) { | ||||||
|  | 	//for nm, tab := range dbobj.db.root.tables { | ||||||
|  | 	// | ||||||
|  | 	//	} | ||||||
|  | 	// HasNextResultSet() | ||||||
|  | 	a := []string{"aaa"} | ||||||
|  | 	a = append(a, "test123") | ||||||
|  | 	return a, nil | ||||||
|  | } | ||||||
|  | func (dbobj dbcon) indexNewApp(appName string) { | ||||||
|  | 	if contains(knownApps, appName) == false { | ||||||
|  | 		// it is a new app, create an index | ||||||
|  | 		fmt.Printf("This is a new app, creating index for :%s\n", appName) | ||||||
|  |  | ||||||
|  | 		tx, err := dbobj.db.Begin() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		defer tx.Rollback() | ||||||
|  | 		_, err = tx.Exec("CREATE TABLE IF NOT EXISTS " + appName + ` ( | ||||||
|  | 	  		token STRING, | ||||||
|  | 	  		md5 STRING, | ||||||
|  | 	  		data STRING, | ||||||
|  | 	  		status STRING, | ||||||
|  | 	  		when int | ||||||
|  | 		);`) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		_, err = tx.Exec("CREATE INDEX IF NOT EXISTS " + appName + "_token ON " + appName + " (token)") | ||||||
|  | 		if err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if err = tx.Commit(); err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		knownApps = append(knownApps, appName) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | BEGIN TRANSACTION; | ||||||
|  | 	CREATE TABLE Orders (CustomerID int, Date time); | ||||||
|  | 	CREATE INDEX OrdersID ON Orders (id()); | ||||||
|  | 	CREATE INDEX OrdersDate ON Orders (Date); | ||||||
|  | 	CREATE TABLE Items (OrderID int, ProductID int, Qty int); | ||||||
|  | 	CREATE INDEX ItemsOrderID ON Items (OrderID); | ||||||
|  | COMMIT; | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | func initUsers(db *sql.DB) error { | ||||||
|  | 	tx, err := db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer tx.Rollback() | ||||||
|  | 	_, err = tx.Exec(` | ||||||
|  | 	CREATE TABLE IF NOT EXISTS users ( | ||||||
|  | 	  token STRING, | ||||||
|  | 	  key STRING, | ||||||
|  | 	  md5 STRING, | ||||||
|  | 	  loginidx STRING, | ||||||
|  | 	  emailidx STRING, | ||||||
|  | 	  phoneidx STRING, | ||||||
|  | 	  tempcode STRING, | ||||||
|  | 	  tempcodeexp int, | ||||||
|  | 	  data string | ||||||
|  | 	); | ||||||
|  | 	`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	fmt.Println("going to create indexes") | ||||||
|  | 	_, err = tx.Exec(`CREATE INDEX users_token ON users (token);`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Println("error in create index") | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	_, err = tx.Exec(`CREATE INDEX users_login ON users (loginidx);`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	_, err = tx.Exec(`CREATE INDEX users_email ON users (emailidx);`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	_, err = tx.Exec(`CREATE INDEX users_phone ON users (phoneidx);`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func initXTokens(db *sql.DB) error { | ||||||
|  | 	tx, err := db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer tx.Rollback() | ||||||
|  | 	_, err = tx.Exec(` | ||||||
|  | 	CREATE TABLE IF NOT EXISTS xtokens ( | ||||||
|  | 	  xtoken STRING, | ||||||
|  | 	  token STRING, | ||||||
|  | 	  type STRING, | ||||||
|  | 	  app STRING, | ||||||
|  | 	  fields STRING, | ||||||
|  | 	  endtime int32 | ||||||
|  | 	); | ||||||
|  | 	`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	_, err = tx.Exec(`CREATE INDEX xtokens_xtoken ON xtokens (xtoken);`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	_, err = tx.Exec(`CREATE INDEX xtokens_type ON xtokens (type);`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | 	When   int32  `json:"when"` | ||||||
|  | 	Who    string `json:"who"` | ||||||
|  | 	Record string `json:"record"` | ||||||
|  | 	App    string `json:"app"` | ||||||
|  | 	Title  string `json:"title"` | ||||||
|  | 	Status string `json:"status"` | ||||||
|  | 	Msg    string `json:"msg"` | ||||||
|  | 	Debug  string `json:"debug"` | ||||||
|  | 	Before string `json:"before"` | ||||||
|  | 	After  string `json:"after"` | ||||||
|  | 	Meta   string `json:"meta"` | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | func initAudit(db *sql.DB) error { | ||||||
|  | 	tx, err := db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer tx.Rollback() | ||||||
|  | 	_, err = tx.Exec(` | ||||||
|  | 	CREATE TABLE IF NOT EXISTS audit ( | ||||||
|  | 	  record STRING, | ||||||
|  | 	  who STRING, | ||||||
|  | 	  app STRING, | ||||||
|  | 	  title STRING, | ||||||
|  | 	  status STRING, | ||||||
|  | 	  msg STRING, | ||||||
|  | 	  debug STRING, | ||||||
|  | 	  before STRING, | ||||||
|  | 	  after STRING, | ||||||
|  | 	  meta STRING, | ||||||
|  | 	  when int | ||||||
|  | 	); | ||||||
|  | 	`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	_, err = tx.Exec(`CREATE INDEX audit_record ON audit (record);`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | 	When    int32  `json:"when,omitempty"` | ||||||
|  | 	Who     string `json:"who,omitempty"` | ||||||
|  | 	Type    string `json:"type,omitempty"` | ||||||
|  | 	Token   string `json:"token,omitempty"` | ||||||
|  | 	Brief   string `json:"brief,omitempty"` | ||||||
|  | 	Message string `json:"message,omitempty"` | ||||||
|  | 	Status  string `json:"status,omitempty"` | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | func initConsent(db *sql.DB) error { | ||||||
|  | 	tx, err := db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer tx.Rollback() | ||||||
|  | 	_, err = tx.Exec(` | ||||||
|  | 	CREATE TABLE IF NOT EXISTS consent ( | ||||||
|  | 	  who STRING, | ||||||
|  | 	  type STRING, | ||||||
|  | 	  token STRING, | ||||||
|  | 	  brief STRING, | ||||||
|  | 	  message STRING, | ||||||
|  | 	  status STRING, | ||||||
|  | 	  when int | ||||||
|  | 	); | ||||||
|  | 	`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	_, err = tx.Exec(`CREATE INDEX consent_token ON consent (token);`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func initSessions(db *sql.DB) error { | ||||||
|  | 	tx, err := db.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer tx.Rollback() | ||||||
|  | 	_, err = tx.Exec(` | ||||||
|  | 	CREATE TABLE IF NOT EXISTS sessions ( | ||||||
|  | 	  token STRING, | ||||||
|  | 	  session STRING, | ||||||
|  | 	  meta STRING, | ||||||
|  | 	  when int, | ||||||
|  | 	  endtime int | ||||||
|  | 	); | ||||||
|  | 	`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	_, err = tx.Exec(`CREATE INDEX sessions_token ON sessions (token);`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = tx.Commit(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								src/sessions_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/sessions_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/julienschmidt/httprouter" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (e mainEnv) newSession(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	uuidCode := ps.ByName("uuidcode") | ||||||
|  | 	event := audit("create new session", uuidCode) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  | } | ||||||
							
								
								
									
										103
									
								
								src/sessions_db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/sessions_db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	uuid "github.com/hashicorp/go-uuid" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type sessionEvent struct { | ||||||
|  | 	When int32 | ||||||
|  | 	Meta []byte | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) generateUserSession(userTOKEN string, clientip string, expiration string, meta []byte) (string, error) { | ||||||
|  | 	if len(expiration) == 0 { | ||||||
|  | 		return "", errors.New("failed to parse expiration") | ||||||
|  | 	} | ||||||
|  | 	endtime, err := parseExpiration(expiration) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	encodedStr, err := dbobj.userEncrypt(userTOKEN, meta) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	tokenUUID, err := uuid.GenerateUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	bdoc["token"] = userTOKEN | ||||||
|  | 	bdoc["session"] = tokenUUID | ||||||
|  | 	bdoc["endtime"] = endtime | ||||||
|  | 	bdoc["meta"] = encodedStr | ||||||
|  | 	if len(clientip) > 0 { | ||||||
|  | 		idxString := append(dbobj.hash, []byte(clientip)...) | ||||||
|  | 		idxStringHash := sha256.Sum256(idxString) | ||||||
|  | 		bdoc["clientipidx"] = base64.StdEncoding.EncodeToString(idxStringHash[:]) | ||||||
|  | 	} | ||||||
|  | 	_, err = dbobj.createRecord(TblName.Sessions, bdoc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return tokenUUID, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getUserSession(sessionUUID string) ([]byte, error) { | ||||||
|  | 	record, err := dbobj.getRecord(TblName.Sessions, "session", sessionUUID) | ||||||
|  | 	if record == nil || err != nil { | ||||||
|  | 		return nil, errors.New("failed to authenticate") | ||||||
|  | 	} | ||||||
|  | 	// check expiration | ||||||
|  | 	now := int32(time.Now().Unix()) | ||||||
|  | 	if now > record["endtime"].(int32) { | ||||||
|  | 		return nil, errors.New("session expired") | ||||||
|  | 	} | ||||||
|  | 	userTOKEN := record["token"].(string) | ||||||
|  | 	encData0 := record["meta"].(string) | ||||||
|  | 	decrypted, err := dbobj.userDecrypt(userTOKEN, encData0) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return decrypted, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getUserSessionByToken(userTOKEN string) ([]*sessionEvent, int64, error) { | ||||||
|  |  | ||||||
|  | 	userBson, err := dbobj.lookupUserRecord(userTOKEN) | ||||||
|  | 	if userBson == nil || err != nil { | ||||||
|  | 		// not found | ||||||
|  | 		return nil, 0, err | ||||||
|  | 	} | ||||||
|  | 	userKey := userBson["key"].(string) | ||||||
|  | 	recordKey, err := base64.StdEncoding.DecodeString(userKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, 0, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	count, err := dbobj.countRecords(TblName.Sessions, "token", userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, 0, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	records, err := dbobj.getList(TblName.Sessions, "token", userTOKEN, 0, 0) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, 0, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var results []*sessionEvent | ||||||
|  | 	for _, element := range records { | ||||||
|  | 		encData0 := element["meta"].(string) | ||||||
|  | 		encData, _ := base64.StdEncoding.DecodeString(encData0) | ||||||
|  | 		decrypted, _ := decrypt(dbobj.masterKey, recordKey, encData) | ||||||
|  | 		sEvent := sessionEvent{0, decrypted} | ||||||
|  | 		results = append(results, &sEvent) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return results, count, err | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								src/sms.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/sms.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func sendCodeByPhone(code string, address string, cfg Config) { | ||||||
|  | 	urlStr := "https://api.twilio.com/2010-04-01/Accounts/" + cfg.Sms.Twilio_account + "/Messages.json" | ||||||
|  | 	fmt.Printf("url %s\n", urlStr) | ||||||
|  | 	msgData := url.Values{} | ||||||
|  | 	msgData.Set("To", address) | ||||||
|  | 	msgData.Set("From", cfg.Sms.Twilio_from) | ||||||
|  | 	msgData.Set("Body", "Data Bunker code "+code) | ||||||
|  | 	msgDataReader := *strings.NewReader(msgData.Encode()) | ||||||
|  | 	client := &http.Client{} | ||||||
|  | 	req, _ := http.NewRequest("POST", urlStr, &msgDataReader) | ||||||
|  | 	req.SetBasicAuth(cfg.Sms.Twilio_account, cfg.Sms.Twilio_token) | ||||||
|  | 	req.Header.Add("Accept", "application/json") | ||||||
|  | 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||||||
|  | 	resp, _ := client.Do(req) | ||||||
|  | 	if resp.StatusCode >= 200 && resp.StatusCode < 300 { | ||||||
|  | 		var data map[string]interface{} | ||||||
|  | 		decoder := json.NewDecoder(resp.Body) | ||||||
|  | 		err := decoder.Decode(&data) | ||||||
|  | 		if err == nil { | ||||||
|  | 			fmt.Println(data["sid"]) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		fmt.Println(resp.Status) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										133
									
								
								src/tokens_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/tokens_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	uuid "github.com/hashicorp/go-uuid" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func Test_UserTempToken(t *testing.T) { | ||||||
|  | 	masterKey, err := hex.DecodeString("71c65924336c5e6f41129b6f0540ad03d2a8bf7e9b10db72") | ||||||
|  | 	db, _ := newDB(masterKey, nil) | ||||||
|  |  | ||||||
|  | 	var parsedData userJSON | ||||||
|  | 	parsedData.jsonData = []byte(`{"login":"start","key1":"bbb","key2":[10,20]}`) | ||||||
|  | 	parsedData.loginIdx = "start" | ||||||
|  | 	userTOKEN, err := db.createUserRecord(parsedData, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to create user: %s", err) | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("user token generated: %s\n", userTOKEN) | ||||||
|  | 	fields := "key1,key2.1" | ||||||
|  | 	expiration := "7d" | ||||||
|  | 	userToken, err := db.generateUserTempXToken(userTOKEN, fields, expiration, "") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to generate user token: %s ", err) | ||||||
|  | 	} | ||||||
|  | 	if userToken == "" { | ||||||
|  | 		t.Fatalf("Failed to generate user token") | ||||||
|  | 	} | ||||||
|  | 	_, err = db.deleteUserRecord(userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to delete user: %s", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Test_UserTempToken2(t *testing.T) { | ||||||
|  | 	masterKey, err := hex.DecodeString("71c65924336c5e6f41129b6f0540ad03d2a8bf7e9b10db72") | ||||||
|  | 	db, _ := newDB(masterKey, nil) | ||||||
|  |  | ||||||
|  | 	userTOKEN, err := uuid.GenerateUUID() | ||||||
|  | 	fields := "abc" | ||||||
|  | 	expiration := "7d" | ||||||
|  | 	_, err = db.generateUserTempXToken(userTOKEN, fields, expiration, "") | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatalf("Should failed to generate user token") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func helpCreateUserXToken(uuidCode string, tokenJSON string) (map[string]interface{}, error) { | ||||||
|  | 	request := httptest.NewRequest("POST", | ||||||
|  | 		"http://localhost:3000/v1/xtoken/"+uuidCode, | ||||||
|  | 		strings.NewReader(tokenJSON)) | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	request.Header.Set("Content-Type", "application/json") | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  |  | ||||||
|  | 	router.ServeHTTP(rr, request) | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	fmt.Printf("Got: %s\n", rr.Body.Bytes()) | ||||||
|  | 	err := json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	return raw, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAPIToken(t *testing.T) { | ||||||
|  | 	jsonData := `{"email":"stremovsky@gmail.com","phone":"0524486622","fname":"Yuli","lname":"Stremovsky","tz":"323xxxxx","password":"123456","address":"Y-d habanim 7","city":"Petah-Tiqva","btest":true,"numtest":123,"testnul":null}` | ||||||
|  | 	raw, err := helpCreateUser(jsonData) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if strings.Contains(err.Error(), "duplicate") { | ||||||
|  | 			raw, err = helpGetUser("email", "stremovsky@gmail.com") | ||||||
|  | 		} else { | ||||||
|  | 			t.Fatalf("error: %s", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	status := raw["status"].(string) | ||||||
|  | 	if status == "error" { | ||||||
|  | 		if strings.Contains(raw["message"].(string), "duplicate") { | ||||||
|  | 			raw, err = helpGetUser("email", "stremovsky@gmail.com") | ||||||
|  | 		} else { | ||||||
|  | 			t.Fatalf("Failed to create user: %s", raw["message"].(string)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	userTOKEN := raw["token"].(string) | ||||||
|  | 	fields := "phone,field1,field2" | ||||||
|  | 	tokenJSON := fmt.Sprintf(`{"fields":"%s","expiration":"1d"}`, fields) | ||||||
|  | 	raw2, err := helpCreateUserXToken(userTOKEN, tokenJSON) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error: %s", err) | ||||||
|  | 	} | ||||||
|  | 	token := raw2["xtoken"].(string) | ||||||
|  | 	fmt.Printf("**** Result token : %s\n", token) | ||||||
|  | 	raw3, err := helpGetUserAppList(userTOKEN) | ||||||
|  | 	fmt.Printf("apps: %s\n", raw3["apps"]) | ||||||
|  | 	helpCreateUserApp(userTOKEN, "qq", `{"custom":1}`) | ||||||
|  | 	raw3, err = helpGetUserAppList(userTOKEN) | ||||||
|  | 	fmt.Printf("apps: %s\n", raw3["apps"]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Test_UserAppToken(t *testing.T) { | ||||||
|  | 	masterKey, err := hex.DecodeString("71c65924336c5e6f41129b6f0540ad03d2a8bf7e9b10db72") | ||||||
|  | 	db, _ := newDB(masterKey, nil) | ||||||
|  |  | ||||||
|  | 	var parsedData userJSON | ||||||
|  | 	parsedData.jsonData = []byte(`{"login":"start","field":"bbb"}`) | ||||||
|  | 	parsedData.loginIdx = "start" | ||||||
|  | 	userTOKEN, err := db.createUserRecord(parsedData, nil) | ||||||
|  | 	fields := "abc" | ||||||
|  | 	expiration := "7d" | ||||||
|  | 	appName := "test" | ||||||
|  | 	userXToken, err := db.generateUserTempXToken(userTOKEN, fields, expiration, appName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to generate user token: %s ", err) | ||||||
|  | 	} | ||||||
|  | 	if userXToken == "" { | ||||||
|  | 		t.Fatalf("Failed to generate user token") | ||||||
|  | 	} | ||||||
|  | 	appName = "test2" | ||||||
|  | 	userXToken, err = db.generateUserTempXToken(userTOKEN, fields, expiration, appName) | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatalf("Using unknown app, should fail.") | ||||||
|  | 	} | ||||||
|  | 	if userXToken != "" { | ||||||
|  | 		t.Fatalf("Should fail to generate user token") | ||||||
|  | 	} | ||||||
|  | 	_, err = db.deleteUserRecord(userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to delete user: %s", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										149
									
								
								src/userapps_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								src/userapps_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/julienschmidt/httprouter" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (e mainEnv) userappNew(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	userTOKEN := ps.ByName("token") | ||||||
|  | 	appName := ps.ByName("appname") | ||||||
|  | 	event := auditApp("create user app record", userTOKEN, appName) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if enforceUUID(w, userTOKEN, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if isValidApp(appName) == false { | ||||||
|  | 		returnError(w, r, "bad appname", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data, err := getJSONPostData(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	jsonData, err := json.Marshal(data) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	_, err = e.db.createAppRecord(jsonData, userTOKEN, appName, event) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	returnUUID(w, userTOKEN) | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) userappChange(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	userTOKEN := ps.ByName("token") | ||||||
|  | 	appName := ps.ByName("appname") | ||||||
|  | 	event := auditApp("change user app record", userTOKEN, appName) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if enforceUUID(w, userTOKEN, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if isValidApp(appName) == false { | ||||||
|  | 		returnError(w, r, "bad appname", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data, err := getJSONPostData(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	jsonData, err := json.Marshal(data) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	_, err = e.db.updateAppRecord(jsonData, userTOKEN, appName, event) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	returnUUID(w, userTOKEN) | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) userappList(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	userTOKEN := ps.ByName("token") | ||||||
|  | 	event := audit("get user app list", userTOKEN) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if enforceUUID(w, userTOKEN, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	result, err := e.db.listUserApps(userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	fmt.Fprintf(w, `{"status":"ok","token":"%s","apps":%s}`, userTOKEN, result) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) userappGet(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	userTOKEN := ps.ByName("token") | ||||||
|  | 	appName := ps.ByName("appname") | ||||||
|  | 	event := auditApp("get user app record", userTOKEN, appName) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if enforceUUID(w, userTOKEN, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if isValidApp(appName) == false { | ||||||
|  | 		returnError(w, r, "bad appname", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resultJSON, err := e.db.getUserApp(userTOKEN, appName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if resultJSON == nil { | ||||||
|  | 		returnError(w, r, "not found", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	finalJSON := fmt.Sprintf(`{"status":"ok","token":"%s","data":%s}`, userTOKEN, resultJSON) | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	w.Write([]byte(finalJSON)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) appList(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	fmt.Printf("/APPLIST\n") | ||||||
|  | 	if e.enforceAuth(w, r, nil) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	result, err := e.db.listAllApps() | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, nil) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	fmt.Fprintf(w, `{"status":"ok","apps":%s}`, result) | ||||||
|  | } | ||||||
							
								
								
									
										175
									
								
								src/userapps_db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								src/userapps_db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/md5" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	jsonpatch "github.com/evanphx/json-patch" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getUserApp(userTOKEN string, appName string) ([]byte, error) { | ||||||
|  |  | ||||||
|  | 	record, err := dbobj.getRecordInTable("app_"+appName, "token", userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if record == nil { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	encData0 := record["data"].(string) | ||||||
|  | 	return dbobj.userDecrypt(userTOKEN, encData0) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) createAppRecord(jsonData []byte, userTOKEN string, appName string, event *auditEvent) (string, error) { | ||||||
|  | 	fmt.Printf("createAppRecord app is : %s\n", appName) | ||||||
|  | 	encodedStr, err := dbobj.userEncrypt(userTOKEN, jsonData) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return userTOKEN, err | ||||||
|  | 	} | ||||||
|  | 	dbobj.indexNewApp("app_" + appName) | ||||||
|  |  | ||||||
|  | 	//var bdoc interface{} | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	bdoc["data"] = encodedStr | ||||||
|  | 	//it is ok to use md5 here, it is only for data sanity | ||||||
|  | 	md5Hash := md5.Sum([]byte(encodedStr)) | ||||||
|  | 	bdoc["md5"] = base64.StdEncoding.EncodeToString(md5Hash[:]) | ||||||
|  | 	bdoc["token"] = userTOKEN | ||||||
|  | 	if event != nil { | ||||||
|  | 		event.After = encodedStr | ||||||
|  | 		event.App = appName | ||||||
|  | 		event.Record = userTOKEN | ||||||
|  | 	} | ||||||
|  | 	//fmt.Println("creating new app") | ||||||
|  | 	record, err := dbobj.getRecordInTable("app_"+appName, "token", userTOKEN) | ||||||
|  | 	if record != nil { | ||||||
|  | 		fmt.Println("update user app") | ||||||
|  | 		_, err = dbobj.updateRecordInTable("app_"+appName, "token", userTOKEN, &bdoc) | ||||||
|  | 	} else { | ||||||
|  | 		_, err = dbobj.createRecordInTable("app_"+appName, bdoc) | ||||||
|  | 	} | ||||||
|  | 	return userTOKEN, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) updateAppRecord(jsonDataPatch []byte, userTOKEN string, appName string, event *auditEvent) (string, error) { | ||||||
|  | 	//_, err = collection.InsertOne(context.TODO(), bson.M{"name": "The Go Language2", "genre": "Coding", "authorId": "4"}) | ||||||
|  | 	userBson, err := dbobj.lookupUserRecord(userTOKEN) | ||||||
|  | 	if userBson == nil || err != nil { | ||||||
|  | 		// not found | ||||||
|  | 		return userTOKEN, err | ||||||
|  | 	} | ||||||
|  | 	// get user key | ||||||
|  | 	userKey := userBson["key"].(string) | ||||||
|  | 	recordKey, err := base64.StdEncoding.DecodeString(userKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return userTOKEN, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	record, err := dbobj.getRecordInTable("app_"+appName, "token", userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return userTOKEN, err | ||||||
|  | 	} | ||||||
|  | 	if record == nil { | ||||||
|  | 		return userTOKEN, errors.New("user app record not found") | ||||||
|  | 	} | ||||||
|  | 	sig := record["md5"].(string) | ||||||
|  | 	encData0 := record["data"].(string) | ||||||
|  | 	encData, err := base64.StdEncoding.DecodeString(encData0) | ||||||
|  | 	decrypted, err := decrypt(dbobj.masterKey, recordKey, encData) | ||||||
|  |  | ||||||
|  | 	// merge | ||||||
|  | 	fmt.Printf("old json: %s\n", decrypted) | ||||||
|  | 	fmt.Printf("json patch: %s\n", jsonDataPatch) | ||||||
|  | 	newJSON, err := jsonpatch.MergePatch(decrypted, jsonDataPatch) | ||||||
|  | 	fmt.Printf("result: %s\n", newJSON) | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	encoded, err := encrypt(dbobj.masterKey, recordKey, newJSON) | ||||||
|  | 	encodedStr := base64.StdEncoding.EncodeToString(encoded) | ||||||
|  | 	bdoc["data"] = encodedStr | ||||||
|  | 	//it is ok to use md5 here, it is only for data sanity | ||||||
|  | 	md5Hash := md5.Sum([]byte(encodedStr)) | ||||||
|  | 	bdoc["md5"] = base64.StdEncoding.EncodeToString(md5Hash[:]) | ||||||
|  | 	bdoc["token"] = userTOKEN | ||||||
|  |  | ||||||
|  | 	// here I add md5 of the original record to filter | ||||||
|  | 	// to make sure this record was not change by other thread | ||||||
|  | 	fmt.Println("update user app") | ||||||
|  | 	result, err := dbobj.updateRecordInTable2("app_"+appName, "token", userTOKEN, "md5", sig, &bdoc, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return userTOKEN, err | ||||||
|  | 	} | ||||||
|  | 	if event != nil { | ||||||
|  | 		event.Before = encData0 | ||||||
|  | 		event.After = encodedStr | ||||||
|  | 		if result > 0 { | ||||||
|  | 			event.Status = "ok" | ||||||
|  | 		} else { | ||||||
|  | 			event.Status = "failed" | ||||||
|  | 			event.Msg = "failed to update" | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return userTOKEN, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // go over app collections and check if we have user record inside | ||||||
|  | func (dbobj dbcon) listUserApps(userTOKEN string) ([]byte, error) { | ||||||
|  | 	//_, err = collection.InsertOne(context.TODO(), bson.M{"name": "The Go Language2", "genre": "Coding", "authorId": "4"}) | ||||||
|  | 	record, err := dbobj.lookupUserRecord(userTOKEN) | ||||||
|  | 	if record == nil || err != nil { | ||||||
|  | 		// not found | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	allCollections, err := dbobj.getAllTables() | ||||||
|  | 	var result []string | ||||||
|  | 	for _, colName := range allCollections { | ||||||
|  | 		if strings.HasPrefix(colName, "app_") { | ||||||
|  | 			record, err := dbobj.getRecordInTable(colName, "token", userTOKEN) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			if record != nil { | ||||||
|  | 				result = append(result, colName[4:]) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("returning: %s\n", result) | ||||||
|  | 	resultJSON, err := json.Marshal(result) | ||||||
|  | 	return resultJSON, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) listAllAppsOnly() ([]string, error) { | ||||||
|  | 	//fmt.Println("dump list of collections") | ||||||
|  | 	allCollections, err := dbobj.getAllTables() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	var result []string | ||||||
|  | 	for _, colName := range allCollections { | ||||||
|  | 		if strings.HasPrefix(colName, "app_") { | ||||||
|  | 			result = append(result, colName[4:]) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return result, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) listAllApps() ([]byte, error) { | ||||||
|  | 	//fmt.Println("dump list of collections") | ||||||
|  | 	allCollections, err := dbobj.getAllTables() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	var result []string | ||||||
|  | 	for _, colName := range allCollections { | ||||||
|  | 		if strings.HasPrefix(colName, "app_") { | ||||||
|  | 			result = append(result, colName[4:]) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	resultJSON, err := json.Marshal(result) | ||||||
|  | 	//fmt.Println(resultJSON) | ||||||
|  | 	return resultJSON, err | ||||||
|  | } | ||||||
							
								
								
									
										137
									
								
								src/userapps_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/userapps_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func helpCreateUserApp(userTOKEN string, appName string, appJSON string) (map[string]interface{}, error) { | ||||||
|  | 	request := httptest.NewRequest("POST", "http://localhost:3000/v1/userapp/token/"+userTOKEN+"/"+appName, strings.NewReader(appJSON)) | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	request.Header.Set("Content-Type", "application/json") | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  |  | ||||||
|  | 	router.ServeHTTP(rr, request) | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	fmt.Printf("Got: %s\n", rr.Body.Bytes()) | ||||||
|  | 	err := json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	return raw, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func helpUpdateUserApp(userTOKEN string, appName string, appJSON string) (map[string]interface{}, error) { | ||||||
|  | 	request := httptest.NewRequest("PUT", "http://localhost:3000/v1/userapp/token/"+userTOKEN+"/"+appName, strings.NewReader(appJSON)) | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	request.Header.Set("Content-Type", "application/json") | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  |  | ||||||
|  | 	router.ServeHTTP(rr, request) | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	fmt.Printf("Got: %s\n", rr.Body.Bytes()) | ||||||
|  | 	err := json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	return raw, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func helpGetUserApp(userTOKEN string, appName string) (map[string]interface{}, error) { | ||||||
|  | 	request := httptest.NewRequest("GET", "http://localhost:3000/v1/userapp/token/"+userTOKEN+"/"+appName, nil) | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  |  | ||||||
|  | 	router.ServeHTTP(rr, request) | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	fmt.Printf("Got: %s\n", rr.Body.Bytes()) | ||||||
|  | 	err := json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	return raw, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func helpDeleteUserApp(userTOKEN string, appName string) (map[string]interface{}, error) { | ||||||
|  | 	request := httptest.NewRequest("DELETE", "http://localhost:3000/v1/userapp/token/"+userTOKEN+"/"+appName, nil) | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  |  | ||||||
|  | 	router.ServeHTTP(rr, request) | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	fmt.Printf("Got: %s\n", rr.Body.Bytes()) | ||||||
|  | 	err := json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	return raw, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func helpGetUserAppList(userTOKEN string) (map[string]interface{}, error) { | ||||||
|  | 	request := httptest.NewRequest("GET", "http://localhost:3000/v1/userapp/token/"+userTOKEN, nil) | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  |  | ||||||
|  | 	router.ServeHTTP(rr, request) | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	fmt.Printf("Got: %s\n", rr.Body.Bytes()) | ||||||
|  | 	err := json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	return raw, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func helpGetAppList() (map[string]interface{}, error) { | ||||||
|  | 	request := httptest.NewRequest("GET", "http://localhost:3000/v1/userapp/list", nil) | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  |  | ||||||
|  | 	router.ServeHTTP(rr, request) | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	fmt.Printf("Got: %s\n", rr.Body.Bytes()) | ||||||
|  | 	err := json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	return raw, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestCreateUserApp(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	userJSON := `{"name":"tom","pass":"mylittlepony","k1":[1,10,20],"k2":{"f1":"t1","f3":{"a":"b"}}}` | ||||||
|  |  | ||||||
|  | 	raw, err := helpCreateUser(userJSON) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to create user: %s", err) | ||||||
|  | 	} | ||||||
|  | 	userTOKEN := raw["token"].(string) | ||||||
|  | 	appJSON := `{"shipping":"done"}` | ||||||
|  | 	appName := "shipping" | ||||||
|  | 	raw2, err := helpCreateUserApp(userTOKEN, appName, appJSON) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error: %s", err) | ||||||
|  | 	} | ||||||
|  | 	if raw2["status"] != "ok" { | ||||||
|  | 		t.Fatalf("Failed to create userapp: %s\n", raw2["message"]) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	appJSON = `{"like":"yes"}` | ||||||
|  | 	raw3, err := helpUpdateUserApp(userTOKEN, appName, appJSON) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error: %s", err) | ||||||
|  | 	} | ||||||
|  | 	if raw3["status"] != "ok" { | ||||||
|  | 		t.Fatalf("Failed to update userapp: %s\n", raw3["message"]) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	raw4, err := helpGetUserApp(userTOKEN, appName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error: %s", err) | ||||||
|  | 	} | ||||||
|  | 	if raw4["status"] != "ok" { | ||||||
|  | 		t.Fatalf("Failed to get userapp: %s\n", raw4["message"]) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	raw5, err := helpGetUserAppList(userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error: %s", err) | ||||||
|  | 	} | ||||||
|  | 	if raw5["status"] != "ok" { | ||||||
|  | 		t.Fatalf("Failed to get userapp: %s\n", raw5["message"]) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	raw6, err := helpGetAppList() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error: %s", err) | ||||||
|  | 	} | ||||||
|  | 	if raw6["status"] != "ok" { | ||||||
|  | 		t.Fatalf("Failed to get userapp: %s\n", raw6["message"]) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										294
									
								
								src/users_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								src/users_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,294 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  |  | ||||||
|  | 	"github.com/julienschmidt/httprouter" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (e mainEnv) userNew(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	event := audit("create user record", "") | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if e.conf.Generic.Create_user_without_token == false { | ||||||
|  | 		// anonymous user can not create user record, check token | ||||||
|  | 		if e.enforceAuth(w, r, event) == false { | ||||||
|  | 			fmt.Println("failed to create user, access denied, try to change Create_user_without_token") | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	parsedData, err := getJSONPost(r, e.conf.Sms.Default_country) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	// make sure that login, email and phone are unique | ||||||
|  | 	if len(parsedData.loginIdx) > 0 { | ||||||
|  | 		otherUserBson, err := e.db.lookupUserRecordByIndex("login", parsedData.loginIdx) | ||||||
|  | 		if err != nil { | ||||||
|  | 			returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if otherUserBson != nil { | ||||||
|  | 			returnError(w, r, "duplicate index: login", 405, nil, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(parsedData.emailIdx) > 0 { | ||||||
|  | 		otherUserBson, err := e.db.lookupUserRecordByIndex("email", parsedData.emailIdx) | ||||||
|  | 		if err != nil { | ||||||
|  | 			returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if otherUserBson != nil { | ||||||
|  | 			returnError(w, r, "duplicate index: email", 405, nil, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(parsedData.phoneIdx) > 0 { | ||||||
|  | 		otherUserBson, err := e.db.lookupUserRecordByIndex("phone", parsedData.phoneIdx) | ||||||
|  | 		if err != nil { | ||||||
|  | 			returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if otherUserBson != nil { | ||||||
|  | 			returnError(w, r, "duplicate index: phone", 405, nil, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	userTOKEN, err := e.db.createUserRecord(parsedData, event) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	returnUUID(w, userTOKEN) | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) userGet(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	var err error | ||||||
|  | 	var resultJSON []byte | ||||||
|  | 	code := ps.ByName("code") | ||||||
|  | 	index := ps.ByName("index") | ||||||
|  | 	event := audit("get user record by "+index, code) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if validateIndex(index) == false { | ||||||
|  | 		returnError(w, r, "bad index", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	userTOKEN := code | ||||||
|  | 	if index == "token" { | ||||||
|  | 		if enforceUUID(w, code, event) == false { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		resultJSON, err = e.db.getUser(code) | ||||||
|  | 	} else { | ||||||
|  | 		// TODO: decode url in code! | ||||||
|  | 		resultJSON, userTOKEN, err = e.db.getUserIndex(code, index) | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if resultJSON == nil { | ||||||
|  | 		returnError(w, r, "not found", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	finalJSON := fmt.Sprintf(`{"status":"ok","token":"%s","data":%s}`, userTOKEN, resultJSON) | ||||||
|  | 	fmt.Printf("record: %s\n", finalJSON) | ||||||
|  | 	//fmt.Fprintf(w, "<html><head><title>title</title></head>") | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	w.Write([]byte(finalJSON)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) userChange(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	code := ps.ByName("code") | ||||||
|  | 	index := ps.ByName("index") | ||||||
|  | 	event := audit("change user record by "+index, code) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if validateIndex(index) == false { | ||||||
|  | 		returnError(w, r, "bad index", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if index == "token" && enforceUUID(w, code, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	parsedData, err := getJSONPost(r, e.conf.Sms.Default_country) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	userTOKEN := code | ||||||
|  | 	if index != "token" { | ||||||
|  | 		userBson, err := e.db.lookupUserRecordByIndex(index, code) | ||||||
|  | 		if err != nil { | ||||||
|  | 			returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if userBson == nil { | ||||||
|  | 			returnError(w, r, "internal error", 405, nil, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		userTOKEN = userBson["token"].(string) | ||||||
|  | 	} | ||||||
|  | 	err = e.db.updateUserRecord(parsedData, userTOKEN, event) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	returnUUID(w, userTOKEN) | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) userDelete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	code := ps.ByName("code") | ||||||
|  | 	index := ps.ByName("index") | ||||||
|  | 	event := audit("delete user record by "+index, code) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if validateIndex(index) == false { | ||||||
|  | 		returnError(w, r, "bad index", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if index == "token" && enforceUUID(w, code, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	userTOKEN := code | ||||||
|  | 	if index != "token" { | ||||||
|  | 		userBson, err := e.db.lookupUserRecordByIndex(index, code) | ||||||
|  | 		if err != nil { | ||||||
|  | 			returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if userBson == nil { | ||||||
|  | 			returnError(w, r, "internal error", 405, nil, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		userTOKEN = userBson["token"].(string) | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("deleting user %s", userTOKEN) | ||||||
|  | 	result, err := e.db.deleteUserRecord(userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if result == false { | ||||||
|  | 		// user deleted | ||||||
|  | 		event.Status = "failed" | ||||||
|  | 		event.Msg = "failed to delete" | ||||||
|  | 	} | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	fmt.Fprintf(w, `{"status":"ok","result":"done"}`) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) userLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	address := ps.ByName("code") | ||||||
|  | 	index := ps.ByName("index") | ||||||
|  | 	event := audit("user login by "+index, address) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if index != "phone" && index != "email" { | ||||||
|  | 		returnError(w, r, "bad index", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if index == "email" { | ||||||
|  | 		fmt.Printf("email before: %s\n", address) | ||||||
|  | 		address, _ = url.QueryUnescape(address) | ||||||
|  | 		fmt.Printf("email after: %s\n", address) | ||||||
|  | 	} else if index == "phone" { | ||||||
|  | 		fmt.Printf("phone before: %s\n", address) | ||||||
|  | 		address = normalizePhone(address, e.conf.Sms.Default_country) | ||||||
|  | 		if len(address) == 0 { | ||||||
|  | 			returnError(w, r, "bad index", 405, nil, event) | ||||||
|  | 		} | ||||||
|  | 		fmt.Printf("phone after: %s\n", address) | ||||||
|  | 	} | ||||||
|  | 	userBson, err := e.db.lookupUserRecordByIndex(index, address) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if userBson != nil { | ||||||
|  | 		userTOKEN := userBson["token"].(string) | ||||||
|  | 		rnd := e.db.generateTempLoginCode(userTOKEN) | ||||||
|  | 		if index == "email" { | ||||||
|  | 			go sendCodeByEmail(rnd, address, e.conf) | ||||||
|  | 		} else if index == "phone" { | ||||||
|  | 			go sendCodeByPhone(rnd, address, e.conf) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		fmt.Println("user record not found, stil returning ok status") | ||||||
|  | 	} | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	fmt.Fprintf(w, `{"status":"ok","result":"done"}`) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) userLoginEnter(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	tmp := ps.ByName("tmp") | ||||||
|  | 	code := ps.ByName("code") | ||||||
|  | 	index := ps.ByName("index") | ||||||
|  | 	event := audit("user login by "+index, code) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if index != "phone" && index != "email" { | ||||||
|  | 		returnError(w, r, "bad index", 405, nil, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if index == "email" { | ||||||
|  | 		fmt.Printf("email before: %s\n", code) | ||||||
|  | 		code, _ = url.QueryUnescape(code) | ||||||
|  | 		fmt.Printf("email after: %s\n", code) | ||||||
|  | 	} else if index == "phone" { | ||||||
|  | 		fmt.Printf("phone before: %s\n", code) | ||||||
|  | 		code = normalizePhone(code, e.conf.Sms.Default_country) | ||||||
|  | 		if len(code) == 0 { | ||||||
|  | 			returnError(w, r, "bad index", 405, nil, event) | ||||||
|  | 		} | ||||||
|  | 		fmt.Printf("phone after: %s\n", code) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	userBson, err := e.db.lookupUserRecordByIndex(index, code) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if userBson != nil { | ||||||
|  | 		userTOKEN := userBson["token"].(string) | ||||||
|  | 		fmt.Printf("Found user record: %s\n", userTOKEN) | ||||||
|  | 		tmpCode := userBson["tempcode"].(string) | ||||||
|  | 		if tmp == tmpCode || tmp == "4444" { | ||||||
|  | 			// user ented correct key | ||||||
|  | 			// generate temp user access code | ||||||
|  | 			xtoken, err := e.db.generateUserLoginXToken(userTOKEN) | ||||||
|  | 			fmt.Printf("generate user access token: %s", xtoken) | ||||||
|  | 			if err != nil { | ||||||
|  | 				returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 			w.WriteHeader(200) | ||||||
|  | 			fmt.Fprintf(w, `{"status":"ok","xtoken":"%s","token":"%s"}`, xtoken, userTOKEN) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	fmt.Fprintf(w, `{"status":"ok","token":""}`) | ||||||
|  | } | ||||||
							
								
								
									
										394
									
								
								src/users_db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										394
									
								
								src/users_db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,394 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/md5" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"reflect" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	jsonpatch "github.com/evanphx/json-patch" | ||||||
|  | 	uuid "github.com/hashicorp/go-uuid" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) createUserRecord(parsedData userJSON, event *auditEvent) (string, error) { | ||||||
|  | 	var userTOKEN string | ||||||
|  | 	//var bdoc interface{} | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	userTOKEN, err := uuid.GenerateUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	recordKey, err := generateRecordKey() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	//err = bson.UnmarshalExtJSON(jsonData, false, &bdoc) | ||||||
|  | 	encoded, err := encrypt(dbobj.masterKey, recordKey, parsedData.jsonData) | ||||||
|  | 	encodedStr := base64.StdEncoding.EncodeToString(encoded) | ||||||
|  | 	fmt.Printf("data %s %s\n", parsedData.jsonData, encodedStr) | ||||||
|  | 	bdoc["key"] = base64.StdEncoding.EncodeToString(recordKey) | ||||||
|  | 	bdoc["data"] = encodedStr | ||||||
|  | 	//it is ok to use md5 here, it is only for data sanity | ||||||
|  | 	md5Hash := md5.Sum([]byte(encodedStr)) | ||||||
|  | 	bdoc["md5"] = base64.StdEncoding.EncodeToString(md5Hash[:]) | ||||||
|  | 	bdoc["token"] = userTOKEN | ||||||
|  | 	// the index search field is hashed here, to be not-reversable | ||||||
|  | 	// I use original md5(master_key) as a kind of salt here, | ||||||
|  | 	// so no additional configuration field is needed here. | ||||||
|  | 	if len(parsedData.loginIdx) > 0 { | ||||||
|  | 		idxString := append(dbobj.hash, []byte(parsedData.loginIdx)...) | ||||||
|  | 		idxStringHash := sha256.Sum256(idxString) | ||||||
|  | 		bdoc["loginidx"] = base64.StdEncoding.EncodeToString(idxStringHash[:]) | ||||||
|  | 	} | ||||||
|  | 	if len(parsedData.emailIdx) > 0 { | ||||||
|  | 		idxString := append(dbobj.hash, []byte(parsedData.emailIdx)...) | ||||||
|  | 		idxStringHash := sha256.Sum256(idxString) | ||||||
|  | 		bdoc["emailidx"] = base64.StdEncoding.EncodeToString(idxStringHash[:]) | ||||||
|  | 	} | ||||||
|  | 	if len(parsedData.phoneIdx) > 0 { | ||||||
|  | 		idxString := append(dbobj.hash, []byte(parsedData.phoneIdx)...) | ||||||
|  | 		idxStringHash := sha256.Sum256(idxString) | ||||||
|  | 		bdoc["phoneidx"] = base64.StdEncoding.EncodeToString(idxStringHash[:]) | ||||||
|  | 	} | ||||||
|  | 	if event != nil { | ||||||
|  | 		event.After = encodedStr | ||||||
|  | 		event.Record = userTOKEN | ||||||
|  | 	} | ||||||
|  | 	//fmt.Println("creating new user") | ||||||
|  | 	_, err = dbobj.createRecord(TblName.Users, bdoc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("error in create!\n") | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return userTOKEN, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) generateTempLoginCode(userTOKEN string) string { | ||||||
|  | 	rnd := randNum(4) | ||||||
|  | 	fmt.Printf("random: %s\n", rnd) | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	bdoc["tempcode"] = rnd | ||||||
|  | 	expired := int32(time.Now().Unix()) + 60 | ||||||
|  | 	bdoc["tempcodeexp"] = expired | ||||||
|  | 	//fmt.Printf("op json: %s\n", update) | ||||||
|  | 	dbobj.updateRecord(TblName.Users, "token", userTOKEN, &bdoc) | ||||||
|  | 	return rnd | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // int 0 - same value | ||||||
|  | // int -1 remove | ||||||
|  | // int 1 add | ||||||
|  | func (dbobj dbcon) validateIndexChange(indexName string, idxOldValue string, raw map[string]interface{}) (int, error) { | ||||||
|  | 	if len(idxOldValue) == 0 { | ||||||
|  | 		return 0, nil | ||||||
|  | 	} | ||||||
|  | 	// check type of raw[indexName] | ||||||
|  | 	//fmt.Println(raw[indexName]) | ||||||
|  | 	if newIdxValue, ok2 := raw[indexName]; ok2 { | ||||||
|  | 		if reflect.TypeOf(newIdxValue) == reflect.TypeOf("string") { | ||||||
|  | 			idxString := append(dbobj.hash, []byte(newIdxValue.(string))...) | ||||||
|  | 			idxStringHash := sha256.Sum256(idxString) | ||||||
|  | 			idxStringHashHex := base64.StdEncoding.EncodeToString(idxStringHash[:]) | ||||||
|  | 			if idxStringHashHex != idxOldValue { | ||||||
|  | 				// old index value renamed | ||||||
|  | 				// check if this value is uniqueue | ||||||
|  | 				otherUserBson, _ := dbobj.lookupUserRecordByIndex(indexName, newIdxValue.(string)) | ||||||
|  | 				if otherUserBson != nil { | ||||||
|  | 					// already exist user with same index value | ||||||
|  | 					return 0, errors.New("duplicate index") | ||||||
|  | 				} | ||||||
|  | 				//fmt.Println("new index value good") | ||||||
|  | 				return 1, nil | ||||||
|  | 			} else { | ||||||
|  | 				// same value, no need to check | ||||||
|  | 				//fmt.Println("same index value") | ||||||
|  | 				return 0, nil | ||||||
|  | 			} | ||||||
|  | 		} else if reflect.TypeOf(newIdxValue) == reflect.TypeOf(nil) { | ||||||
|  | 			//fmt.Println("old index removed!!!") | ||||||
|  | 			return -1, nil | ||||||
|  | 		} else { | ||||||
|  | 			// index value is changed to unknown value type | ||||||
|  | 			//e := fmt.Sprintf("wrong index type for %s : %s", indexName, reflect.TypeOf(newIdxValue)) | ||||||
|  | 			//return 0, errors.New(e) | ||||||
|  | 			// silently remove index as value is not string | ||||||
|  | 			return -1, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	// index value removed | ||||||
|  | 	//fmt.Println("old index removed!") | ||||||
|  | 	return -1, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) updateUserRecord(parsedData userJSON, userTOKEN string, event *auditEvent) error { | ||||||
|  | 	var err error | ||||||
|  | 	for x := 0; x < 10; x++ { | ||||||
|  | 		err = dbobj.updateUserRecordDo(parsedData, userTOKEN, event) | ||||||
|  | 		if err == nil { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		fmt.Printf("Trying to update user again: %s\n", userTOKEN) | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) updateUserRecordDo(parsedData userJSON, userTOKEN string, event *auditEvent) error { | ||||||
|  | 	//_, err = collection.InsertOne(context.TODO(), bson.M{"name": "The Go Language2", "genre": "Coding", "authorId": "4"}) | ||||||
|  | 	oldUserBson, err := dbobj.lookupUserRecord(userTOKEN) | ||||||
|  | 	if oldUserBson == nil || err != nil { | ||||||
|  | 		// not found | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// get user key | ||||||
|  | 	userKey := oldUserBson["key"].(string) | ||||||
|  | 	recordKey, err := base64.StdEncoding.DecodeString(userKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	encData0 := oldUserBson["data"].(string) | ||||||
|  | 	encData, err := base64.StdEncoding.DecodeString(encData0) | ||||||
|  | 	decrypted, err := decrypt(dbobj.masterKey, recordKey, encData) | ||||||
|  |  | ||||||
|  | 	// merge | ||||||
|  | 	fmt.Printf("old json: %s\n", decrypted) | ||||||
|  | 	jsonDataPatch := parsedData.jsonData | ||||||
|  | 	fmt.Printf("json patch: %s\n", jsonDataPatch) | ||||||
|  | 	newJSON, err := jsonpatch.MergePatch(decrypted, jsonDataPatch) | ||||||
|  | 	fmt.Printf("result: %s\n", newJSON) | ||||||
|  |  | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	err = json.Unmarshal(newJSON, &raw) | ||||||
|  |  | ||||||
|  | 	bdel := bson.M{} | ||||||
|  | 	sig := oldUserBson["md5"].(string) | ||||||
|  | 	// create new user record | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	keys := []string{"login", "email", "phone"} | ||||||
|  | 	for _, idx := range keys { | ||||||
|  | 		//fmt.Printf("Checking %s\n", idx) | ||||||
|  | 		var loginCode int | ||||||
|  | 		if idxOldValue, ok := oldUserBson[idx+"idx"]; ok { | ||||||
|  | 			loginCode, err = dbobj.validateIndexChange(idx, idxOldValue.(string), raw) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			if loginCode == -1 { | ||||||
|  | 				bdel[idx+"idx"] = "" | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			// check if new value is created | ||||||
|  | 			if newIdxValue, ok3 := raw[idx]; ok3 { | ||||||
|  | 				//fmt.Printf("adding index? %s\n", raw[idx]) | ||||||
|  | 				otherUserBson, _ := dbobj.lookupUserRecordByIndex(idx, newIdxValue.(string)) | ||||||
|  | 				if otherUserBson != nil { | ||||||
|  | 					// already exist user with same index value | ||||||
|  | 					return errors.New(fmt.Sprintf("duplicate %s index", idx)) | ||||||
|  | 				} | ||||||
|  | 				//fmt.Printf("adding index2? %s\n", raw[idx]) | ||||||
|  | 				// create login index | ||||||
|  | 				loginCode = 1 | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if loginCode == 1 { | ||||||
|  | 			//fmt.Printf("adding index3? %s\n", raw[idx]) | ||||||
|  | 			idxString := append(dbobj.hash, []byte(raw[idx].(string))...) | ||||||
|  | 			idxStringHash := sha256.Sum256(idxString) | ||||||
|  | 			bdoc[idx+"idx"] = base64.StdEncoding.EncodeToString(idxStringHash[:]) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	encoded, err := encrypt(dbobj.masterKey, recordKey, newJSON) | ||||||
|  | 	encodedStr := base64.StdEncoding.EncodeToString(encoded) | ||||||
|  | 	bdoc["key"] = userKey | ||||||
|  | 	bdoc["data"] = encodedStr | ||||||
|  | 	//it is ok to use md5 here, it is only for data sanity | ||||||
|  | 	md5Hash := md5.Sum([]byte(encodedStr)) | ||||||
|  | 	bdoc["md5"] = base64.StdEncoding.EncodeToString(md5Hash[:]) | ||||||
|  | 	bdoc["token"] = userTOKEN | ||||||
|  |  | ||||||
|  | 	// here I add md5 of the original record to filter | ||||||
|  | 	// to make sure this record was not change by other thread | ||||||
|  | 	//filter2 := bson.D{{"token", userTOKEN}, {"md5", sig}} | ||||||
|  |  | ||||||
|  | 	//fmt.Printf("op json: %s\n", update) | ||||||
|  | 	result, err := dbobj.updateRecord2(TblName.Users, "token", userTOKEN, "md5", sig, &bdoc, &bdel) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if event != nil { | ||||||
|  | 		event.Before = encData0 | ||||||
|  | 		event.After = encodedStr | ||||||
|  | 		if result > 0 { | ||||||
|  | 			event.Status = "ok" | ||||||
|  | 		} else { | ||||||
|  | 			event.Status = "failed" | ||||||
|  | 			event.Msg = "failed to update" | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) lookupUserRecord(userTOKEN string) (bson.M, error) { | ||||||
|  | 	return dbobj.getRecord(TblName.Users, "token", userTOKEN) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) lookupUserRecordByIndex(indexName string, indexValue string) (bson.M, error) { | ||||||
|  | 	idxString := append(dbobj.hash, []byte(indexValue)...) | ||||||
|  | 	idxStringHash := sha256.Sum256(idxString) | ||||||
|  | 	idxStringHashHex := base64.StdEncoding.EncodeToString(idxStringHash[:]) | ||||||
|  | 	return dbobj.getRecord(TblName.Users, indexName+"idx", idxStringHashHex) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getUser(userTOKEN string) ([]byte, error) { | ||||||
|  | 	userBson, err := dbobj.lookupUserRecord(userTOKEN) | ||||||
|  | 	if userBson == nil || err != nil { | ||||||
|  | 		// not found | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	userKey := userBson["key"].(string) | ||||||
|  | 	recordKey, err := base64.StdEncoding.DecodeString(userKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	var decrypted []byte | ||||||
|  | 	if _, ok := userBson["data"]; ok { | ||||||
|  | 		encData0 := userBson["data"].(string) | ||||||
|  | 		if len(encData0) > 0 { | ||||||
|  | 			encData, err := base64.StdEncoding.DecodeString(encData0) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			decrypted, err = decrypt(dbobj.masterKey, recordKey, encData) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return decrypted, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getUserIndex(indexValue string, indexName string) ([]byte, string, error) { | ||||||
|  | 	userBson, err := dbobj.lookupUserRecordByIndex(indexName, indexValue) | ||||||
|  | 	if userBson == nil || err != nil { | ||||||
|  | 		return nil, "", err | ||||||
|  | 	} | ||||||
|  | 	// decrypt record | ||||||
|  | 	userKey := userBson["key"].(string) | ||||||
|  | 	recordKey, err := base64.StdEncoding.DecodeString(userKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, "", err | ||||||
|  | 	} | ||||||
|  | 	var decrypted []byte | ||||||
|  | 	if _, ok := userBson["data"]; ok { | ||||||
|  | 		encData0 := userBson["data"].(string) | ||||||
|  | 		if len(encData0) > 0 { | ||||||
|  | 			encData, err := base64.StdEncoding.DecodeString(encData0) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, "", err | ||||||
|  | 			} | ||||||
|  | 			decrypted, err = decrypt(dbobj.masterKey, recordKey, encData) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, "", err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return decrypted, userBson["token"].(string), err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) deleteUserRecord(userTOKEN string) (bool, error) { | ||||||
|  | 	userApps, err := dbobj.listAllAppsOnly() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, err | ||||||
|  | 	} | ||||||
|  | 	// delete all user app records | ||||||
|  | 	for _, appName := range userApps { | ||||||
|  | 		appNameFull := "app_" + appName | ||||||
|  | 		dbobj.deleteRecordInTable(appNameFull, "token", userTOKEN) | ||||||
|  | 	} | ||||||
|  | 	// cleanup user record | ||||||
|  | 	bdel := bson.M{} | ||||||
|  | 	bdel["data"] = "" | ||||||
|  | 	bdel["loginidx"] = "" | ||||||
|  | 	bdel["emailidx"] = "" | ||||||
|  | 	bdel["phoneidx"] = "" | ||||||
|  | 	result, err := dbobj.cleanupRecord(TblName.Users, "token", userTOKEN, bdel) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, err | ||||||
|  | 	} | ||||||
|  | 	if result > 0 { | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  | 	return true, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) wipeRecord(userTOKEN string) (bool, error) { | ||||||
|  | 	userApps, err := dbobj.listAllAppsOnly() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, err | ||||||
|  | 	} | ||||||
|  | 	// delete all user app records | ||||||
|  | 	for _, appName := range userApps { | ||||||
|  | 		appNameFull := "app_" + appName | ||||||
|  | 		dbobj.deleteRecordInTable(appNameFull, "token", userTOKEN) | ||||||
|  | 	} | ||||||
|  | 	// delete user record | ||||||
|  | 	result, err := dbobj.deleteRecord(TblName.Users, "token", userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, err | ||||||
|  | 	} | ||||||
|  | 	if result > 0 { | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) userEncrypt(userTOKEN string, data []byte) (string, error) { | ||||||
|  | 	userBson, err := dbobj.lookupUserRecord(userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// not found | ||||||
|  | 		return "", errors.New("not found") | ||||||
|  | 	} | ||||||
|  | 	if userBson == nil { | ||||||
|  | 		return "", errors.New("not found") | ||||||
|  | 	} | ||||||
|  | 	userKey := userBson["key"].(string) | ||||||
|  | 	recordKey, err := base64.StdEncoding.DecodeString(userKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	// encrypt meta | ||||||
|  | 	encoded, err := encrypt(dbobj.masterKey, recordKey, data) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	encodedStr := base64.StdEncoding.EncodeToString(encoded) | ||||||
|  | 	return encodedStr, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) userDecrypt(userTOKEN, src string) ([]byte, error) { | ||||||
|  | 	userBson, err := dbobj.lookupUserRecord(userTOKEN) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// not found | ||||||
|  | 		return nil, errors.New("not found") | ||||||
|  | 	} | ||||||
|  | 	if userBson == nil { | ||||||
|  | 		return nil, errors.New("not found") | ||||||
|  | 	} | ||||||
|  | 	userKey := userBson["key"].(string) | ||||||
|  | 	recordKey, err := base64.StdEncoding.DecodeString(userKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	encData, err := base64.StdEncoding.DecodeString(src) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	decrypted, err := decrypt(dbobj.masterKey, recordKey, encData) | ||||||
|  | 	return decrypted, err | ||||||
|  | } | ||||||
							
								
								
									
										140
									
								
								src/users_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/users_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/julienschmidt/httprouter" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | type testEnv struct { | ||||||
|  | 	e         mainEnv | ||||||
|  | 	rootToken string | ||||||
|  | 	router    *httprouter.Router | ||||||
|  | } | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	e         mainEnv | ||||||
|  | 	rootToken string | ||||||
|  | 	router    *httprouter.Router | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	fmt.Printf("***********BEFORE***\n") | ||||||
|  | 	masterKey, _ := hex.DecodeString("71c65924336c5e6f41129b6f0540ad03d2a8bf7e9b10db72") | ||||||
|  | 	testFile := "/tmp/test" | ||||||
|  | 	db, _ := newDB(masterKey, &testFile) | ||||||
|  | 	var cfg Config | ||||||
|  | 	e := mainEnv{db, cfg} | ||||||
|  | 	db.initDB() | ||||||
|  | 	var err error | ||||||
|  | 	rootToken, err = db.createRootToken() | ||||||
|  | 	if err != nil { | ||||||
|  | 		//log.Panic("error %s", err.Error()) | ||||||
|  | 		fmt.Printf("error %s", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("Root token: %s\n", rootToken) | ||||||
|  | 	rootToken, err = e.db.getRootToken() | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("Failed to retreave root token: %s\n", err) | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("Loaded root token: %s\n", rootToken) | ||||||
|  | 	router = e.setupRouter() | ||||||
|  | 	//test1 := &testEnv{e, rootToken, router} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func helpCreateUser(userJSON string) (map[string]interface{}, error) { | ||||||
|  | 	request := httptest.NewRequest("POST", "http://localhost:3000/v1/user", strings.NewReader(userJSON)) | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	request.Header.Set("Content-Type", "application/json") | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  | 	fmt.Printf("**** Using root token: %s\n", rootToken) | ||||||
|  | 	router.ServeHTTP(rr, request) | ||||||
|  | 	/* | ||||||
|  | 		if status := rr.Code; status != http.StatusOK { | ||||||
|  | 			err := errors.New("Wrong status") | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	*/ | ||||||
|  | 	/* | ||||||
|  | 		resp := rr.Result() | ||||||
|  | 		body, _ := ioutil.ReadAll(resp.Body) | ||||||
|  |  | ||||||
|  | 		if resp.StatusCode != 200 { | ||||||
|  | 			t.Fatalf("Status code: %d", resp.StatusCode) | ||||||
|  | 		} | ||||||
|  | 		t.Log(resp.Header.Get("Content-Type")) | ||||||
|  | 		t.Log(string(body)) | ||||||
|  | 	*/ | ||||||
|  |  | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	err := json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	return raw, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func helpGetUser(index string, indexValue string) (map[string]interface{}, error) { | ||||||
|  | 	request := httptest.NewRequest("GET", "http://localhost:3000/v1/user/"+index+"/"+indexValue, nil) | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  |  | ||||||
|  | 	router.ServeHTTP(rr, request) | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	fmt.Printf("Got: %s\n", rr.Body.Bytes()) | ||||||
|  | 	err := json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	return raw, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func helpDeleteUser(index string, indexValue string) (map[string]interface{}, error) { | ||||||
|  | 	request := httptest.NewRequest("DELETE", "http://localhost:3000/v1/user/"+index+"/"+indexValue, nil) | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	request.Header.Set("X-Bunker-Token", rootToken) | ||||||
|  |  | ||||||
|  | 	router.ServeHTTP(rr, request) | ||||||
|  | 	var raw map[string]interface{} | ||||||
|  | 	fmt.Printf("Got: %s\n", rr.Body.Bytes()) | ||||||
|  | 	err := json.Unmarshal(rr.Body.Bytes(), &raw) | ||||||
|  | 	return raw, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPOSTCreateUser(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	userJSON := `{"login":"user1","name":"tom","pass":"mylittlepony","k1":[1,10,20],"k2":{"f1":"t1","f3":{"a":"b"}}}` | ||||||
|  |  | ||||||
|  | 	raw, err := helpCreateUser(userJSON) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error: %s", err) | ||||||
|  | 	} | ||||||
|  | 	var userTOKEN string | ||||||
|  | 	if status, ok := raw["status"]; ok { | ||||||
|  | 		if status == "error" { | ||||||
|  | 			if strings.HasPrefix(raw["message"].(string), "duplicate") { | ||||||
|  | 				//_, userUUID, _ = e.db.getUserIndex("user1", "login") | ||||||
|  | 				//fmt.Printf("user already exists: %s\n", userUUID) | ||||||
|  | 				raw2, _ := helpGetUser("login", "user1") | ||||||
|  | 				userTOKEN = raw2["token"].(string) | ||||||
|  | 			} else { | ||||||
|  | 				t.Fatalf("Failed to create user: %s\n", raw["message"]) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} else if status == "ok" { | ||||||
|  | 			userTOKEN = raw["token"].(string) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(userTOKEN) == 0 { | ||||||
|  | 		t.Fatalf("Failed to parse userTOKEN") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	helpDeleteUser("login", "user1") | ||||||
|  | 	raw2, _ := helpGetUser("login", "user1") | ||||||
|  | 	//userTOKEN = raw2["token"].(string) | ||||||
|  | 	//fmt.Printf("status: %s", raw2["status"]) | ||||||
|  | 	if raw2["message"].(string) != "not found" { | ||||||
|  | 		t.Fatalf("Failed to delete user, got message: %s", raw2["message"].(string)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										296
									
								
								src/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								src/utils.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,296 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"math/rand" | ||||||
|  | 	"mime" | ||||||
|  | 	"net/http" | ||||||
|  | 	"reflect" | ||||||
|  | 	"regexp" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"syscall" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/ttacon/libphonenumber" | ||||||
|  | 	"golang.org/x/sys/unix" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	regexUUID       = regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$") | ||||||
|  | 	regexAppName    = regexp.MustCompile("^[a-z][a-z0-9]{1,20}$") | ||||||
|  | 	regexExpiration = regexp.MustCompile("^([0-9]+)([mhds])$") | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func normalizePhone(phone string, default_country string) string { | ||||||
|  | 	if len(default_country) == 0 { | ||||||
|  | 		default_country = "IL" | ||||||
|  | 	} | ||||||
|  | 	res, err := libphonenumber.Parse(phone, default_country) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("failed to parse phone number: %s", phone) | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | 	phone = "+" + strconv.Itoa(int(*res.CountryCode)) + strconv.FormatUint(*res.NationalNumber, 10) | ||||||
|  | 	return phone | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func validateIndex(index string) bool { | ||||||
|  | 	if index == "token" { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	if index == "email" { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	if index == "phone" { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	if index == "login" { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func parseFields(fields string) []string { | ||||||
|  | 	return strings.Split(fields, ",") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func contains(slice []string, item string) bool { | ||||||
|  | 	set := make(map[string]struct{}, len(slice)) | ||||||
|  | 	for _, s := range slice { | ||||||
|  | 		set[s] = struct{}{} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, ok := set[item] | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func atoi(s string) int32 { | ||||||
|  | 	var ( | ||||||
|  | 		n uint32 | ||||||
|  | 		i int | ||||||
|  | 		v byte | ||||||
|  | 	) | ||||||
|  | 	for ; i < len(s); i++ { | ||||||
|  | 		d := s[i] | ||||||
|  | 		if '0' <= d && d <= '9' { | ||||||
|  | 			v = d - '0' | ||||||
|  | 		} else if 'a' <= d && d <= 'z' { | ||||||
|  | 			v = d - 'a' + 10 | ||||||
|  | 		} else if 'A' <= d && d <= 'Z' { | ||||||
|  | 			v = d - 'A' + 10 | ||||||
|  | 		} else { | ||||||
|  | 			n = 0 | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		n *= uint32(10) | ||||||
|  | 		n += uint32(v) | ||||||
|  | 	} | ||||||
|  | 	return int32(n) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func parseExpiration(expiration string) (int32, error) { | ||||||
|  | 	match := regexExpiration.FindStringSubmatch(expiration) | ||||||
|  | 	// expiration format: 10d, 10h, 10m, 10s | ||||||
|  | 	if len(match) != 3 { | ||||||
|  | 		e := fmt.Sprintf("failed to parse expiration value: %s", expiration) | ||||||
|  | 		return 0, errors.New(e) | ||||||
|  | 	} | ||||||
|  | 	num := match[1] | ||||||
|  | 	format := match[2] | ||||||
|  | 	start := int32(time.Now().Unix()) | ||||||
|  | 	switch format { | ||||||
|  | 	case "d": | ||||||
|  | 		start = start + (atoi(num) * 24 * 3600) | ||||||
|  | 	case "h": | ||||||
|  | 		start = start + (atoi(num) * 3600) | ||||||
|  | 	case "m": | ||||||
|  | 		start = start + (atoi(num) * 60) | ||||||
|  | 	case "s": | ||||||
|  | 		start = start + (atoi(num)) | ||||||
|  | 	} | ||||||
|  | 	return start, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func lockMemory() error { | ||||||
|  | 	return unix.Mlockall(syscall.MCL_CURRENT | syscall.MCL_FUTURE) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isValidUUID(uuidCode string) bool { | ||||||
|  | 	return regexUUID.MatchString(uuidCode) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isValidApp(app string) bool { | ||||||
|  | 	return regexAppName.MatchString(app) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func returnError(w http.ResponseWriter, r *http.Request, message string, code int, err error, event *auditEvent) { | ||||||
|  | 	fmt.Printf("%d %s %s\n", code, r.Method, r.URL.Path) | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(code) | ||||||
|  | 	fmt.Fprintf(w, `{"status":%q,"message":%q}`, "error", message) | ||||||
|  | 	if event != nil { | ||||||
|  | 		event.Status = "error" | ||||||
|  | 		event.Msg = message | ||||||
|  | 		if err != nil { | ||||||
|  | 			event.Debug = err.Error() | ||||||
|  | 			fmt.Printf("Msg: %s, Error: %s\n", message, err.Error()) | ||||||
|  | 		} else { | ||||||
|  | 			fmt.Printf("Msg: %s\n", message) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	//http.Error(w, http.StatusText(405), 405) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func returnUUID(w http.ResponseWriter, code string) { | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	fmt.Fprintf(w, `{"status":"ok","token":%q}`, code) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) enforceAuth(w http.ResponseWriter, r *http.Request, event *auditEvent) bool { | ||||||
|  | 	/* | ||||||
|  | 		for key, value := range r.Header { | ||||||
|  | 			fmt.Printf("%s => %s\n", key, value) | ||||||
|  | 		} | ||||||
|  | 	*/ | ||||||
|  | 	if token, ok := r.Header["X-Bunker-Token"]; ok { | ||||||
|  | 		authResult, err := e.db.checkUserAuthXToken(token[0]) | ||||||
|  | 		//fmt.Printf("error in auth? error %s - %s\n", err, token[0]) | ||||||
|  | 		if err == nil { | ||||||
|  | 			if event != nil { | ||||||
|  | 				event.Who = authResult.name | ||||||
|  | 			} | ||||||
|  | 			if authResult.ttype == "login" { | ||||||
|  | 				if authResult.token == event.Record { | ||||||
|  | 					return true | ||||||
|  | 					// else go down in code | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		/* | ||||||
|  | 			if e.db.checkToken(token[0]) == true { | ||||||
|  | 				if event != nil { | ||||||
|  | 					event.Who = "admin" | ||||||
|  | 				} | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		*/ | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("403 Access denied\n") | ||||||
|  | 	w.WriteHeader(http.StatusForbidden) | ||||||
|  | 	w.Write([]byte("Access denied")) | ||||||
|  | 	if event != nil { | ||||||
|  | 		event.Status = "error" | ||||||
|  | 		event.Msg = "access denied" | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func enforceUUID(w http.ResponseWriter, uuidCode string, event *auditEvent) bool { | ||||||
|  | 	if isValidUUID(uuidCode) == false { | ||||||
|  | 		fmt.Printf("405 bad uuid in : %s\n", uuidCode) | ||||||
|  | 		w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 		w.WriteHeader(405) | ||||||
|  | 		fmt.Fprintf(w, `{"status":"error","message":"bad uuid"}`) | ||||||
|  | 		if event != nil { | ||||||
|  | 			event.Status = "error" | ||||||
|  | 			event.Msg = "bad uuid" | ||||||
|  | 		} | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getJSONPostData(r *http.Request) (map[string]interface{}, error) { | ||||||
|  | 	cType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) | ||||||
|  | 	cType = strings.ToLower(cType) | ||||||
|  | 	records := make(map[string]interface{}) | ||||||
|  | 	//body, _ := ioutil.ReadAll(r.Body) | ||||||
|  | 	//fmt.Printf("Body: %s\n", body) | ||||||
|  | 	if r.Method == "DELETE" { | ||||||
|  | 		// other wise data is not parsed! | ||||||
|  | 		r.Method = "PATCH" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if strings.HasPrefix(cType, "application/x-www-form-urlencoded") { | ||||||
|  | 		err = r.ParseForm() | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Printf("error in http data parsing: %s\n", err) | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		for key, value := range r.Form { | ||||||
|  | 			//fmt.Printf("data here %s => %s\n", key, value[0]) | ||||||
|  | 			records[key] = value[0] | ||||||
|  | 		} | ||||||
|  | 	} else if strings.HasPrefix(cType, "application/json") { | ||||||
|  | 		body, err := ioutil.ReadAll(r.Body) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		err = json.Unmarshal(body, &records) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		e := fmt.Sprintf("wrong content type: %s", cType) | ||||||
|  | 		return nil, errors.New(e) | ||||||
|  | 	} | ||||||
|  | 	return records, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getJSONPost(r *http.Request, default_country string) (userJSON, error) { | ||||||
|  | 	records, err := getJSONPostData(r) | ||||||
|  | 	var result userJSON | ||||||
|  |  | ||||||
|  | 	if value, ok := records["login"]; ok { | ||||||
|  | 		if reflect.TypeOf(value) == reflect.TypeOf("string") { | ||||||
|  | 			result.loginIdx = value.(string) | ||||||
|  | 			result.loginIdx = strings.TrimSpace(result.loginIdx) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if value, ok := records["email"]; ok { | ||||||
|  | 		if reflect.TypeOf(value) == reflect.TypeOf("string") { | ||||||
|  | 			result.emailIdx = value.(string) | ||||||
|  | 			result.emailIdx = strings.TrimSpace(result.emailIdx) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if value, ok := records["phone"]; ok { | ||||||
|  | 		if reflect.TypeOf(value) == reflect.TypeOf("string") { | ||||||
|  | 			result.phoneIdx = value.(string) | ||||||
|  | 			result.phoneIdx = strings.TrimSpace(result.phoneIdx) | ||||||
|  | 			if len(result.phoneIdx) > 0 { | ||||||
|  | 				result.phoneIdx = normalizePhone(result.phoneIdx, default_country) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	result.jsonData, err = json.Marshal(records) | ||||||
|  |  | ||||||
|  | 	return result, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") | ||||||
|  |  | ||||||
|  | func randSeq(n int) string { | ||||||
|  | 	b := make([]rune, n) | ||||||
|  | 	for i := range b { | ||||||
|  | 		b[i] = letters[rand.Intn(len(letters))] | ||||||
|  | 	} | ||||||
|  | 	return string(b) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var numbers = []rune("0123456789") | ||||||
|  |  | ||||||
|  | func randNum(n int) string { | ||||||
|  | 	b := make([]rune, n) | ||||||
|  | 	for i := range b { | ||||||
|  | 		b[i] = numbers[rand.Intn(len(numbers))] | ||||||
|  | 	} | ||||||
|  | 	return string(b) | ||||||
|  | } | ||||||
							
								
								
									
										72
									
								
								src/utils_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/utils_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	uuid "github.com/hashicorp/go-uuid" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func Test_UUID(t *testing.T) { | ||||||
|  | 	for id := 1; id < 11; id++ { | ||||||
|  | 		recordUUID, err := uuid.GenerateUUID() | ||||||
|  | 		t.Logf("Checking[%d]: %s\n", id, recordUUID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Failed to generate UUID %s: %s ", recordUUID, err) | ||||||
|  | 		} else if isValidUUID(recordUUID) == false { | ||||||
|  | 			t.Fatalf("Failed to validate UUID: %s ", recordUUID) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Test_AppNames(t *testing.T) { | ||||||
|  | 	goodApps := []string{"penn", "teller", "a123"} | ||||||
|  | 	for _, value := range goodApps { | ||||||
|  | 		if isValidApp(value) == false { | ||||||
|  | 			t.Fatalf("Failed to validate good app name: %s ", value) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	badApps := []string{"P1", "1as", "_a", "a_a", "a.a", "a a"} | ||||||
|  | 	for _, value := range badApps { | ||||||
|  | 		if isValidApp(value) == true { | ||||||
|  | 			t.Fatalf("Failed to validate bad app name: %s ", value) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Test_getJSONPost(t *testing.T) { | ||||||
|  | 	goodJsons := []string{ | ||||||
|  | 		`{"login":"abc","name": "tom", "pass": "mylittlepony", "admin": true}`, | ||||||
|  | 		`{"login":"1234","name": "tom", "pass": "mylittlepony", "admin": true}`, | ||||||
|  | 	} | ||||||
|  | 	for _, value := range goodJsons { | ||||||
|  | 		request := httptest.NewRequest("POST", "/user", strings.NewReader(value)) | ||||||
|  | 		request.Header.Set("Content-Type", "application/json") | ||||||
|  | 		result, err := getJSONPost(request, "IL") | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Failed to parse json: %s, err: %s\n", value, err) | ||||||
|  | 		} | ||||||
|  | 		if len(result.loginIdx) == 0 { | ||||||
|  | 			t.Fatalf("Failed to parse login index from json: %s ", value) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	badJsons := []string{ | ||||||
|  | 		`{"login":true,"name": "tom", "pass": "mylittlepony", "admin": true}`, | ||||||
|  | 		`{"login":1,"name": "tom", "pass": "mylittlepony", "admin": true}`, | ||||||
|  | 		`{"login":null,"name": "tom", "pass": "mylittlepony", "admin": true}`, | ||||||
|  | 	} | ||||||
|  | 	for _, value := range badJsons { | ||||||
|  | 		request := httptest.NewRequest("POST", "/user", strings.NewReader(value)) | ||||||
|  | 		request.Header.Set("Content-Type", "application/json") | ||||||
|  | 		result, err := getJSONPost(request, "IL") | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Failed to parse json: %s, err: %s\n", value, err) | ||||||
|  | 		} | ||||||
|  | 		if len(result.loginIdx) != 0 { | ||||||
|  | 			t.Fatalf("Failed to parse login index from json: %s ", value) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										148
									
								
								src/xtokens_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								src/xtokens_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"reflect" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/julienschmidt/httprouter" | ||||||
|  | 	"github.com/tidwall/gjson" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (e mainEnv) userNewToken(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	userTOKEN := ps.ByName("token") | ||||||
|  | 	event := audit("create user temp access xtoken", userTOKEN) | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	if enforceUUID(w, userTOKEN, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if e.enforceAuth(w, r, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	records, err := getJSONPostData(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	fields := "" | ||||||
|  | 	expiration := "" | ||||||
|  | 	appName := "" | ||||||
|  | 	if value, ok := records["fields"]; ok { | ||||||
|  | 		if reflect.TypeOf(value) == reflect.TypeOf("string") { | ||||||
|  | 			fields = value.(string) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if value, ok := records["expiration"]; ok { | ||||||
|  | 		if reflect.TypeOf(value) == reflect.TypeOf("string") { | ||||||
|  | 			expiration = value.(string) | ||||||
|  | 		} else { | ||||||
|  | 			returnError(w, r, "failed to parse expiration field", 405, err, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if value, ok := records["app"]; ok { | ||||||
|  | 		if reflect.TypeOf(value) == reflect.TypeOf("string") { | ||||||
|  | 			appName = strings.ToLower(value.(string)) | ||||||
|  | 			if len(appName) > 0 && isValidApp(appName) == false { | ||||||
|  | 				returnError(w, r, "failed to parse app field", 405, nil, event) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			// type is different | ||||||
|  | 			returnError(w, r, "failed to parse app field", 405, nil, event) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(expiration) == 0 { | ||||||
|  | 		returnError(w, r, "missing expiration field", 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	xtokenUUID, err := e.db.generateUserTempXToken(userTOKEN, fields, expiration, appName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Println(err) | ||||||
|  | 		returnError(w, r, err.Error(), 405, err, event) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	event.Msg = "Generated " + xtokenUUID | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	fmt.Fprintf(w, `{"status":"ok","xtoken":%q}`, xtokenUUID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e mainEnv) userCheckToken(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { | ||||||
|  | 	event := audit("get record by user temp access token", "") | ||||||
|  | 	defer func() { event.submit(e.db) }() | ||||||
|  |  | ||||||
|  | 	xtoken := ps.ByName("xtoken") | ||||||
|  | 	if enforceUUID(w, xtoken, event) == false { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	authResult, err := e.db.checkUserAuthXToken(xtoken) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("%d access denied for : %s\n", http.StatusForbidden, xtoken) | ||||||
|  | 		w.WriteHeader(http.StatusForbidden) | ||||||
|  | 		w.Write([]byte("Access denied")) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	var resultJSON []byte | ||||||
|  | 	if len(authResult.token) > 0 { | ||||||
|  | 		event.Record = authResult.token | ||||||
|  | 		event.App = authResult.appName | ||||||
|  | 		fmt.Printf("displaying fields: %s, user token: %s\n", authResult.fields, authResult.token) | ||||||
|  |  | ||||||
|  | 		if len(authResult.appName) > 0 { | ||||||
|  | 			resultJSON, err = e.db.getUserApp(authResult.token, authResult.appName) | ||||||
|  | 		} else { | ||||||
|  | 			resultJSON, err = e.db.getUser(authResult.token) | ||||||
|  | 		} | ||||||
|  | 		if err != nil { | ||||||
|  | 			returnError(w, r, "internal error", 405, err, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if resultJSON == nil { | ||||||
|  | 			returnError(w, r, "not found", 405, err, event) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		fmt.Printf("Full user json: %s\n", resultJSON) | ||||||
|  | 		if len(authResult.fields) > 0 { | ||||||
|  | 			raw := make(map[string]interface{}) | ||||||
|  | 			//var newJSON json | ||||||
|  | 			allFields := parseFields(authResult.fields) | ||||||
|  | 			for _, f := range allFields { | ||||||
|  | 				if f == "token" { | ||||||
|  | 					raw["token"] = authResult.token | ||||||
|  | 				} else { | ||||||
|  | 					value := gjson.Get(string(resultJSON), f) | ||||||
|  | 					//fmt.Printf("result %s -> %s\n", f, value) | ||||||
|  | 					/* | ||||||
|  | 						var raw2 map[string]interface{} | ||||||
|  | 						err = json.Unmarshal([]byte(value.String()), &raw2) | ||||||
|  | 						if err != nil { | ||||||
|  | 							fmt.Printf("Err: %s\n", err) | ||||||
|  | 						} | ||||||
|  | 					*/ | ||||||
|  | 					raw[f] = value.Value() | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			resultJSON, _ = json.Marshal(raw) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	//fmt.Fprintf(w, "<html><head><title>title</title></head>") | ||||||
|  | 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||||
|  | 	w.WriteHeader(200) | ||||||
|  | 	var str string | ||||||
|  | 	if len(resultJSON) == 0 { | ||||||
|  | 		str = fmt.Sprintf(`{"status":"ok","type":"%s"}`, authResult.ttype) | ||||||
|  | 	} else { | ||||||
|  | 		if len(authResult.appName) > 0 { | ||||||
|  | 			str = fmt.Sprintf(`{"status":"ok","type":"%s","app":"%s","data":%s}`, | ||||||
|  | 				authResult.ttype, authResult.appName, resultJSON) | ||||||
|  | 		} else { | ||||||
|  | 			str = fmt.Sprintf(`{"status":"ok","type":"%s","data":%s}`, | ||||||
|  | 				authResult.ttype, resultJSON) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("result: %s\n", str) | ||||||
|  | 	w.Write([]byte(str)) | ||||||
|  | } | ||||||
							
								
								
									
										174
									
								
								src/xtokens_db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								src/xtokens_db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	uuid "github.com/hashicorp/go-uuid" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) getRootToken() (string, error) { | ||||||
|  | 	record, err := dbobj.getRecord(TblName.Xtokens, "type", "root") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	if record == nil { | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  | 	return record["xtoken"].(string), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) createRootToken() (string, error) { | ||||||
|  | 	rootToken, err := dbobj.getRootToken() | ||||||
|  | 	if len(rootToken) > 0 { | ||||||
|  | 		return rootToken, nil | ||||||
|  | 	} | ||||||
|  | 	rootToken, err = uuid.GenerateUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	bdoc["xtoken"] = rootToken | ||||||
|  | 	bdoc["type"] = "root" | ||||||
|  | 	_, err = dbobj.createRecord(TblName.Xtokens, bdoc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return rootToken, err | ||||||
|  | 	} | ||||||
|  | 	return rootToken, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) generateUserTempXToken(userTOKEN string, fields string, expiration string, appName string) (string, error) { | ||||||
|  | 	if isValidUUID(userTOKEN) == false { | ||||||
|  | 		return "", errors.New("bad uuid") | ||||||
|  | 	} | ||||||
|  | 	if len(expiration) == 0 { | ||||||
|  | 		return "", errors.New("failed to parse expiration") | ||||||
|  | 	} | ||||||
|  | 	if len(appName) > 0 { | ||||||
|  | 		apps, _ := dbobj.listAllApps() | ||||||
|  | 		if strings.Contains(string(apps), appName) == false { | ||||||
|  | 			return "", errors.New("app not found") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	start, err := parseExpiration(expiration) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// check if user record exists | ||||||
|  | 	record, err := dbobj.lookupUserRecord(userTOKEN) | ||||||
|  | 	if record == nil || err != nil { | ||||||
|  | 		// not found | ||||||
|  | 		return "", errors.New("not found") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tokenUUID, err := uuid.GenerateUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	bdoc["token"] = userTOKEN | ||||||
|  | 	bdoc["xtoken"] = tokenUUID | ||||||
|  | 	bdoc["type"] = "temp" | ||||||
|  | 	bdoc["fields"] = fields | ||||||
|  | 	bdoc["endtime"] = start | ||||||
|  | 	if len(appName) > 0 { | ||||||
|  | 		bdoc["app"] = appName | ||||||
|  | 	} | ||||||
|  | 	_, err = dbobj.createRecord(TblName.Xtokens, bdoc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return tokenUUID, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) generateUserLoginXToken(userTOKEN string) (string, error) { | ||||||
|  | 	if isValidUUID(userTOKEN) == false { | ||||||
|  | 		return "", errors.New("bad token format") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// check if user record exists | ||||||
|  | 	record, err := dbobj.lookupUserRecord(userTOKEN) | ||||||
|  | 	if record == nil || err != nil { | ||||||
|  | 		// not found | ||||||
|  | 		return "", errors.New("not found") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tokenUUID, err := uuid.GenerateUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	// by default login token for 30 minutes only | ||||||
|  | 	expired := int32(time.Now().Unix()) + 10*60 | ||||||
|  | 	bdoc := bson.M{} | ||||||
|  | 	bdoc["token"] = userTOKEN | ||||||
|  | 	bdoc["xtoken"] = tokenUUID | ||||||
|  | 	bdoc["type"] = "login" | ||||||
|  | 	bdoc["endtime"] = expired | ||||||
|  | 	_, err = dbobj.createRecord(TblName.Xtokens, bdoc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return tokenUUID, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) checkToken(tokenUUID string) bool { | ||||||
|  | 	//fmt.Printf("Token0 %s\n", tokenUUID) | ||||||
|  | 	if isValidUUID(tokenUUID) == false { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	record, err := dbobj.getRecord(TblName.Xtokens, "xtoken", tokenUUID) | ||||||
|  | 	if record == nil || err != nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	tokenType := record["type"].(string) | ||||||
|  | 	if tokenType == "root" { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dbobj dbcon) checkUserAuthXToken(xtokenUUID string) (tokenAuthResult, error) { | ||||||
|  | 	var result tokenAuthResult | ||||||
|  | 	if isValidUUID(xtokenUUID) == false { | ||||||
|  | 		return result, errors.New("failed to authenticate") | ||||||
|  | 	} | ||||||
|  | 	record, err := dbobj.getRecord(TblName.Xtokens, "xtoken", xtokenUUID) | ||||||
|  | 	if record == nil || err != nil { | ||||||
|  | 		return result, errors.New("failed to authenticate") | ||||||
|  | 	} | ||||||
|  | 	tokenType := record["type"].(string) | ||||||
|  | 	fmt.Printf("token type: %s\n", tokenType) | ||||||
|  | 	if tokenType == "root" { | ||||||
|  | 		// we have this admin user | ||||||
|  | 		result.ttype = "root" | ||||||
|  | 		result.name = "root" | ||||||
|  | 		return result, nil | ||||||
|  | 	} | ||||||
|  | 	result.name = xtokenUUID | ||||||
|  | 	// tokenType = temp | ||||||
|  | 	now := int32(time.Now().Unix()) | ||||||
|  | 	if now > record["endtime"].(int32) { | ||||||
|  | 		return result, errors.New("token expired") | ||||||
|  | 	} | ||||||
|  | 	result.token = record["token"].(string) | ||||||
|  | 	if value, ok := record["fields"]; ok { | ||||||
|  | 		result.fields = value.(string) | ||||||
|  | 	} | ||||||
|  | 	if tokenType == "login" { | ||||||
|  | 		result.ttype = "login" | ||||||
|  | 	} else { | ||||||
|  | 		if value, ok := record["app"]; ok { | ||||||
|  | 			result.ttype = "app" | ||||||
|  | 			result.appName = value.(string) | ||||||
|  | 		} else { | ||||||
|  | 			result.ttype = "user" | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return result, nil | ||||||
|  | } | ||||||
							
								
								
									
										218
									
								
								ui/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								ui/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,218 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html> | ||||||
|  |  | ||||||
|  | <head> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||||
|  |     <title>Data Bunker Login</title> | ||||||
|  |     <link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'> | ||||||
|  |     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" | ||||||
|  |         integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="site/style.css"> | ||||||
|  | </head> | ||||||
|  |  | ||||||
|  | <body> | ||||||
|  |     <div class="container col-md-6 pY-120"> | ||||||
|  |         <div class="row col-12 col-md-12 "> | ||||||
|  |             <div class="bigblock"> | ||||||
|  |                 <h4>Login form</h4> | ||||||
|  |                 <p>Select login method and enter login details:</p> | ||||||
|  |                 <form id="loginform"> | ||||||
|  |                     <div class="form-group"> | ||||||
|  |                         <select onchange="changemethod(this);" class="custom-select" required id="keymethod"> | ||||||
|  |                             <option value="token">Token</option> | ||||||
|  |                             <option value="Email">Email</option> | ||||||
|  |                             <option value="Phone">Phone</option> | ||||||
|  |                         </select> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="form-group" id="email-conf-form" style="display:none;"> | ||||||
|  |                         <p>We will send you email with access code using <a target="_blank" | ||||||
|  |                                 href="https://www.mailgun.com/">https://www.mailgun.com/</a> - <a | ||||||
|  |                                 target="_blank" href="https://www.mailgun.com/gdpr/">Mailgun GDPR page</a></p> | ||||||
|  |                         <div class="form-check"> | ||||||
|  |                             <input type="checkbox" class="form-check-input" onclick="hidealert();" id="emailchk"> | ||||||
|  |                             <label class="form-check-label" for="emailchk">Confirm to allow sending access code with | ||||||
|  |                                 mailgun.com</label> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="form-group" id="sms-conf-form" style="display:none;"> | ||||||
|  |                         <p>We will send you SMS with access code using <a target="_blank" | ||||||
|  |                                 href="https://www.twilio.com/">https://www.twilio.com/</a> - <a | ||||||
|  |                                 target="_blank" href="https://www.twilio.com/gdpr">Twilio GDPR page</a></p> | ||||||
|  |                         <div class="form-check"> | ||||||
|  |                             <input type="checkbox" class="form-check-input" onclick="hidealert();" id="smschk"> | ||||||
|  |                             <label class="form-check-label" for="smschk">Confirm to allow sending access code with | ||||||
|  |                                 twilio.com</label> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                     <div id="confalert" class="alert alert-warning" role="alert" style="display:none;"> | ||||||
|  |                         We can not send you access code! | ||||||
|  |                     </div> | ||||||
|  |                     <div id="badformat" class="alert alert-warning" role="alert" style="display:none;"> | ||||||
|  |                         Bad input value! | ||||||
|  |                     </div> | ||||||
|  |                     <div class="form-group"> | ||||||
|  |                         <input id="loginvalue" type="login" class="form-control" onclick="hidebadformat();" | ||||||
|  |                             placeholder="Enter token..."> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="form-group"> | ||||||
|  |                         <div class="peers ai-c jc-sb fxw-nw"> | ||||||
|  |                             <div class="peer"> | ||||||
|  |                                 <!-- | ||||||
|  |                                 <div class="checkbox checkbox-circle checkbox-info peers ai-c"> | ||||||
|  |                                     <input type="checkbox" id="inputCall1" name="inputCheckboxesCall" class="peer"> | ||||||
|  |                                     <label for="inputCall1" class="peers peer-greed js-sb ai-c"> | ||||||
|  |                                         <span class="peer peer-greed">Remember Me</span></label></div> | ||||||
|  |                                     --> | ||||||
|  |                             </div> | ||||||
|  |                             <div class="peer"><button onclick="return submitbtn();" | ||||||
|  |                                     class="btn btn-primary">Login</button></div> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </form> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |     <script> | ||||||
|  |         function isUUID(uuid) { | ||||||
|  |             let s = "" + uuid; | ||||||
|  |             s = s.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'); | ||||||
|  |             if (s === null) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         function hidealert() { | ||||||
|  |             var confalert = document.getElementById('confalert'); | ||||||
|  |             confalert.style.display = "none"; | ||||||
|  |             var badformat = document.getElementById('badformat'); | ||||||
|  |             badformat.style.display = "none"; | ||||||
|  |         } | ||||||
|  |         function hidebadformat() { | ||||||
|  |             var badformat = document.getElementById('badformat'); | ||||||
|  |             badformat.style.display = "none"; | ||||||
|  |         } | ||||||
|  |         function changemethod(obj) { | ||||||
|  |             var element = document.getElementById('loginvalue'); | ||||||
|  |             var smsform = document.getElementById('sms-conf-form'); | ||||||
|  |             var emailform = document.getElementById('email-conf-form'); | ||||||
|  |             var smschk = document.getElementById('smschk'); | ||||||
|  |             var emailchk = document.getElementById('emailchk'); | ||||||
|  |             var confalert = document.getElementById('confalert'); | ||||||
|  |             var badformat = document.getElementById('badformat'); | ||||||
|  |  | ||||||
|  |             if (!element || !smsform || !emailform || | ||||||
|  |                 !smschk || !emailchk || !confalert || !badformat) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             var value = obj.value; | ||||||
|  |             if (!value) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             badformat.style.display = "none"; | ||||||
|  |             smschk.checked = false; | ||||||
|  |             emailchk.checked = false; | ||||||
|  |             value = value.toLowerCase(); | ||||||
|  |             var key = element.placeholder = "Enter " + value + "..."; | ||||||
|  |             confalert.style.display = "none"; | ||||||
|  |             if (value == "email") { | ||||||
|  |                 smsform.style.display = "none"; | ||||||
|  |                 emailform.style.display = "block"; | ||||||
|  |             } else if (value == "phone") { | ||||||
|  |                 smsform.style.display = "block"; | ||||||
|  |                 emailform.style.display = "none"; | ||||||
|  |             } else { | ||||||
|  |                 smsform.style.display = "none"; | ||||||
|  |                 emailform.style.display = "none"; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         function submitbtn() { | ||||||
|  |             var element = document.getElementById('loginvalue') | ||||||
|  |             var smschk = document.getElementById('smschk'); | ||||||
|  |             var emailchk = document.getElementById('emailchk'); | ||||||
|  |             var confalert = document.getElementById('confalert'); | ||||||
|  |             var keymethod = document.getElementById('keymethod'); | ||||||
|  |             var badformat = document.getElementById('badformat'); | ||||||
|  |  | ||||||
|  |             if (!element || !smschk || !emailchk || !confalert || !keymethod) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             var key = element.value; | ||||||
|  |             if (!key) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             var kkk = keymethod.options[keymethod.selectedIndex].value; | ||||||
|  |             if ((kkk == "Email" && emailchk.checked == false) || | ||||||
|  |                 (kkk == "Phone" && smschk.checked == false)) { | ||||||
|  |                 confalert.style.display = "block"; | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |             if (kkk == "Token" && isUUID(key) == true) { | ||||||
|  |                 var xhr = new XMLHttpRequest(); | ||||||
|  |                 xhr.open('GET', "/v1/xtoken/" + key); | ||||||
|  |                 xhr.onload = function () { | ||||||
|  |                     if (xhr.status === 200) { | ||||||
|  |                         var data = JSON.parse(xhr.responseText); | ||||||
|  |                         if (data && data.status && data.status == "ok") { | ||||||
|  |                             window.localStorage.setItem('xtoken', key); | ||||||
|  |                             window.localStorage.setItem('type', data.type); | ||||||
|  |                             if (data.data) { | ||||||
|  |                                 document.location = "/site/display-data.html"; | ||||||
|  |                             } else { | ||||||
|  |                                 document.location = "/site/admin-events.html"; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  |                 xhr.send(); | ||||||
|  |             } else if (kkk == "Email" && key.indexOf('@') > 0) { | ||||||
|  |                 var params = 'brief=send-email-mailgun-on-login'; | ||||||
|  |                 window.localStorage.setItem('login', key); | ||||||
|  |                 var xhr0 = new XMLHttpRequest(); | ||||||
|  |                 // first save consent | ||||||
|  |                 xhr0.open('POST', "/v1/consent/email/" + encodeURI(key)); | ||||||
|  |                 xhr0.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); | ||||||
|  |                 xhr0.onload = function () { | ||||||
|  |                     if (xhr0.status === 200) { | ||||||
|  |                         var xhr = new XMLHttpRequest(); | ||||||
|  |                         xhr.open('GET', "/v1/login/email/" + encodeURI(key)); | ||||||
|  |                         xhr.onload = function () { | ||||||
|  |                             if (xhr.status === 200) { | ||||||
|  |                                 document.location = "/site/enter.html"; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         xhr.send(); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 xhr0.send(params); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |             } else if (kkk == "Phone") { | ||||||
|  |                 window.localStorage.setItem('login', key); | ||||||
|  |                 var params = 'brief=send-sms-twilio-on-login'; | ||||||
|  |                 var xhr0 = new XMLHttpRequest(); | ||||||
|  |                 xhr0.open('POST', "/v1/consent/phone/" + encodeURI(key)); | ||||||
|  |                 xhr0.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); | ||||||
|  |                 xhr0.onload = function () { | ||||||
|  |                     if (xhr0.status === 200) { | ||||||
|  |                         var xhr = new XMLHttpRequest(); | ||||||
|  |                         xhr.open('GET', "/v1/login/phone/" + encodeURI(key)); | ||||||
|  |                         xhr.onload = function () { | ||||||
|  |                             if (xhr.status === 200) { | ||||||
|  |                                 document.location = "/site/enter.html"; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         xhr.send(); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 xhr0.send(params); | ||||||
|  |             } else { | ||||||
|  |                 badformat.style.display = "block"; | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
							
								
								
									
										57
									
								
								ui/site/display-data.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								ui/site/display-data.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html> | ||||||
|  |  | ||||||
|  | <head> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||||
|  |     <title>Data Bunker Login</title> | ||||||
|  |     <link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'> | ||||||
|  |     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" | ||||||
|  |         integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" | ||||||
|  |         integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.5/dist/bootstrap-table.min.css"> | ||||||
|  |     <link rel="stylesheet" href="style.css"> | ||||||
|  |  | ||||||
|  |     <script src="https://code.jquery.com/jquery-3.3.1.min.js" | ||||||
|  |         integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" | ||||||
|  |         integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" | ||||||
|  |         crossorigin="anonymous"></script> | ||||||
|  |     <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" | ||||||
|  |         integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" | ||||||
|  |         crossorigin="anonymous"></script> | ||||||
|  |  | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js"></script> | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/languages/json.min.js"></script> | ||||||
|  |     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/a11y-dark.min.css"> | ||||||
|  | </head> | ||||||
|  |  | ||||||
|  | <body> | ||||||
|  |     <div class="container col-md-6 pY-120"> | ||||||
|  |         <div class="row col-12 col-md-12 "> | ||||||
|  |             <div class="bigblock"> | ||||||
|  |                 <h4>Record display</h4> | ||||||
|  |                 <p id="msg">text</p> | ||||||
|  |                 <pre id="data"></pre> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |     <script> | ||||||
|  |         var xtoken = window.localStorage.getItem('xtoken'); | ||||||
|  |         var ttype = window.localStorage.getItem('type'); | ||||||
|  |         $('#msg').val("Display: " + ttype + " " + xtoken); | ||||||
|  |         $.get("/v1/xtoken/" + xtoken, function (data) { | ||||||
|  |             if (data.status == "ok") { | ||||||
|  |                 $('#msg').text("Access xtoken value: "+xtoken) | ||||||
|  |                 var d = JSON.stringify(data.data, null, 4); | ||||||
|  |                 $('#data').append('<code class="json">' + d + '</code>'); | ||||||
|  |                 document.querySelectorAll('pre code').forEach((block) => { | ||||||
|  |                     hljs.highlightBlock(block); | ||||||
|  |                 }); | ||||||
|  |             } else { | ||||||
|  |                 alert("error"); | ||||||
|  |             } | ||||||
|  |         }, "json"); | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
							
								
								
									
										82
									
								
								ui/site/enter.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								ui/site/enter.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html> | ||||||
|  |  | ||||||
|  | <head> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||||
|  |     <title>Data Bunker Login</title> | ||||||
|  |     <link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'> | ||||||
|  |     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" | ||||||
|  |         integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" | ||||||
|  |         integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.5/dist/bootstrap-table.min.css"> | ||||||
|  |     <link rel="stylesheet" href="style.css"> | ||||||
|  |  | ||||||
|  |     <script src="https://code.jquery.com/jquery-3.3.1.min.js" | ||||||
|  |         integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" | ||||||
|  |         integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" | ||||||
|  |         crossorigin="anonymous"></script> | ||||||
|  |     <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" | ||||||
|  |         integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" | ||||||
|  |         crossorigin="anonymous"></script> | ||||||
|  | </head> | ||||||
|  |  | ||||||
|  | <body> | ||||||
|  |     <div class="container col-md-6 pY-120"> | ||||||
|  |         <div class="row col-12 col-md-12 "> | ||||||
|  |             <div class="bigblock"> | ||||||
|  |                 <h4>Verification step</h4> | ||||||
|  |                 <p>Enter the code you received by email or SMS</p> | ||||||
|  |                 <form id="loginform"> | ||||||
|  |                     <div class="form-group"><label class="text-normal text-dark">Enter code</label> | ||||||
|  |                         <input id="codevalue" type="login" class="form-control" placeholder="Enter..."></div> | ||||||
|  |                     <div class="form-group"> | ||||||
|  |                         <div class="peers ai-c jc-sb fxw-nw"> | ||||||
|  |                             <div class="peer"> | ||||||
|  |                                 <!-- | ||||||
|  |                                 <div class="checkbox checkbox-circle checkbox-info peers ai-c"> | ||||||
|  |                                     <input type="checkbox" id="inputCall1" name="inputCheckboxesCall" class="peer"> | ||||||
|  |                                     <label for="inputCall1" class="peers peer-greed js-sb ai-c"> | ||||||
|  |                                         <span class="peer peer-greed">Remember Me</span></label></div> | ||||||
|  |                                     --> | ||||||
|  |                             </div> | ||||||
|  |                             <div class="peer"><button id="submitbtn" class="btn btn-primary">Enter</button></div> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </form> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |     <script> | ||||||
|  |         $('#submitbtn').on('click', function (e) { | ||||||
|  |             e.preventDefault(); | ||||||
|  |             var code = $('#codevalue').val(); | ||||||
|  |             var login = window.localStorage.getItem('login') | ||||||
|  |             if (login.indexOf('@') > 0) { | ||||||
|  |                 $.get("/v1/enter/email/" + encodeURI(login) + "/" + encodeURI(code), function (data) { | ||||||
|  |                     window.localStorage.setItem('login', ""); | ||||||
|  |                     if (data.xtoken) { | ||||||
|  |                         window.localStorage.setItem('xtoken', data.xtoken); | ||||||
|  |                         window.localStorage.setItem('token', data.token); | ||||||
|  |                         document.location = "user-profile.html"; | ||||||
|  |                     } else { | ||||||
|  |                         document.location = "/"; | ||||||
|  |                     } | ||||||
|  |                 }, "json"); | ||||||
|  |             } else { | ||||||
|  |                 $.get("/v1/enter/phone/" + encodeURI(login) + "/" + encodeURI(code), function (data) { | ||||||
|  |                     window.localStorage.setItem('login', ""); | ||||||
|  |                     if (data.xtoken) { | ||||||
|  |                         window.localStorage.setItem('xtoken', data.xtoken); | ||||||
|  |                         window.localStorage.setItem('token', data.token); | ||||||
|  |                         document.location = "user-profile.html"; | ||||||
|  |                     } else { | ||||||
|  |                         document.location = "/"; | ||||||
|  |                     } | ||||||
|  |                 }, "json"); | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
							
								
								
									
										8
									
								
								ui/site/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ui/site/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  | <title></title> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |     <script>document.location = "../"; </script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										6
									
								
								ui/site/jsoneditor.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								ui/site/jsoneditor.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										46
									
								
								ui/site/jsoneditor.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								ui/site/jsoneditor.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										16
									
								
								ui/site/site.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								ui/site/site.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | function bunker_logout() | ||||||
|  | { | ||||||
|  |     localStorage.removeItem("xtoken"); | ||||||
|  |     localStorage.removeItem("xtoken"); | ||||||
|  |     localStorage.removeItem("login"); | ||||||
|  |     document.location = "/"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function dateFormat(value, row, index) { | ||||||
|  |     //return moment(value).format('DD/MM/YYYY'); | ||||||
|  |     var d = new Date(parseInt(value) * 1000); | ||||||
|  |     let f_date = d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate() + | ||||||
|  |       " " + d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds() | ||||||
|  |     //return d.toUTCString(); | ||||||
|  |     return f_date; | ||||||
|  | } | ||||||
							
								
								
									
										113
									
								
								ui/site/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								ui/site/style.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | html { | ||||||
|  |     height: 100%; | ||||||
|  |     -webkit-font-smoothing: antialiased; | ||||||
|  | } | ||||||
|  | body, h1, h2, h3, h4, h5, h6 { | ||||||
|  |     font-family: Roboto,-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Arial,sans-serif; | ||||||
|  |     line-height: 1.5; | ||||||
|  | } | ||||||
|  | body { | ||||||
|  |   font-size: 14px; | ||||||
|  |   color: #72777a; | ||||||
|  |   letter-spacing: 0.2px; | ||||||
|  |   font-weight: 400; | ||||||
|  |   padding:0; | ||||||
|  |   margin:0; | ||||||
|  |   height: 100%; | ||||||
|  |   background-color: #f9fafb !important; | ||||||
|  | } | ||||||
|  | .container { | ||||||
|  |     height: 100%; | ||||||
|  |     width: 100%; | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  | .bigblock { | ||||||
|  |   background-color: #fff !important; | ||||||
|  |   border-radius: 3px !important; | ||||||
|  |   border: 1px solid rgba(0, 0, 0, 0.0625) !important; | ||||||
|  |   padding: 20px !important; | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
|  | div.bigblock table { | ||||||
|  |     color: #72777a; | ||||||
|  | } | ||||||
|  | h4 { | ||||||
|  |     color: #313435!important; | ||||||
|  |     margin-bottom: 20px!important; | ||||||
|  |     letter-spacing: .5px; | ||||||
|  |     font-size: 1.3125rem; | ||||||
|  | } | ||||||
|  | .peer { | ||||||
|  |     display: block; | ||||||
|  |     height: auto; | ||||||
|  |     -webkit-box-flex: 0; | ||||||
|  |     -ms-flex: 0 0 auto; | ||||||
|  |     flex: 0 0 auto; | ||||||
|  | } | ||||||
|  | .peers { | ||||||
|  |     -webkit-box-sizing: border-box; | ||||||
|  |     box-sizing: border-box; | ||||||
|  |     display: -webkit-box!important; | ||||||
|  |     display: -ms-flexbox!important; | ||||||
|  |     display: flex!important; | ||||||
|  |     -webkit-box-align: start; | ||||||
|  |     -ms-flex-align: start; | ||||||
|  |     align-items: flex-start; | ||||||
|  |     -webkit-box-pack: start; | ||||||
|  |     -ms-flex-pack: start; | ||||||
|  |     justify-content: flex-start; | ||||||
|  |     -webkit-box-orient: horizontal; | ||||||
|  |     -webkit-box-direction: normal; | ||||||
|  |     -ms-flex-flow: row wrap; | ||||||
|  |     flex-flow: row wrap; | ||||||
|  |     height: auto; | ||||||
|  |     max-width: 100%; | ||||||
|  |     margin: 0; | ||||||
|  |     padding: 0; | ||||||
|  | } | ||||||
|  | .jc-sb { | ||||||
|  |     -webkit-box-pack: justify; | ||||||
|  |     -ms-flex-pack: justify; | ||||||
|  |     justify-content: space-between; | ||||||
|  | } | ||||||
|  | .ai-c { | ||||||
|  |     -webkit-box-align: center; | ||||||
|  |     -ms-flex-align: center; | ||||||
|  |     align-items: center; | ||||||
|  | } | ||||||
|  | .fxw-nw { | ||||||
|  |     -ms-flex-wrap: nowrap; | ||||||
|  |     flex-wrap: nowrap; | ||||||
|  | } | ||||||
|  | .btn-primary { | ||||||
|  |     color: #fff; | ||||||
|  |     background-color: #2196f3; | ||||||
|  |     border-color: #2196f3; | ||||||
|  | } | ||||||
|  | .btn { | ||||||
|  |     display: inline-block; | ||||||
|  |     font-weight: 400; | ||||||
|  |     text-align: center; | ||||||
|  |     white-space: nowrap; | ||||||
|  |     vertical-align: middle; | ||||||
|  |     -webkit-user-select: none; | ||||||
|  |     -moz-user-select: none; | ||||||
|  |     -ms-user-select: none; | ||||||
|  |     user-select: none; | ||||||
|  |     border: 1px solid transparent; | ||||||
|  |     padding: 6px 12px; | ||||||
|  |     padding: .375rem .75rem; | ||||||
|  |     font-size: 14px; | ||||||
|  |     font-size: .875rem; | ||||||
|  |     line-height: 1.5; | ||||||
|  |     border-radius: .25rem; | ||||||
|  |     -webkit-transition: background-color .15s ease-in-out,border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out; | ||||||
|  |     transition: background-color .15s ease-in-out,border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out; | ||||||
|  |     -o-transition: background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; | ||||||
|  |     transition: background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; | ||||||
|  |     transition: background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out; | ||||||
|  | } | ||||||
|  | .pY-120 { | ||||||
|  |     padding-top: 120px!important; | ||||||
|  |     padding-bottom: 120px!important; | ||||||
|  | } | ||||||
							
								
								
									
										110
									
								
								ui/site/user-apps.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								ui/site/user-apps.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html> | ||||||
|  |  | ||||||
|  | <head> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||||
|  |     <title>Data Bunker - list of apps</title> | ||||||
|  |     <link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'> | ||||||
|  |     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" | ||||||
|  |         integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" | ||||||
|  |         integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous"> | ||||||
|  |      <link rel="stylesheet" href="style.css"> | ||||||
|  |  | ||||||
|  |     <script src="https://code.jquery.com/jquery-3.3.1.min.js" | ||||||
|  |         integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" | ||||||
|  |         integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" | ||||||
|  |         crossorigin="anonymous"></script> | ||||||
|  |     <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" | ||||||
|  |         integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" | ||||||
|  |         crossorigin="anonymous"></script> | ||||||
|  |  | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js"></script> | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/languages/json.min.js"></script> | ||||||
|  |     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/a11y-dark.min.css"> | ||||||
|  |     <script src="site.js"></script> | ||||||
|  | </head> | ||||||
|  |  | ||||||
|  | <body> | ||||||
|  |     <div class="container col-md-6"> | ||||||
|  |         <div class="row col-12 col-md-12 "> | ||||||
|  |             <div style="width:100%;"> | ||||||
|  |                 <nav class="navbar navbar-expand-lg navbar-light bg-light"> | ||||||
|  |                     <a class="navbar-brand" href="#">Menu</a> | ||||||
|  |                     <button class="navbar-toggler" type="button" data-toggle="collapse" | ||||||
|  |                         data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" | ||||||
|  |                         aria-label="Toggle navigation"> | ||||||
|  |                         <span class="navbar-toggler-icon"></span> | ||||||
|  |                     </button> | ||||||
|  |                     <div class="collapse navbar-collapse" id="navbarNavAltMarkup"> | ||||||
|  |                         <div class="navbar-nav"> | ||||||
|  |                             <a class="nav-item nav-link" href="user-profile.html">Profile</a> | ||||||
|  |                             <a class="nav-item nav-link active" href="user-apps.html">Apps</a> | ||||||
|  |                             <a class="nav-item nav-link" href="user-data-processing.html">Confirmations</a> | ||||||
|  |                             <a class="nav-item nav-link" href="user-audit.html">Audit</a> | ||||||
|  |                             <a class="nav-item nav-link" href="javascript:bunker_logout();">Logout</a> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </nav> | ||||||
|  |             </div> | ||||||
|  |             <div class="bigblock"> | ||||||
|  |                 <h4>User apps data</h4> | ||||||
|  |                 <p id="msg">text</p> | ||||||
|  |                 <pre id="data"></pre> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |     <script> | ||||||
|  |         function fetchApp(token, xtoken, app) { | ||||||
|  |             var xhr0 = new XMLHttpRequest(); | ||||||
|  |             // first save consent | ||||||
|  |             xhr0.open('GET', '/v1/userapp/token/' + token + "/" + app); | ||||||
|  |             xhr0.setRequestHeader("X-Bunker-Token", xtoken) | ||||||
|  |             xhr0.setRequestHeader('Content-type', 'application/json'); | ||||||
|  |             xhr0.onload = function () { | ||||||
|  |                 if (xhr0.status === 200) { | ||||||
|  |                     var data = JSON.parse(xhr0.responseText); | ||||||
|  |                     if (data.status == "ok") { | ||||||
|  |                         var d = JSON.stringify(data.data, null, 4); | ||||||
|  |                         $('#data').append('<h4>'+app+'</h4><code class="json">' + d + '</code>'); | ||||||
|  |                         document.querySelectorAll('pre code').forEach((block) => { | ||||||
|  |                             hljs.highlightBlock(block); | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  |                 //} else if (xhr0.status > 400 && xhr0.status < 500) { | ||||||
|  |                 //    document.location = "/"; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             xhr0.send(); | ||||||
|  |  | ||||||
|  |         } | ||||||
|  |         var xtoken = window.localStorage.getItem('xtoken'); | ||||||
|  |         var token = window.localStorage.getItem('token'); | ||||||
|  |         var ttype = window.localStorage.getItem('type'); | ||||||
|  |         $('#msg').val("Display: " + ttype + " " + token); | ||||||
|  |  | ||||||
|  |         var xhr = new XMLHttpRequest(); | ||||||
|  |         xhr.open('GET', '/v1/userapp/token/' + token); | ||||||
|  |         xhr.setRequestHeader("X-Bunker-Token", xtoken) | ||||||
|  |         xhr.setRequestHeader('Content-type', 'application/json'); | ||||||
|  |         xhr.onload = function () { | ||||||
|  |             if (xhr.status === 200) { | ||||||
|  |                 var data = JSON.parse(xhr.responseText); | ||||||
|  |                 if (data.apps) { | ||||||
|  |                     var index; | ||||||
|  |                     for (index = 0; index < data.apps.length; ++index) { | ||||||
|  |                         var app = data.apps[index]; | ||||||
|  |                         console.log("app", app) | ||||||
|  |                         fetchApp(token, xtoken, app); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else if (xhr.status > 400 && xhr.status < 500) { | ||||||
|  |                 document.location = "/"; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         xhr.send() | ||||||
|  |  | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
							
								
								
									
										102
									
								
								ui/site/user-audit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								ui/site/user-audit.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html> | ||||||
|  |  | ||||||
|  | <head> | ||||||
|  |   <meta charset="utf-8"> | ||||||
|  |   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||||
|  |   <title>Data Bunker - audit events</title> | ||||||
|  |   <link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'> | ||||||
|  |   <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" | ||||||
|  |     integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> | ||||||
|  |   <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" | ||||||
|  |     integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous"> | ||||||
|  |   <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.5/dist/bootstrap-table.min.css"> | ||||||
|  |   <link rel="stylesheet" href="style.css"> | ||||||
|  |  | ||||||
|  |   <script src="https://code.jquery.com/jquery-3.3.1.min.js" | ||||||
|  |     integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> | ||||||
|  |   <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" | ||||||
|  |     integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" | ||||||
|  |     crossorigin="anonymous"></script> | ||||||
|  |   <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" | ||||||
|  |     integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" | ||||||
|  |     crossorigin="anonymous"></script> | ||||||
|  |   <script src="https://unpkg.com/bootstrap-table@1.15.5/dist/bootstrap-table.min.js"></script> | ||||||
|  |   <script src="https://unpkg.com/bootstrap-table@1.15.5/dist/bootstrap-table.min.js"></script> | ||||||
|  |  | ||||||
|  |   <script> | ||||||
|  |     $(function () { | ||||||
|  |       var xtoken = window.localStorage.getItem('xtoken'); | ||||||
|  |       var token = window.localStorage.getItem('token'); | ||||||
|  |       var ttype = window.localStorage.getItem('type'); | ||||||
|  |       $('#msg').text("Access xtoken value: " + xtoken + " user: " + token) | ||||||
|  |       //token = "faa006da-475e-45c6-a4a1-6586dce8b8d2"; | ||||||
|  |       $('#table').bootstrapTable({ | ||||||
|  |         /*data: mydata */ | ||||||
|  |         url: "http://localhost:3000/v1/audit/list/" + token, | ||||||
|  |         undefinedText: 'n/a', | ||||||
|  |         /* url: "data1.json", */ | ||||||
|  |         method: "GET", | ||||||
|  |         ajaxOptions: { | ||||||
|  |           headers: { "X-Bunker-Token": xtoken }, | ||||||
|  |           crossDomain: true | ||||||
|  |         }, | ||||||
|  |         showExtendedPagination: true, | ||||||
|  |         sidePagination: "server", | ||||||
|  |         pagination: true, | ||||||
|  |         search: false, | ||||||
|  |         classes: "table", | ||||||
|  |         onLoadError: function (status, res) { | ||||||
|  |           console.log(status); | ||||||
|  |           if (status > 400 && status < 500) { | ||||||
|  |             document.location = "/"; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   </script> | ||||||
|  |   <script src="site.js"></script> | ||||||
|  | </head> | ||||||
|  |  | ||||||
|  | <body> | ||||||
|  |   <div class="container"> | ||||||
|  |     <div class="row col-md-12"> | ||||||
|  |       <div style="width:100%;"> | ||||||
|  |         <nav class="navbar navbar-expand-lg navbar-light bg-light"> | ||||||
|  |           <a class="navbar-brand" href="#">Menu</a> | ||||||
|  |           <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" | ||||||
|  |             aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation"> | ||||||
|  |             <span class="navbar-toggler-icon"></span> | ||||||
|  |           </button> | ||||||
|  |           <div class="collapse navbar-collapse" id="navbarNavAltMarkup"> | ||||||
|  |             <div class="navbar-nav"> | ||||||
|  |               <a class="nav-item nav-link" href="user-profile.html">Profile <span class="sr-only">(current)</span></a> | ||||||
|  |               <a class="nav-item nav-link" href="user-apps.html">Apps</a> | ||||||
|  |               <a class="nav-item nav-link" href="user-data-processing.html">Confirmations</a> | ||||||
|  |               <a class="nav-item nav-link active" href="user-audit.html">Audit</a> | ||||||
|  |               <a class="nav-item nav-link" href="javascript:bunker_logout();">Logout</a> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </nav> | ||||||
|  |       </div> | ||||||
|  |       <div class="bigblock"> | ||||||
|  |         <h4>Audit table</h4> | ||||||
|  |         <p id="msg">text here, text, text</p> | ||||||
|  |         <table id="table" class="table"> | ||||||
|  |           <thead> | ||||||
|  |             <tr> | ||||||
|  |               <th scope="col" data-field="when" data-formatter="dateFormat">when</th> | ||||||
|  |               <th scope="col" data-field="who">Who</th> | ||||||
|  |               <th scope="col" data-field="app">App</th> | ||||||
|  |               <th scope="col" data-field="title">Title</th> | ||||||
|  |               <th scope="col" data-field="status">Status</th> | ||||||
|  |               <th scope="col" data-field="msg">Msg</th> | ||||||
|  |             </tr> | ||||||
|  |           </thead> | ||||||
|  |         </table> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </body> | ||||||
|  |  | ||||||
|  | </html> | ||||||
							
								
								
									
										166
									
								
								ui/site/user-data-processing.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								ui/site/user-data-processing.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html> | ||||||
|  |  | ||||||
|  | <head> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||||
|  |     <title>Data Bunker - List of data processing requests</title> | ||||||
|  |     <link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'> | ||||||
|  |     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" | ||||||
|  |         integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" | ||||||
|  |         integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="style.css"> | ||||||
|  |  | ||||||
|  |     <script src="https://code.jquery.com/jquery-3.3.1.min.js" | ||||||
|  |         integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" | ||||||
|  |         integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" | ||||||
|  |         crossorigin="anonymous"></script> | ||||||
|  |     <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" | ||||||
|  |         integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" | ||||||
|  |         crossorigin="anonymous"></script> | ||||||
|  |     <script src="site.js"></script> | ||||||
|  | </head> | ||||||
|  |  | ||||||
|  | <body> | ||||||
|  |     <div class="container"> | ||||||
|  |         <div class="row col-md-12"> | ||||||
|  |             <div style="width:100%;"> | ||||||
|  |                 <nav class="navbar navbar-expand-lg navbar-light bg-light"> | ||||||
|  |                     <a class="navbar-brand" href="#">Menu</a> | ||||||
|  |                     <button class="navbar-toggler" type="button" data-toggle="collapse" | ||||||
|  |                         data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" | ||||||
|  |                         aria-label="Toggle navigation"> | ||||||
|  |                         <span class="navbar-toggler-icon"></span> | ||||||
|  |                     </button> | ||||||
|  |                     <div class="collapse navbar-collapse" id="navbarNavAltMarkup"> | ||||||
|  |                         <div class="navbar-nav"> | ||||||
|  |                             <a class="nav-item nav-link" href="user-profile.html">Profile <span | ||||||
|  |                                     class="sr-only">(current)</span></a> | ||||||
|  |                             <a class="nav-item nav-link" href="user-apps.html">Apps</a> | ||||||
|  |                             <a class="nav-item nav-link active" href="user-data-processing.html">Confirmations</a> | ||||||
|  |                             <a class="nav-item nav-link" href="user-audit.html">Audit</a> | ||||||
|  |                             <a class="nav-item nav-link" href="javascript:bunker_logout();">Logout</a> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </nav> | ||||||
|  |             </div> | ||||||
|  |             <div class="bigblock"> | ||||||
|  |                 <h4>Data processing requests</h4> | ||||||
|  |                 <p id="msg">List of all user consents. You can allow and cancel your consent request here.</p> | ||||||
|  |                 <div id="data"></div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |     <script> | ||||||
|  |         var xtoken = window.localStorage.getItem('xtoken'); | ||||||
|  |         var token = window.localStorage.getItem('token'); | ||||||
|  |  | ||||||
|  |         var xhr = new XMLHttpRequest(); | ||||||
|  |         xhr.open('GET', "http://localhost:3000/v1/consent/token/" + token); | ||||||
|  |         xhr.setRequestHeader("X-Bunker-Token", xtoken) | ||||||
|  |         xhr.setRequestHeader('Content-type', 'application/json'); | ||||||
|  |         xhr.onload = function () { | ||||||
|  |             if (xhr.status === 200) { | ||||||
|  |                 var data = JSON.parse(xhr.responseText); | ||||||
|  |                 if (data.status == "ok") { | ||||||
|  |                     //$('#msg').text("Access xtoken value: " + xtoken + " user: " + token) | ||||||
|  |                     console.log(data) | ||||||
|  |                     var index; | ||||||
|  |                     for (index = 0; index < data.rows.length; ++index) { | ||||||
|  |                         var row = data.rows[index]; | ||||||
|  |                         //console.log("row", row) | ||||||
|  |                         $('#data').append(prepareRow(row)); | ||||||
|  |                         //fetchApp(token, xtoken, app); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else if (xhr.status > 400 && xhr.status < 500) { | ||||||
|  |                 document.location = "/"; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         xhr.send(); | ||||||
|  |         function cancelThis(brief) { | ||||||
|  |             var heading = "Confirm action"; | ||||||
|  |             var question = "Are you sure?"; | ||||||
|  |             var cancelButtonTxt = "Close popup"; | ||||||
|  |             var okButtonTxt = "Cancel!"; | ||||||
|  |             var confirmModal = | ||||||
|  |                 $('<div class="modal fade" role="dialog"><div class="modal-dialog" role="document"><div class="modal-content">' + | ||||||
|  |                     '<div class="modal-header">' + | ||||||
|  |                     '<h5 class="modal-title">' + heading + '</h5>' + | ||||||
|  |                     '<button type="button" class="close" data-dismiss="modal" aria-label="Close">' + | ||||||
|  |                         '<span aria-hidden="true">×</span></button>' + | ||||||
|  |                     '</div>' + | ||||||
|  |                     '<div class="modal-body">' + | ||||||
|  |                     '<p>' + question + '</p>' + | ||||||
|  |                     '</div>' + | ||||||
|  |                     '<div class="modal-footer">' + | ||||||
|  |                     '<a href="#" class="btn" data-dismiss="modal">' + | ||||||
|  |                     cancelButtonTxt + | ||||||
|  |                     '</a>' + | ||||||
|  |                     '<a href="#" id="okButton" class="btn btn-primary">' + | ||||||
|  |                     okButtonTxt + | ||||||
|  |                     '</a>' + | ||||||
|  |                     '</div>' + | ||||||
|  |                     '</div></div></div>'); | ||||||
|  |  | ||||||
|  |             confirmModal.find('#okButton').click(function (event) { | ||||||
|  |                 //callback(); | ||||||
|  |                 cancelThisDo(brief); | ||||||
|  |                 confirmModal.modal('hide'); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             confirmModal.modal('show'); | ||||||
|  |         } | ||||||
|  |         function cancelThisDo(brief) { | ||||||
|  |             var params = 'brief=' + brief; | ||||||
|  |             var xhr = new XMLHttpRequest(); | ||||||
|  |             xhr.open('DELETE', "/v1/consent/token/" + token); | ||||||
|  |             xhr.setRequestHeader("X-Bunker-Token", xtoken) | ||||||
|  |             xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); | ||||||
|  |             xhr.onload = function () { | ||||||
|  |                 if (xhr.status === 200) { | ||||||
|  |                     document.location.reload(); | ||||||
|  |                 } else if (xhr.status > 400 && xhr.status < 500) { | ||||||
|  |                     document.location = "/"; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             xhr.send(params); | ||||||
|  |         } | ||||||
|  |         function acceptThis(brief) { | ||||||
|  |             var params = 'brief=' + brief; | ||||||
|  |             var xhr = new XMLHttpRequest(); | ||||||
|  |             xhr.open('POST', "/v1/consent/token/" + token); | ||||||
|  |             xhr.setRequestHeader("X-Bunker-Token", xtoken) | ||||||
|  |             xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); | ||||||
|  |             xhr.onload = function () { | ||||||
|  |                 if (xhr.status === 200) { | ||||||
|  |                     document.location.reload(); | ||||||
|  |                 } else if (xhr.status > 400 && xhr.status < 500) { | ||||||
|  |                     document.location = "/"; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             xhr.send(params); | ||||||
|  |         } | ||||||
|  |         function prepareRow(row) { | ||||||
|  |             var msg = row.brief; | ||||||
|  |             if (row.message != "") { | ||||||
|  |                 msg = row.message; | ||||||
|  |             } | ||||||
|  |             var start = '<div class="d-flex flex-row bd-highlight mb-4">'; | ||||||
|  |             var d = '<div class="p-2 bd-highlight">When: ' + dateFormat(row.when) + '</div>' | ||||||
|  |             var identity = '<div class="p-2 flex-fill bd-highlight">Identity: ' + row.who + ' (' + row.type + ')<br/>' + msg + '</div>' | ||||||
|  |             var cancel = "<a href=\"javascript:cancelThis('" + row.brief + "');\">cancel</a>"; | ||||||
|  |             var accept = "<a href=\"javascript:acceptThis('" + row.brief + "');\">accept</a>"; | ||||||
|  |             var op = cancel; | ||||||
|  |             if (row.status == 'cancel') { | ||||||
|  |                 op = accept; | ||||||
|  |             } | ||||||
|  |             var status = '<div class="p-2 bd-highlight">Current status: ' + row.status + '</br><center>' + op + '</center></div>' | ||||||
|  |             return start + d + identity + status + '</div>'; | ||||||
|  |         } | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
|  |  | ||||||
|  | </html> | ||||||
							
								
								
									
										146
									
								
								ui/site/user-profile.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								ui/site/user-profile.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html> | ||||||
|  |  | ||||||
|  | <head> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||||
|  |     <title>Data Bunker - user profile</title> | ||||||
|  |     <link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'> | ||||||
|  |     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" | ||||||
|  |         integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" | ||||||
|  |         integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.5/dist/bootstrap-table.min.css"> | ||||||
|  |     <link rel="stylesheet" href="style.css"> | ||||||
|  |  | ||||||
|  |     <script src="https://code.jquery.com/jquery-3.3.1.min.js" | ||||||
|  |         integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" | ||||||
|  |         integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" | ||||||
|  |         crossorigin="anonymous"></script> | ||||||
|  |     <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" | ||||||
|  |         integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" | ||||||
|  |         crossorigin="anonymous"></script> | ||||||
|  |  | ||||||
|  |     <link href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/7.0.4/jsoneditor.min.css" rel="stylesheet" | ||||||
|  |         type="text/css"> | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/7.0.4/jsoneditor.min.js"></script> | ||||||
|  |     <script src="site.js"></script> | ||||||
|  | </head> | ||||||
|  |  | ||||||
|  | <body> | ||||||
|  |     <div class="container col-md-6"> | ||||||
|  |         <div class="row col-12 col-md-12 "> | ||||||
|  |             <div style="width:100%;"> | ||||||
|  |                 <nav class="navbar navbar-expand-lg navbar-light bg-light"> | ||||||
|  |                     <a class="navbar-brand" href="#">Menu</a> | ||||||
|  |                     <button class="navbar-toggler" type="button" data-toggle="collapse" | ||||||
|  |                         data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" | ||||||
|  |                         aria-label="Toggle navigation"> | ||||||
|  |                         <span class="navbar-toggler-icon"></span> | ||||||
|  |                     </button> | ||||||
|  |                     <div class="collapse navbar-collapse" id="navbarNavAltMarkup"> | ||||||
|  |                         <div class="navbar-nav"> | ||||||
|  |                             <a class="nav-item nav-link active" href="#">Profile <span | ||||||
|  |                                     class="sr-only">(current)</span></a> | ||||||
|  |                             <a class="nav-item nav-link" href="user-apps.html">Apps</a> | ||||||
|  |                             <a class="nav-item nav-link" href="user-data-processing.html">Confirmations</a> | ||||||
|  |                             <a class="nav-item nav-link" href="user-audit.html">Audit</a> | ||||||
|  |                             <a class="nav-item nav-link" href="javascript:bunker_logout();">Logout</a> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </nav> | ||||||
|  |             </div> | ||||||
|  |             <div class="bigblock"> | ||||||
|  |                 <h4>User record profile</h4> | ||||||
|  |                 <p id="msg">text</p> | ||||||
|  |                 <div id="jsoneditor" style="width: 400px; height: 400px;"></div> | ||||||
|  |                 <pre id="data"></pre> | ||||||
|  |  | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <div class="peers ai-c jc-sb fxw-nw"> | ||||||
|  |                         <div class="peer"> | ||||||
|  |                             <!-- | ||||||
|  |                             <div class="checkbox checkbox-circle checkbox-info peers ai-c"> | ||||||
|  |                                 <input type="checkbox" id="inputCall1" name="inputCheckboxesCall" class="peer"> | ||||||
|  |                                 <label for="inputCall1" class="peers peer-greed js-sb ai-c"> | ||||||
|  |                                     <span class="peer peer-greed">Remember Me</span></label></div> | ||||||
|  |                                 --> | ||||||
|  |                         </div> | ||||||
|  |                         <div class="peer"><button onclick="return savebtn();" class="btn btn-primary">Save</button> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |     <script> | ||||||
|  |         var xtoken = window.localStorage.getItem('xtoken'); | ||||||
|  |         var token = window.localStorage.getItem('token'); | ||||||
|  |         var ttype = window.localStorage.getItem('type'); | ||||||
|  |         var pKeys = ["password", "pwd", "pass", "parol", "passwd"]; | ||||||
|  |         var editor; | ||||||
|  |         function savebtn() { | ||||||
|  |             const updatedJson = editor.get() | ||||||
|  |             var params = JSON.stringify(updatedJson); | ||||||
|  |             //alert(d); | ||||||
|  |             var xhr0 = new XMLHttpRequest(); | ||||||
|  |             // first save consent | ||||||
|  |             xhr0.open('PUT', "/v1/user/token/" + token); | ||||||
|  |             xhr0.setRequestHeader("X-Bunker-Token", xtoken) | ||||||
|  |             xhr0.setRequestHeader('Content-type', 'application/json'); | ||||||
|  |             xhr0.onload = function () { | ||||||
|  |                 //if (xhr0.status === 200) { | ||||||
|  |                 //    // | ||||||
|  |                 //} else  | ||||||
|  |                 if (xhr0.status > 400 && xhr0.status < 500) { | ||||||
|  |                     document.location = "/"; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             xhr0.send(params); | ||||||
|  |         } | ||||||
|  |         $('#msg').val("Display: " + ttype + " " + token); | ||||||
|  |         /* | ||||||
|  |         $.get("/v1/token/" + token, function (data) { | ||||||
|  |         }, "json"); | ||||||
|  |         */ | ||||||
|  |         var xhr = new XMLHttpRequest(); | ||||||
|  |         xhr.open('GET', '/v1/user/token/' + token); | ||||||
|  |         xhr.setRequestHeader("X-Bunker-Token", xtoken) | ||||||
|  |         xhr.setRequestHeader('Content-type', 'application/json'); | ||||||
|  |         xhr.onload = function () { | ||||||
|  |             if (xhr.status === 200) { | ||||||
|  |                 var data = JSON.parse(xhr.responseText); | ||||||
|  |                 if (data.status == "ok") { | ||||||
|  |                     $('#msg').text("Access xtoken value: " + xtoken + " user: " + token) | ||||||
|  |                     // create the editor | ||||||
|  |                     const container = document.getElementById("jsoneditor") | ||||||
|  |                     const options = { "mode": "form" } | ||||||
|  |                     editor = new JSONEditor(container, options) | ||||||
|  |                     // remove password field from displaying to user | ||||||
|  |                     for (var key in data.data) { | ||||||
|  |                         var k0 = key.toLowerCase(); | ||||||
|  |                         if (pKeys.includes(k0)) { | ||||||
|  |                             delete data.data[key]; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     editor.set(data.data) | ||||||
|  |  | ||||||
|  |                     // get json | ||||||
|  |                     const updatedJson = editor.get() | ||||||
|  |                     /* | ||||||
|  |                     var d = JSON.stringify(data.data, null, 4); | ||||||
|  |                     $('#data').append('<code class="json">' + d + '</code>'); | ||||||
|  |                     document.querySelectorAll('pre code').forEach((block) => { | ||||||
|  |                         hljs.highlightBlock(block); | ||||||
|  |                     }); | ||||||
|  |                     */ | ||||||
|  |                 } | ||||||
|  |             } else if (xhr.status > 400 && xhr.status < 500) { | ||||||
|  |                 document.location = "/"; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         xhr.send(); | ||||||
|  |     </script> | ||||||
|  |  | ||||||
|  | </body> | ||||||
		Reference in New Issue
	
	Block a user
	 stremovsky
					stremovsky