mirror of
https://github.com/optim-enterprises-bv/databunker.git
synced 2025-10-29 09:02:22 +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