initial commit
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/data
|
||||||
|
node_modules
|
||||||
|
docker-compose.*.yml
|
||||||
87
.github/CLA/ENTITY.md
vendored
Normal file
87
.github/CLA/ENTITY.md
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Contributor Agreement
|
||||||
|
|
||||||
|
## Entity Contributor Exclusive License Agreement
|
||||||
|
|
||||||
|
## (including the Traditional Patent License OPTION)
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to the owners of this code repository ("We" or "Us").
|
||||||
|
|
||||||
|
The purpose of this contributor agreement ("Agreement") is to clarify and document the rights granted by contributors to Us. To make this document effective, please check the box when submitting a Pull Request or an Issue, where applicable.
|
||||||
|
|
||||||
|
### How to use this Contributor Agreement
|
||||||
|
|
||||||
|
If You are an employee and have created the Contribution as part of your employment, You need to have Your employer approve this Agreement or sign the Entity version of this document. If You do not own the Copyright in the entire work of authorship, any other author of the Contribution should also sign this.
|
||||||
|
|
||||||
|
### 1\. Definitions
|
||||||
|
|
||||||
|
**"You"** means the individual Copyright owner who Submits a Contribution to Us.
|
||||||
|
|
||||||
|
**"Legal Entity"** means an entity that is not a natural person.
|
||||||
|
|
||||||
|
**"Affiliate"** means any other Legal Entity that controls, is controlled by, or under common control with that Legal Entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such Legal Entity, whether by contract or otherwise, (ii) ownership of fifty percent (50%) or more of the outstanding shares or securities that vote to elect the management or other persons who direct such Legal Entity or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
**"Contribution"** means any original work of authorship, including any original modifications or additions to an existing work of authorship, Submitted by You to Us, in which You own the Copyright.
|
||||||
|
|
||||||
|
**"Copyright"** means all rights protecting works of authorship, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence.
|
||||||
|
|
||||||
|
**"Material"** means the software or documentation made available by Us to third parties. When this Agreement covers more than one software project, the Material means the software or documentation to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material.
|
||||||
|
|
||||||
|
**"Submit"** means any act by which a Contribution is transferred to Us by You by means of tangible or intangible media, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us, but excluding any transfer that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
|
||||||
|
|
||||||
|
**"Documentation"** means any non-software portion of a Contribution.
|
||||||
|
|
||||||
|
### 2\. License grant
|
||||||
|
|
||||||
|
#### 2.1 Copyright license to Us
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this Agreement, You hereby grant to Us a worldwide, royalty-free, Exclusive, perpetual and irrevocable (except as stated in Section 8.2) license, with the right to transfer an unlimited number of non-exclusive licenses or to grant sublicenses to third parties, under the Copyright covering the Contribution to use the Contribution by all means, including, but not limited to:
|
||||||
|
|
||||||
|
* publish the Contribution,
|
||||||
|
* modify the Contribution,
|
||||||
|
* prepare derivative works based upon or containing the Contribution and/or to combine the Contribution with other Materials,
|
||||||
|
* reproduce the Contribution in original or modified form,
|
||||||
|
* distribute, to make the Contribution available to the public, display and publicly perform the Contribution in original or modified form.
|
||||||
|
|
||||||
|
#### 2.2 Moral rights
|
||||||
|
|
||||||
|
Moral Rights remain unaffected to the extent they are recognized and not waivable by applicable law. Notwithstanding, You may add your name to the attribution mechanism customary used in the Materials you Contribute to, such as the header of the source code files of Your Contribution, and We will respect this attribution when using Your Contribution.
|
||||||
|
|
||||||
|
### 3\. Patents
|
||||||
|
|
||||||
|
#### 3.1 Patent license
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this Agreement You hereby grant to Us and to recipients of Materials distributed by Us a worldwide, royalty-free, non-exclusive, perpetual and irrevocable (except as stated in Section 3.2) patent license, with the right to transfer an unlimited number of non-exclusive licenses or to grant sublicenses to third parties, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with any Material (and portions of such combination). This license applies to all patents owned or controlled by You, whether already acquired or hereafter acquired, that would be infringed by making, having made, using, selling, offering for sale, importing or otherwise transferring of Your Contribution(s) alone or by combination of Your Contribution(s) with any Material.
|
||||||
|
|
||||||
|
#### 3.2 Revocation of patent license
|
||||||
|
|
||||||
|
You reserve the right to revoke the patent license stated in section 3.1 if We make any infringement claim that is targeted at your Contribution and not asserted for a Defensive Purpose. An assertion of claims of the Patents shall be considered for a "Defensive Purpose" if the claims are asserted against an entity that has filed, maintained, threatened, or voluntarily participated in a patent infringement lawsuit against Us or any of Our licensees.
|
||||||
|
|
||||||
|
### 4. Disclaimer
|
||||||
|
|
||||||
|
THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US AND BY US TO YOU. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION AND EXTENT TO THE MINIMUM PERIOD AND EXTENT PERMITTED BY LAW.
|
||||||
|
|
||||||
|
### 5. Consequential damage waiver
|
||||||
|
|
||||||
|
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU OR WE BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED.
|
||||||
|
|
||||||
|
### 6. Approximation of disclaimer and damage waiver
|
||||||
|
|
||||||
|
IF THE DISCLAIMER AND DAMAGE WAIVER MENTIONED IN SECTION 4. AND SECTION 5. CANNOT BE GIVEN LEGAL EFFECT UNDER APPLICABLE LOCAL LAW, REVIEWING COURTS SHALL APPLY LOCAL LAW THAT MOST CLOSELY APPROXIMATES AN ABSOLUTE WAIVER OF ALL CIVIL OR CONTRACTUAL LIABILITY IN CONNECTION WITH THE CONTRIBUTION.
|
||||||
|
|
||||||
|
### 7. Term
|
||||||
|
|
||||||
|
7.1 This Agreement shall come into effect upon Your acceptance of the terms and conditions.
|
||||||
|
|
||||||
|
7.3 In the event of a termination of this Agreement Sections 4, 5, 6, 7 and 8 shall survive such termination and shall remain in full force thereafter. For the avoidance of doubt, Free and Open Source Software (sub)licenses that have already been granted for Contributions at the date of the termination shall remain in full force after the termination of this Agreement.
|
||||||
|
|
||||||
|
### 8 Miscellaneous
|
||||||
|
|
||||||
|
8.1 This Agreement and all disputes, claims, actions, suits or other proceedings arising out of this agreement or relating in any way to it shall be governed by the laws of the United States of America excluding its private international law provisions.
|
||||||
|
|
||||||
|
8.2 This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings.
|
||||||
|
|
||||||
|
8.3 In case of Your death, this agreement shall continue with Your heirs. In case of more than one heir, all heirs must exercise their rights through a commonly authorized person.
|
||||||
|
|
||||||
|
8.4 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and that is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law.
|
||||||
|
|
||||||
|
8.5 You agree to notify Us of any facts or circumstances of which you become aware that would make this Agreement inaccurate in any respect.
|
||||||
87
.github/CLA/INDIVIDUAL.md
vendored
Normal file
87
.github/CLA/INDIVIDUAL.md
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Contributor Agreement
|
||||||
|
|
||||||
|
## Individual Contributor Exclusive License Agreement
|
||||||
|
|
||||||
|
## (including the Traditional Patent License OPTION)
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to the owners of this code repository ("We" or "Us").
|
||||||
|
|
||||||
|
The purpose of this contributor agreement ("Agreement") is to clarify and document the rights granted by contributors to Us. To make this document effective, please check the box when submitting a Pull Request or an Issue, where applicable.
|
||||||
|
|
||||||
|
### How to use this Contributor Agreement
|
||||||
|
|
||||||
|
If You are an employee and have created the Contribution as part of your employment, You need to have Your employer approve this Agreement or sign the Entity version of this document. If You do not own the Copyright in the entire work of authorship, any other author of the Contribution should also sign this.
|
||||||
|
|
||||||
|
### 1\. Definitions
|
||||||
|
|
||||||
|
**"You"** means the individual Copyright owner who Submits a Contribution to Us.
|
||||||
|
|
||||||
|
**"Legal Entity"** means an entity that is not a natural person.
|
||||||
|
|
||||||
|
**"Affiliate"** means any other Legal Entity that controls, is controlled by, or under common control with that Legal Entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such Legal Entity, whether by contract or otherwise, (ii) ownership of fifty percent (50%) or more of the outstanding shares or securities that vote to elect the management or other persons who direct such Legal Entity or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
**"Contribution"** means any original work of authorship, including any original modifications or additions to an existing work of authorship, Submitted by You to Us, in which You own the Copyright.
|
||||||
|
|
||||||
|
**"Copyright"** means all rights protecting works of authorship, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence.
|
||||||
|
|
||||||
|
**"Material"** means the software or documentation made available by Us to third parties. When this Agreement covers more than one software project, the Material means the software or documentation to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material.
|
||||||
|
|
||||||
|
**"Submit"** means any act by which a Contribution is transferred to Us by You by means of tangible or intangible media, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us, but excluding any transfer that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
|
||||||
|
|
||||||
|
**"Documentation"** means any non-software portion of a Contribution.
|
||||||
|
|
||||||
|
### 2\. License grant
|
||||||
|
|
||||||
|
#### 2.1 Copyright license to Us
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this Agreement, You hereby grant to Us a worldwide, royalty-free, Exclusive, perpetual and irrevocable (except as stated in Section 8.2) license, with the right to transfer an unlimited number of non-exclusive licenses or to grant sublicenses to third parties, under the Copyright covering the Contribution to use the Contribution by all means, including, but not limited to:
|
||||||
|
|
||||||
|
* publish the Contribution,
|
||||||
|
* modify the Contribution,
|
||||||
|
* prepare derivative works based upon or containing the Contribution and/or to combine the Contribution with other Materials,
|
||||||
|
* reproduce the Contribution in original or modified form,
|
||||||
|
* distribute, to make the Contribution available to the public, display and publicly perform the Contribution in original or modified form.
|
||||||
|
|
||||||
|
#### 2.2 Moral rights
|
||||||
|
|
||||||
|
Moral Rights remain unaffected to the extent they are recognized and not waivable by applicable law. Notwithstanding, You may add your name to the attribution mechanism customary used in the Materials you Contribute to, such as the header of the source code files of Your Contribution, and We will respect this attribution when using Your Contribution.
|
||||||
|
|
||||||
|
### 3\. Patents
|
||||||
|
|
||||||
|
#### 3.1 Patent license
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this Agreement You hereby grant to Us and to recipients of Materials distributed by Us a worldwide, royalty-free, non-exclusive, perpetual and irrevocable (except as stated in Section 3.2) patent license, with the right to transfer an unlimited number of non-exclusive licenses or to grant sublicenses to third parties, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with any Material (and portions of such combination). This license applies to all patents owned or controlled by You, whether already acquired or hereafter acquired, that would be infringed by making, having made, using, selling, offering for sale, importing or otherwise transferring of Your Contribution(s) alone or by combination of Your Contribution(s) with any Material.
|
||||||
|
|
||||||
|
#### 3.2 Revocation of patent license
|
||||||
|
|
||||||
|
You reserve the right to revoke the patent license stated in section 3.1 if We make any infringement claim that is targeted at your Contribution and not asserted for a Defensive Purpose. An assertion of claims of the Patents shall be considered for a "Defensive Purpose" if the claims are asserted against an entity that has filed, maintained, threatened, or voluntarily participated in a patent infringement lawsuit against Us or any of Our licensees.
|
||||||
|
|
||||||
|
### 4. Disclaimer
|
||||||
|
|
||||||
|
THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US AND BY US TO YOU. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION AND EXTENT TO THE MINIMUM PERIOD AND EXTENT PERMITTED BY LAW.
|
||||||
|
|
||||||
|
### 5. Consequential damage waiver
|
||||||
|
|
||||||
|
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU OR WE BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED.
|
||||||
|
|
||||||
|
### 6. Approximation of disclaimer and damage waiver
|
||||||
|
|
||||||
|
IF THE DISCLAIMER AND DAMAGE WAIVER MENTIONED IN SECTION 4. AND SECTION 5. CANNOT BE GIVEN LEGAL EFFECT UNDER APPLICABLE LOCAL LAW, REVIEWING COURTS SHALL APPLY LOCAL LAW THAT MOST CLOSELY APPROXIMATES AN ABSOLUTE WAIVER OF ALL CIVIL OR CONTRACTUAL LIABILITY IN CONNECTION WITH THE CONTRIBUTION.
|
||||||
|
|
||||||
|
### 7. Term
|
||||||
|
|
||||||
|
7.1 This Agreement shall come into effect upon Your acceptance of the terms and conditions.
|
||||||
|
|
||||||
|
7.3 In the event of a termination of this Agreement Sections 4, 5, 6, 7 and 8 shall survive such termination and shall remain in full force thereafter. For the avoidance of doubt, Free and Open Source Software (sub)licenses that have already been granted for Contributions at the date of the termination shall remain in full force after the termination of this Agreement.
|
||||||
|
|
||||||
|
### 8 Miscellaneous
|
||||||
|
|
||||||
|
8.1 This Agreement and all disputes, claims, actions, suits or other proceedings arising out of this agreement or relating in any way to it shall be governed by the laws of the United States of America excluding its private international law provisions.
|
||||||
|
|
||||||
|
8.2 This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings.
|
||||||
|
|
||||||
|
8.3 In case of Your death, this agreement shall continue with Your heirs. In case of more than one heir, all heirs must exercise their rights through a commonly authorized person.
|
||||||
|
|
||||||
|
8.4 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and that is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law.
|
||||||
|
|
||||||
|
8.5 You agree to notify Us of any facts or circumstances of which you become aware that would make this Agreement inaccurate in any respect.
|
||||||
50
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
50
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
description: The following form provides context for your submitted issue.
|
||||||
|
title: '[Bug]: '
|
||||||
|
labels:
|
||||||
|
- review
|
||||||
|
- bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out the below report.
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version, or commit number if you are using a branch and stipulate your fork if one exists
|
||||||
|
description: "If you are running on a branch using git execute this command in order to fetch the latest commit ID: `git log -1`. Please also stipulate if you are using a forked version and include a link to the fork source code."
|
||||||
|
placeholder: "0.1"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: Also tell us, what did you expect to happen?
|
||||||
|
placeholder: A clear and concise description of what the bug is. Please include screenshots where relevant.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce
|
||||||
|
attributes:
|
||||||
|
label: How To Reproduce
|
||||||
|
description: How can we reproduce this issue? (as minimally and as precisely as possible)
|
||||||
|
placeholder: A clear and concise description of how to reproduce the issue.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant log output
|
||||||
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Code of Conduct
|
||||||
|
description: By submitting this issue, you agree to follow our Code of Conduct as defined in `/CODE_OF_CONDUCT.md`.
|
||||||
|
options:
|
||||||
|
- label: I agree to follow this project's Code of Conduct
|
||||||
|
required: true
|
||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
2
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
blank_issues_enabled: false
|
||||||
53
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: The following form provides context for your requested feature.
|
||||||
|
title: '[Enhancement]: '
|
||||||
|
labels:
|
||||||
|
- review
|
||||||
|
- enhancement
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out the below report.
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version, or commit number if you are using a branch and stipulate your fork if one exists
|
||||||
|
description: "If you are running on a branch using git execute this command in order to fetch the latest commit ID: `git log -1`. Please also stipulate if you are using a forked version and include a link to the fork source code."
|
||||||
|
placeholder: "0.1"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to a problem?
|
||||||
|
placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like
|
||||||
|
placeholder: A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered
|
||||||
|
placeholder: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
placeholder: Add any other context or screenshots about the feature request here.
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Code of Conduct
|
||||||
|
description: By submitting this issue, you agree to follow our Code of Conduct as defined in `/CODE_OF_CONDUCT.md`.
|
||||||
|
options:
|
||||||
|
- label: I agree to follow this project's Code of Conduct
|
||||||
|
required: true
|
||||||
21
.github/pull_request_template.md
vendored
Normal file
21
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!---
|
||||||
|
|
||||||
|
Thanks for taking the time to fill out the below information about your PR.
|
||||||
|
|
||||||
|
--->
|
||||||
|
|
||||||
|
## What does this PR do?
|
||||||
|
Explain the motivation behind your PR. Provide a clear and concise description of what the PR is. Please include screenshots where relevant.
|
||||||
|
|
||||||
|
## Why is this change important?
|
||||||
|
Explain the motivation behind your PR.
|
||||||
|
|
||||||
|
## How to test this PR locally?
|
||||||
|
Commands to run the tests or instructions to test the changes
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
By submitting this PR, you agree to follow our Code of Conduct as defined in `/CODE_OF_CONDUCT.md`.
|
||||||
|
By submitting this PR, you are agreeing to the Contributor Agreement(s) as defined in links from `/CONTRIBUTING.md`.
|
||||||
|
|
||||||
|
--->
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/data
|
||||||
|
node_modules
|
||||||
|
packages
|
||||||
|
docker-compose.*.yml
|
||||||
14
.vscode/settings.json
vendored
Normal file
14
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"sqltools.connections": [
|
||||||
|
{
|
||||||
|
"previewLimit": 50,
|
||||||
|
"server": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"driver": "PostgreSQL",
|
||||||
|
"name": "local-jlinc",
|
||||||
|
"database": "jlinc",
|
||||||
|
"username": "jlinc",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
133
CODE_OF_CONDUCT.md
Normal file
133
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official email address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
|
|
||||||
6
CONTRIBUTING.md
Normal file
6
CONTRIBUTING.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Contribution Guidelines
|
||||||
|
|
||||||
|
Thank you for contributing! Before you contribute, we ask some things of you:
|
||||||
|
|
||||||
|
- Please follow our Code of Conduct, the Contributor Covenant. You can find a copy [in this repository](CODE_OF_CONDUCT.md) or under https://www.contributor-covenant.org/
|
||||||
|
- All Contributors must agree to [a CLA](.github/CLA/INDIVIDUAL.md). When opening a PR, the system will guide you through the process. However, if you contribute on behalf of a legal entity, we ask of you to agree to [a different CLA](.github/CLA/ENTITY.md). In that case, please contact us.
|
||||||
5
Dockerfile
Normal file
5
Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM node:20
|
||||||
|
ADD src /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm install
|
||||||
|
CMD ["npm", "start"]
|
||||||
557
LICENSE.md
Normal file
557
LICENSE.md
Normal file
@@ -0,0 +1,557 @@
|
|||||||
|
Server Side Public License
|
||||||
|
VERSION 1, OCTOBER 16, 2018
|
||||||
|
|
||||||
|
Copyright © 2018 MongoDB, Inc.
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this
|
||||||
|
license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
“This License” refers to Server Side Public License.
|
||||||
|
|
||||||
|
“Copyright” also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
“The Program” refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as “you”. “Licensees” and
|
||||||
|
“recipients” may be individuals or organizations.
|
||||||
|
|
||||||
|
To “modify” a work means to copy from or adapt all or part of the work in
|
||||||
|
a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a “modified version” of the
|
||||||
|
earlier work or a work “based on” the earlier work.
|
||||||
|
|
||||||
|
A “covered work” means either the unmodified Program or a work based on
|
||||||
|
the Program.
|
||||||
|
|
||||||
|
To “propagate” a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To “convey” a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through a
|
||||||
|
computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays “Appropriate Legal Notices” to the
|
||||||
|
extent that it includes a convenient and prominently visible feature that
|
||||||
|
(1) displays an appropriate copyright notice, and (2) tells the user that
|
||||||
|
there is no warranty for the work (except to the extent that warranties
|
||||||
|
are provided), that licensees may convey the work under this License, and
|
||||||
|
how to view a copy of this License. If the interface presents a list of
|
||||||
|
user commands or options, such as a menu, a prominent item in the list
|
||||||
|
meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The “source code” for a work means the preferred form of the work for
|
||||||
|
making modifications to it. “Object code” means any non-source form of a
|
||||||
|
work.
|
||||||
|
|
||||||
|
A “Standard Interface” means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that is
|
||||||
|
widely used among developers working in that language. The “System
|
||||||
|
Libraries” of an executable work include anything, other than the work as
|
||||||
|
a whole, that (a) is included in the normal form of packaging a Major
|
||||||
|
Component, but which is not part of that Major Component, and (b) serves
|
||||||
|
only to enable use of the work with that Major Component, or to implement
|
||||||
|
a Standard Interface for which an implementation is available to the
|
||||||
|
public in source code form. A “Major Component”, in this context, means a
|
||||||
|
major essential component (kernel, window system, and so on) of the
|
||||||
|
specific operating system (if any) on which the executable work runs, or
|
||||||
|
a compiler used to produce the work, or an object code interpreter used
|
||||||
|
to run it.
|
||||||
|
|
||||||
|
The “Corresponding Source” for a work in object code form means all the
|
||||||
|
source code needed to generate, install, and (for an executable work) run
|
||||||
|
the object code and to modify the work, including scripts to control
|
||||||
|
those activities. However, it does not include the work's System
|
||||||
|
Libraries, or general-purpose tools or generally available free programs
|
||||||
|
which are used unmodified in performing those activities but which are
|
||||||
|
not part of the work. For example, Corresponding Source includes
|
||||||
|
interface definition files associated with source files for the work, and
|
||||||
|
the source code for shared libraries and dynamically linked subprograms
|
||||||
|
that the work is specifically designed to require, such as by intimate
|
||||||
|
data communication or control flow between those subprograms and other
|
||||||
|
parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users can
|
||||||
|
regenerate automatically from other parts of the Corresponding Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program, subject to section 13. The
|
||||||
|
output from running a covered work is covered by this License only if the
|
||||||
|
output, given its content, constitutes a covered work. This License
|
||||||
|
acknowledges your rights of fair use or other equivalent, as provided by
|
||||||
|
copyright law. Subject to section 13, you may make, run and propagate
|
||||||
|
covered works that you do not convey, without conditions so long as your
|
||||||
|
license otherwise remains in force. You may convey covered works to
|
||||||
|
others for the sole purpose of having them make modifications exclusively
|
||||||
|
for you, or provide you with facilities for running those works, provided
|
||||||
|
that you comply with the terms of this License in conveying all
|
||||||
|
material for which you do not control copyright. Those thus making or
|
||||||
|
running the covered works for you must do so exclusively on your
|
||||||
|
behalf, under your direction and control, on terms that prohibit them
|
||||||
|
from making any copies of your copyrighted material outside their
|
||||||
|
relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under the
|
||||||
|
conditions stated below. Sublicensing is not allowed; section 10 makes it
|
||||||
|
unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article 11
|
||||||
|
of the WIPO copyright treaty adopted on 20 December 1996, or similar laws
|
||||||
|
prohibiting or restricting circumvention of such measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention is
|
||||||
|
effected by exercising rights under this License with respect to the
|
||||||
|
covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's users,
|
||||||
|
your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice; keep
|
||||||
|
intact all notices stating that this License and any non-permissive terms
|
||||||
|
added in accord with section 7 apply to the code; keep intact all notices
|
||||||
|
of the absence of any warranty; and give all recipients a copy of this
|
||||||
|
License along with the Program. You may charge any price or no price for
|
||||||
|
each copy that you convey, and you may offer support or warranty
|
||||||
|
protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the terms
|
||||||
|
of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified it,
|
||||||
|
and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is released
|
||||||
|
under this License and any conditions added under section 7. This
|
||||||
|
requirement modifies the requirement in section 4 to “keep intact all
|
||||||
|
notices”.
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this License to
|
||||||
|
anyone who comes into possession of a copy. This License will therefore
|
||||||
|
apply, along with any applicable section 7 additional terms, to the
|
||||||
|
whole of the work, and all its parts, regardless of how they are
|
||||||
|
packaged. This License gives no permission to license the work in any
|
||||||
|
other way, but it does not invalidate such permission if you have
|
||||||
|
separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your work
|
||||||
|
need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work, and
|
||||||
|
which are not combined with it such as to form a larger program, in or on
|
||||||
|
a volume of a storage or distribution medium, is called an “aggregate” if
|
||||||
|
the compilation and its resulting copyright are not used to limit the
|
||||||
|
access or legal rights of the compilation's users beyond what the
|
||||||
|
individual works permit. Inclusion of a covered work in an aggregate does
|
||||||
|
not cause this License to apply to the other parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms of
|
||||||
|
sections 4 and 5, provided that you also convey the machine-readable
|
||||||
|
Corresponding Source under the terms of this License, in one of these
|
||||||
|
ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium customarily
|
||||||
|
used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a written
|
||||||
|
offer, valid for at least three years and valid for as long as you
|
||||||
|
offer spare parts or customer support for that product model, to give
|
||||||
|
anyone who possesses the object code either (1) a copy of the
|
||||||
|
Corresponding Source for all the software in the product that is
|
||||||
|
covered by this License, on a durable physical medium customarily used
|
||||||
|
for software interchange, for a price no more than your reasonable cost
|
||||||
|
of physically performing this conveying of source, or (2) access to
|
||||||
|
copy the Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This alternative is
|
||||||
|
allowed only occasionally and noncommercially, and only if you received
|
||||||
|
the object code with such an offer, in accord with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated place
|
||||||
|
(gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to copy
|
||||||
|
the object code is a network server, the Corresponding Source may be on
|
||||||
|
a different server (operated by you or a third party) that supports
|
||||||
|
equivalent copying facilities, provided you maintain clear directions
|
||||||
|
next to the object code saying where to find the Corresponding Source.
|
||||||
|
Regardless of what server hosts the Corresponding Source, you remain
|
||||||
|
obligated to ensure that it is available for as long as needed to
|
||||||
|
satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided you
|
||||||
|
inform other peers where the object code and Corresponding Source of
|
||||||
|
the work are being offered to the general public at no charge under
|
||||||
|
subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be included
|
||||||
|
in conveying the object code work.
|
||||||
|
|
||||||
|
A “User Product” is either (1) a “consumer product”, which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, “normally used” refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
“Installation Information” for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as part
|
||||||
|
of a transaction in which the right of possession and use of the User
|
||||||
|
Product is transferred to the recipient in perpetuity or for a fixed term
|
||||||
|
(regardless of how the transaction is characterized), the Corresponding
|
||||||
|
Source conveyed under this section must be accompanied by the
|
||||||
|
Installation Information. But this requirement does not apply if neither
|
||||||
|
you nor any third party retains the ability to install modified object
|
||||||
|
code on the User Product (for example, the work has been installed in
|
||||||
|
ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access
|
||||||
|
to a network may be denied when the modification itself materially
|
||||||
|
and adversely affects the operation of the network or violates the
|
||||||
|
rules and protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided, in
|
||||||
|
accord with this section must be in a format that is publicly documented
|
||||||
|
(and with an implementation available to the public in source code form),
|
||||||
|
and must require no special password or key for unpacking, reading or
|
||||||
|
copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
“Additional permissions” are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall be
|
||||||
|
treated as though they were included in this License, to the extent that
|
||||||
|
they are valid under applicable law. If additional permissions apply only
|
||||||
|
to part of the Program, that part may be used separately under those
|
||||||
|
permissions, but the entire Program remains governed by this License
|
||||||
|
without regard to the additional permissions. When you convey a copy of
|
||||||
|
a covered work, you may at your option remove any additional permissions
|
||||||
|
from that copy, or from any part of it. (Additional permissions may be
|
||||||
|
written to require their own removal in certain cases when you modify the
|
||||||
|
work.) You may place additional permissions on material, added by you to
|
||||||
|
a covered work, for which you have or can give appropriate copyright
|
||||||
|
permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you add
|
||||||
|
to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some trade
|
||||||
|
names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that material
|
||||||
|
by anyone who conveys the material (or modified versions of it) with
|
||||||
|
contractual assumptions of liability to the recipient, for any
|
||||||
|
liability that these contractual assumptions directly impose on those
|
||||||
|
licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered “further
|
||||||
|
restrictions” within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further restriction,
|
||||||
|
you may remove that term. If a license document contains a further
|
||||||
|
restriction but permits relicensing or conveying under this License, you
|
||||||
|
may add to a covered work material governed by the terms of that license
|
||||||
|
document, provided that the further restriction does not survive such
|
||||||
|
relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you must
|
||||||
|
place, in the relevant source files, a statement of the additional terms
|
||||||
|
that apply to those files, or a notice indicating where to find the
|
||||||
|
applicable terms. Additional terms, permissive or non-permissive, may be
|
||||||
|
stated in the form of a separately written license, or stated as
|
||||||
|
exceptions; the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or modify
|
||||||
|
it is void, and will automatically terminate your rights under this
|
||||||
|
License (including any patent licenses granted under the third paragraph
|
||||||
|
of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your license
|
||||||
|
from a particular copyright holder is reinstated (a) provisionally,
|
||||||
|
unless and until the copyright holder explicitly and finally terminates
|
||||||
|
your license, and (b) permanently, if the copyright holder fails to
|
||||||
|
notify you of the violation by some reasonable means prior to 60 days
|
||||||
|
after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is reinstated
|
||||||
|
permanently if the copyright holder notifies you of the violation by some
|
||||||
|
reasonable means, this is the first time you have received notice of
|
||||||
|
violation of this License (for any work) from that copyright holder, and
|
||||||
|
you cure the violation prior to 30 days after your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or run a
|
||||||
|
copy of the Program. Ancillary propagation of a covered work occurring
|
||||||
|
solely as a consequence of using peer-to-peer transmission to receive a
|
||||||
|
copy likewise does not require acceptance. However, nothing other than
|
||||||
|
this License grants you permission to propagate or modify any covered
|
||||||
|
work. These actions infringe copyright if you do not accept this License.
|
||||||
|
Therefore, by modifying or propagating a covered work, you indicate your
|
||||||
|
acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically receives
|
||||||
|
a license from the original licensors, to run, modify and propagate that
|
||||||
|
work, subject to this License. You are not responsible for enforcing
|
||||||
|
compliance by third parties with this License.
|
||||||
|
|
||||||
|
An “entity transaction” is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered work
|
||||||
|
results from an entity transaction, each party to that transaction who
|
||||||
|
receives a copy of the work also receives whatever licenses to the work
|
||||||
|
the party's predecessor in interest had or could give under the previous
|
||||||
|
paragraph, plus a right to possession of the Corresponding Source of the
|
||||||
|
work from the predecessor in interest, if the predecessor has it or can
|
||||||
|
get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the rights
|
||||||
|
granted or affirmed under this License. For example, you may not impose a
|
||||||
|
license fee, royalty, or other charge for exercise of rights granted
|
||||||
|
under this License, and you may not initiate litigation (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that any patent claim
|
||||||
|
is infringed by making, using, selling, offering for sale, or importing
|
||||||
|
the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A “contributor” is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The work
|
||||||
|
thus licensed is called the contributor's “contributor version”.
|
||||||
|
|
||||||
|
A contributor's “essential patent claims” are all patent claims owned or
|
||||||
|
controlled by the contributor, whether already acquired or hereafter
|
||||||
|
acquired, that would be infringed by some manner, permitted by this
|
||||||
|
License, of making, using, or selling its contributor version, but do not
|
||||||
|
include claims that would be infringed only as a consequence of further
|
||||||
|
modification of the contributor version. For purposes of this definition,
|
||||||
|
“control” includes the right to grant patent sublicenses in a manner
|
||||||
|
consistent with the requirements of this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to make,
|
||||||
|
use, sell, offer for sale, import and otherwise run, modify and propagate
|
||||||
|
the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a “patent license” is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To “grant” such a patent license to a party
|
||||||
|
means to make such an agreement or commitment not to enforce a patent
|
||||||
|
against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license, and
|
||||||
|
the Corresponding Source of the work is not available for anyone to copy,
|
||||||
|
free of charge and under the terms of this License, through a publicly
|
||||||
|
available network server or other readily accessible means, then you must
|
||||||
|
either (1) cause the Corresponding Source to be so available, or (2)
|
||||||
|
arrange to deprive yourself of the benefit of the patent license for this
|
||||||
|
particular work, or (3) arrange, in a manner consistent with the
|
||||||
|
requirements of this License, to extend the patent license to downstream
|
||||||
|
recipients. “Knowingly relying” means you have actual knowledge that, but
|
||||||
|
for the patent license, your conveying the covered work in a country, or
|
||||||
|
your recipient's use of the covered work in a country, would infringe
|
||||||
|
one or more identifiable patents in that country that you have reason
|
||||||
|
to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties receiving
|
||||||
|
the covered work authorizing them to use, propagate, modify or convey a
|
||||||
|
specific copy of the covered work, then the patent license you grant is
|
||||||
|
automatically extended to all recipients of the covered work and works
|
||||||
|
based on it.
|
||||||
|
|
||||||
|
A patent license is “discriminatory” if it does not include within the
|
||||||
|
scope of its coverage, prohibits the exercise of, or is conditioned on
|
||||||
|
the non-exercise of one or more of the rights that are specifically
|
||||||
|
granted under this License. You may not convey a covered work if you are
|
||||||
|
a party to an arrangement with a third party that is in the business of
|
||||||
|
distributing software, under which you make payment to the third party
|
||||||
|
based on the extent of your activity of conveying the work, and under
|
||||||
|
which the third party grants, to any of the parties who would receive the
|
||||||
|
covered work from you, a discriminatory patent license (a) in connection
|
||||||
|
with copies of the covered work conveyed by you (or copies made from
|
||||||
|
those copies), or (b) primarily for and in connection with specific
|
||||||
|
products or compilations that contain the covered work, unless you
|
||||||
|
entered into that arrangement, or that patent license was granted, prior
|
||||||
|
to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting any
|
||||||
|
implied license or other defenses to infringement that may otherwise be
|
||||||
|
available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot use,
|
||||||
|
propagate or convey a covered work so as to satisfy simultaneously your
|
||||||
|
obligations under this License and any other pertinent obligations, then
|
||||||
|
as a consequence you may not use, propagate or convey it at all. For
|
||||||
|
example, if you agree to terms that obligate you to collect a royalty for
|
||||||
|
further conveying from those to whom you convey the Program, the only way
|
||||||
|
you could satisfy both those terms and this License would be to refrain
|
||||||
|
entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Offering the Program as a Service.
|
||||||
|
|
||||||
|
If you make the functionality of the Program or a modified version
|
||||||
|
available to third parties as a service, you must make the Service Source
|
||||||
|
Code available via network download to everyone at no charge, under the
|
||||||
|
terms of this License. Making the functionality of the Program or
|
||||||
|
modified version available to third parties as a service includes,
|
||||||
|
without limitation, enabling third parties to interact with the
|
||||||
|
functionality of the Program or modified version remotely through a
|
||||||
|
computer network, offering a service the value of which entirely or
|
||||||
|
primarily derives from the value of the Program or modified version, or
|
||||||
|
offering a service that accomplishes for users the primary purpose of the
|
||||||
|
Program or modified version.
|
||||||
|
|
||||||
|
“Service Source Code” means the Corresponding Source for the Program or
|
||||||
|
the modified version, and the Corresponding Source for all programs that
|
||||||
|
you use to make the Program or modified version available as a service,
|
||||||
|
including, without limitation, management software, user interfaces,
|
||||||
|
application program interfaces, automation software, monitoring software,
|
||||||
|
backup software, storage software and hosting software, all such that a
|
||||||
|
user could run an instance of the service using the Service Source Code
|
||||||
|
you make available.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
MongoDB, Inc. may publish revised and/or new versions of the Server Side
|
||||||
|
Public License from time to time. Such new versions will be similar in
|
||||||
|
spirit to the present version, but may differ in detail to address new
|
||||||
|
problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies that a certain numbered version of the Server Side Public
|
||||||
|
License “or any later version” applies to it, you have the option of
|
||||||
|
following the terms and conditions either of that numbered version or of
|
||||||
|
any later version published by MongoDB, Inc. If the Program does not
|
||||||
|
specify a version number of the Server Side Public License, you may
|
||||||
|
choose any version ever published by MongoDB, Inc.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future versions of
|
||||||
|
the Server Side Public License can be used, that proxy's public statement
|
||||||
|
of acceptance of a version permanently authorizes you to choose that
|
||||||
|
version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different permissions.
|
||||||
|
However, no additional obligations are imposed on any author or copyright
|
||||||
|
holder as a result of your choosing to follow a later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
|
||||||
|
ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF
|
||||||
|
THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
|
||||||
|
LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU
|
||||||
|
OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided above
|
||||||
|
cannot be given local legal effect according to their terms, reviewing
|
||||||
|
courts shall apply local law that most closely approximates an absolute
|
||||||
|
waiver of all civil liability in connection with the Program, unless a
|
||||||
|
warranty or assumption of liability accompanies a copy of the Program in
|
||||||
|
return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
12
README.md
Normal file
12
README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="./assets/logo.svg" width="600">
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
# JLINC Server
|
||||||
|
|
||||||
|
The JLINC Server is the official way to run the JLINC API and JLINC Archiver services.
|
||||||
|
|
||||||
|
Documentation: https://docs.jlinc.io
|
||||||
|
|
||||||
|
Details of the JLINC protocol, schema, and context can be found at: https://protocol.jlinc.org/.
|
||||||
1
assets/logo.svg
Normal file
1
assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 362.08 100.47"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#31a9ba;}</style></defs><title>JLINC Logo H White</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M157.32,29.36h8.35V57.2q0,13.9-13.92,13.91H137.84q-14.22,0-14-13.91l0-1.4h8.35v.25q0,6.72,6.68,6.71h11.66q6.7,0,6.71-6.73Z"/><path class="cls-1" d="M223.12,62.76v8.35H181.37V29.36h8.35v33.4Z"/><path class="cls-1" d="M247.18,71.11h-8.35V29.36h8.35Z"/><path class="cls-1" d="M271.23,42V71.11h-8.35V29.36h8.51l24.89,29.09V29.36h8.35V71.11h-8.49Z"/><path class="cls-1" d="M362.08,62.76v8.35H334.25q-13.92,0-13.92-13.91V43.28q0-13.92,13.92-13.92h27.83v8.35H335.39q-6.7,0-6.71,6.68V56.05q0,6.72,6.74,6.71Z"/><path class="cls-2" d="M8.52,41.22l9,9L36.46,69.16l7.76-7.76L25.3,42.48l-9.42-9.42q-4.5-4.52-5.14-9.26c-.48-3.11,1-6.36,4.35-9.73Q19.22,9.95,23.64,10t9.26,4.83l4,4,7.2-7.21L38.52,6a20.93,20.93,0,0,0-7.91-4.9A17.1,17.1,0,0,0,20,.61Q14,2,8.05,8a28.56,28.56,0,0,0-6.33,9.5,18.2,18.2,0,0,0-.79,11.4Q2.34,35,8.52,41.22Z"/><path class="cls-2" d="M61.4,56.25,42.48,75.17l-9.42,9.42q-4.52,4.51-9.26,5.14t-9.73-4.35Q9.95,81.27,10,76.83t4.83-9.26l4-3.95-7.21-7.21L6,62a21,21,0,0,0-4.9,7.92A17.09,17.09,0,0,0,.61,80.48q1.43,6,7.36,12a28.87,28.87,0,0,0,9.5,6.33,18.2,18.2,0,0,0,11.4.79Q35,98.13,41.22,92l9-9L69.16,64Z"/><path class="cls-2" d="M92,59.26l-9-9L64,31.31l-7.76,7.76L75.17,58l9.42,9.42q4.51,4.52,5.14,9.26t-4.35,9.74c-2.74,2.74-5.59,4.12-8.55,4.11s-6-1.55-9.26-4.83l-3.95-4-7.21,7.2L62,94.48a21.11,21.11,0,0,0,7.92,4.91,17.06,17.06,0,0,0,10.6.47q6-1.42,12-7.36A28.87,28.87,0,0,0,98.76,83a18.16,18.16,0,0,0,.79-11.39Q98.13,65.43,92,59.26Z"/><path class="cls-2" d="M39.07,44.22,58,25.3l9.42-9.42q4.52-4.5,9.26-5.14c3.12-.48,6.36,1,9.74,4.35,2.74,2.75,4.11,5.59,4.11,8.55s-1.55,6-4.83,9.26l-4,4,7.2,7.2,5.54-5.54a21.07,21.07,0,0,0,4.91-7.91A17.17,17.17,0,0,0,99.86,20Q98.44,14,92.5,8.05A28.56,28.56,0,0,0,83,1.72,18.19,18.19,0,0,0,71.6.93Q65.44,2.34,59.26,8.52l-9,9L31.31,36.46Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
81
docker-compose.yml
Normal file
81
docker-compose.yml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
jlinc-server:
|
||||||
|
image: registry.jlinc.io/jlinc-server
|
||||||
|
container_name: jlinc-server
|
||||||
|
environment:
|
||||||
|
# BASE CONFIGURATION
|
||||||
|
# ==================
|
||||||
|
# If you wish to display debug logging
|
||||||
|
# DEBUG: "true"
|
||||||
|
#
|
||||||
|
# DB URL
|
||||||
|
POSTGRES_URL: postgresql://jlinc:password@jlinc-db/jlinc
|
||||||
|
# Public URLs, callback URLS will be PUBLIC_CALLBACK_URL + '/callback/<module>'
|
||||||
|
PUBLIC_CORE_URL: http://localhost:9090
|
||||||
|
PUBLIC_ARCHIVE_URL: http://localhost:9090
|
||||||
|
PUBLIC_CALLBACK_URL: http://localhost:9090
|
||||||
|
# Enabled app modules
|
||||||
|
APP_MODULES: core, archive
|
||||||
|
# Enabled authentication modules
|
||||||
|
AUTH_MODULES: oidc, github, google
|
||||||
|
# Secure secret for memory store, generate with `openssl rand -hex 64`
|
||||||
|
SECURE_SECRET: 9ed678e6da3333a53c635039a5f53015ba3d4c841a0ade4e46f31c2d42c9b3e71edcdda7e1d75d066a39a8f56f9eb325d556a90195c6e7f45c6e2fffd0e98a7a
|
||||||
|
# Default FedID server to use for identity management
|
||||||
|
DEFAULT_FEDID_URL: https://fedid-test.jlinc.io
|
||||||
|
|
||||||
|
# AUTH MODULES
|
||||||
|
# ============
|
||||||
|
|
||||||
|
# OpenID Connect
|
||||||
|
# --------------
|
||||||
|
# Works with any OIDC provider
|
||||||
|
# OIDC_CLIENT_ID:
|
||||||
|
# OIDC_CLIENT_SECRET:
|
||||||
|
# OIDC_ISSUER: https://fedid-test.jlinc.io/oidc
|
||||||
|
# OIDC_AUTHORIZATION_URL: https://fedid-test.jlinc.io/oidc/auth
|
||||||
|
# OIDC_TOKEN_URL: https://fedid-test.jlinc.io/oidc/token
|
||||||
|
# OIDC_USERINFO_URL: https://fedid-test.jlinc.io/oidc/me
|
||||||
|
# OIDC_LOGOUT_URL: https://fedid-test.jlinc.io/oidc/session/end
|
||||||
|
|
||||||
|
# GitHub
|
||||||
|
# --------------
|
||||||
|
# Set up your GitHub app: https://github.com/settings/applications/new
|
||||||
|
# GITHUB_CLIENT_ID:
|
||||||
|
# GITHUB_CLIENT_SECRET:
|
||||||
|
|
||||||
|
# Google
|
||||||
|
# --------------
|
||||||
|
# Set up your Google app: https://console.developers.google.com/
|
||||||
|
# - Create a new app
|
||||||
|
# - Set up auth: https://console.cloud.google.com/auth/overview
|
||||||
|
# GOOGLE_CLIENT_ID:
|
||||||
|
# GOOGLE_CLIENT_SECRET:
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- jlinc-db
|
||||||
|
networks:
|
||||||
|
- jlinc
|
||||||
|
# For dev
|
||||||
|
ports:
|
||||||
|
- 9090:9090
|
||||||
|
|
||||||
|
jlinc-db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
container_name: jlinc-db
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=jlinc
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=jlinc
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:5432:5432
|
||||||
|
volumes:
|
||||||
|
- ./data/db:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- jlinc
|
||||||
|
|
||||||
|
networks:
|
||||||
|
jlinc:
|
||||||
6
src/.prettierrc
Normal file
6
src/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 180
|
||||||
|
}
|
||||||
18
src/apps.js
Normal file
18
src/apps.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { getConfig } from "./common/config.js";
|
||||||
|
import { getPool } from "./db/index.js";
|
||||||
|
|
||||||
|
export async function loadApps() {
|
||||||
|
const config = getConfig();
|
||||||
|
const client = await getPool();
|
||||||
|
for (const type in config.appModules) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO public.app (
|
||||||
|
type
|
||||||
|
) VALUES (
|
||||||
|
$1
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
|
`, [
|
||||||
|
type,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/common/config.js
Normal file
39
src/common/config.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const config = {
|
||||||
|
debug: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadConfig() {
|
||||||
|
if (process.env.DEBUG) config.debug = process.env.DEBUG;
|
||||||
|
if (process.env.POSTGRES_URL) config.postgresUrl = process.env.POSTGRES_URL;
|
||||||
|
if (process.env.PUBLIC_CORE_URL) config.publicCoreUrl = process.env.PUBLIC_CORE_URL;
|
||||||
|
if (process.env.PUBLIC_ARCHIVE_URL) config.publicArchiveUrl = process.env.PUBLIC_ARCHIVE_URL;
|
||||||
|
if (process.env.PUBLIC_CALLBACK_URL) config.publicCallbackUrl = process.env.PUBLIC_CALLBACK_URL;
|
||||||
|
if (process.env.SECURE_SECRET) config.secureSecret = process.env.SECURE_SECRET;
|
||||||
|
if (process.env.DEFAULT_FEDID_URL) config.defaultFedidUrl = process.env.DEFAULT_FEDID_URL;
|
||||||
|
if (process.env.AUTH_MODULES) {
|
||||||
|
config.authModules = {};
|
||||||
|
const authModules = process.env.AUTH_MODULES.split(',').map(a => a.trim());
|
||||||
|
for (const type of authModules) {
|
||||||
|
const authModulePath = `../modules/auth/${type}.js`;
|
||||||
|
const { getModuleConfig } = await import(authModulePath);
|
||||||
|
config.authModules[type] = getModuleConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.appModules = {};
|
||||||
|
let appModules = [
|
||||||
|
'core',
|
||||||
|
'archive',
|
||||||
|
];
|
||||||
|
if (process.env.APP_MODULES) {
|
||||||
|
appModules = process.env.APP_MODULES.split(',').map(a => a.trim());
|
||||||
|
}
|
||||||
|
for (const type of appModules) {
|
||||||
|
const appModulePath = `../modules/app/${type}.js`;
|
||||||
|
const { getModuleConfig } = await import(appModulePath);
|
||||||
|
config.appModules[type] = getModuleConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
1
src/common/sleep.js
Normal file
1
src/common/sleep.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
JLINC General Audit Agreement
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
This document outlines the terms and conditions for using the JLINC protocol for auditing purposes. The purpose of this agreement is to clarify that no formal legal agreements are established between parties beyond their use of the JLINC protocol for data communication auditing.
|
||||||
|
|
||||||
|
**Purpose:** This agreement exists solely to provide a framework for using the JLINC protocol to audit data communication among parties. It does not create any legal obligations or enforceable commitments beyond its scope.
|
||||||
|
|
||||||
|
**Awareness of Agreement:** Recognizing that some parties may not be aware of this agreement, it is understood that by utilizing the JLINC protocol under this agreement, they are not deemed to have accepted these terms.
|
||||||
|
|
||||||
|
**Use of Protocol:** Under this agreement, the JLINC protocol is employed exclusively for auditing data communication, ensuring transparency and efficiency in transactions without establishing any formal agreements beyond this document.
|
||||||
|
|
||||||
|
**No Data Provenance/Protection:** It is clarified that no data provenance or protection measures are provided under this agreement. Entities leveraging this agreement are responsible for their own data integrity and security outside the protocol's use.
|
||||||
|
|
||||||
|
**Liability:** Liability is limited to actions taken within the context of using the JLINC protocol. No party assumes responsibility for others' actions beyond their direct involvement with the JLINC protocol.
|
||||||
|
|
||||||
|
**Governing Law:** This agreement is governed by the laws of the system operator's and user's jurisdictions. Any legal matters arising from this agreement must be resolved in courts within those jurisdictions.
|
||||||
|
|
||||||
|
By adhering to these terms, parties acknowledge their responsibilities and agree to use the specified protocol solely for auditing purposes without implied legal obligations beyond those stated herein.
|
||||||
190
src/db/index.js
Normal file
190
src/db/index.js
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
import pkg from "pg";
|
||||||
|
const { Pool } = pkg;
|
||||||
|
|
||||||
|
import { sleep } from "../common/sleep.js";
|
||||||
|
import { getConfig } from "../common/config.js";
|
||||||
|
import { data } from "../modules/core/data/index.js";
|
||||||
|
|
||||||
|
let pool;
|
||||||
|
|
||||||
|
export async function init() {
|
||||||
|
const config = getConfig();
|
||||||
|
let ready = false;
|
||||||
|
let client;
|
||||||
|
while (!ready) {
|
||||||
|
try {
|
||||||
|
pool = new Pool({
|
||||||
|
connectionString: config.postgresUrl,
|
||||||
|
});
|
||||||
|
client = await pool.connect();
|
||||||
|
const res = await client.query(`SELECT 1`);
|
||||||
|
if (res.rows.length < 1) {
|
||||||
|
throw new Error("");
|
||||||
|
}
|
||||||
|
ready = true;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
} catch (e) {
|
||||||
|
console.log("DB not ready, waiting...");
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPool() {
|
||||||
|
return await pool.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function close() {
|
||||||
|
console.log(`Closing DB`);
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function migrate() {
|
||||||
|
console.log(`Starting migration`);
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
const migrationExists = await client.query(`
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN (SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = 'system' AND table_name = 'migrate') > 0
|
||||||
|
THEN TRUE
|
||||||
|
ELSE FALSE
|
||||||
|
END AS exists
|
||||||
|
`);
|
||||||
|
if (!migrationExists.rows[0].exists) {
|
||||||
|
await client.query(`
|
||||||
|
DROP SCHEMA IF EXISTS system CASCADE;
|
||||||
|
CREATE SCHEMA system;
|
||||||
|
|
||||||
|
-- Migrations
|
||||||
|
DROP TABLE IF EXISTS system.migrate CASCADE;
|
||||||
|
CREATE TABLE system.migrate (
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__system__migrate__id ON system.migrate (id);
|
||||||
|
CREATE INDEX idx__system__migrate__status ON system.migrate (status);
|
||||||
|
CREATE INDEX idx__system__migrate__created_ts ON system.migrate (created_ts);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
let lastMigration = "0";
|
||||||
|
const lastMigrationResult = await client.query(`
|
||||||
|
SELECT id
|
||||||
|
FROM system.migrate
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1;
|
||||||
|
`);
|
||||||
|
if (lastMigrationResult.rows.length > 0 && lastMigrationResult.rows[0].id) {
|
||||||
|
lastMigration = lastMigrationResult.rows[0].id;
|
||||||
|
}
|
||||||
|
const files = fs.readdirSync("./db/migrations", {
|
||||||
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
for await (const file of files) {
|
||||||
|
const migrationId = file.name.slice(0, 6);
|
||||||
|
if (migrationId > lastMigration) {
|
||||||
|
const label = file.name.slice(7, file.name.length - 4);
|
||||||
|
console.log(`Running migration ${label} (${migrationId})`);
|
||||||
|
const sqlStr = fs.readFileSync(path.join("./db/migrations", file.name), "utf8");
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
await client.query(sqlStr);
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO system.migrate (
|
||||||
|
id,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
'complete'
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
[migrationId],
|
||||||
|
);
|
||||||
|
await client.query("COMMIT");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw new Error("migration error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
console.log(`Ending migration`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function populateAgreements() {
|
||||||
|
console.log(`Starting agreement population`);
|
||||||
|
const config = getConfig();
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync("./db/agreements", {
|
||||||
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
for await (const file of files) {
|
||||||
|
const agreementId = file.name.slice(0, 36);
|
||||||
|
const title = file.name.slice(39, file.name.length - 3);
|
||||||
|
const markdown = fs.readFileSync(path.join("./db/agreements", file.name), "utf8").trim();
|
||||||
|
const hash = createHash('sha256')
|
||||||
|
.update(markdown)
|
||||||
|
.digest('hex')
|
||||||
|
try {
|
||||||
|
const agreementExists = await client.query(`
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN (SELECT COUNT(1) FROM agreement WHERE agreement_id_uuid = $1 AND user_id IS NULL) > 0
|
||||||
|
THEN TRUE
|
||||||
|
ELSE FALSE
|
||||||
|
END AS exists
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
agreementId,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if (!agreementExists.rows[0].exists) {
|
||||||
|
console.log(`Adding agreement '${title}' (${agreementId})`);
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO agreement_content (
|
||||||
|
title,
|
||||||
|
markdown,
|
||||||
|
hash
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
|
`, [
|
||||||
|
title,
|
||||||
|
markdown,
|
||||||
|
hash,
|
||||||
|
]);
|
||||||
|
const agreement = {
|
||||||
|
uri: `${config.publicCoreUrl}/agreements/${hash}`,
|
||||||
|
purposes: [],
|
||||||
|
caveats: [],
|
||||||
|
shortNames: [],
|
||||||
|
validRoles: [],
|
||||||
|
}
|
||||||
|
await data.agreement.create(agreement, null, client, agreementId);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
throw new Error("agreement population error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
console.log(`Ending agreement population`);
|
||||||
|
}
|
||||||
45
src/db/migrations/000000 - init.sql
Normal file
45
src/db/migrations/000000 - init.sql
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
-- DROP TABLE IF EXISTS public.user CASCADE;
|
||||||
|
CREATE TABLE public.user (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
username TEXT,
|
||||||
|
photo TEXT,
|
||||||
|
issuer TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
identifier TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__user__issuer_identifier UNIQUE (issuer, identifier)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__user__username ON public.user (username);
|
||||||
|
CREATE INDEX idx__user__identifier ON public.user (identifier);
|
||||||
|
CREATE INDEX idx__user__type ON public.user (type);
|
||||||
|
CREATE INDEX idx__user__issuer ON public.user (issuer);
|
||||||
|
CREATE INDEX idx__user__created_ts ON public.user (created_ts);
|
||||||
|
CREATE INDEX idx__user__updated_ts ON public.user (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.app CASCADE;
|
||||||
|
CREATE TABLE public.app (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
type TEXT UNIQUE NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__app__type ON public.app (type);
|
||||||
|
CREATE INDEX idx__app__created_ts ON public.app (created_ts);
|
||||||
|
CREATE INDEX idx__app__updated_ts ON public.app (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.auth CASCADE;
|
||||||
|
CREATE TABLE public.auth (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
app_id BIGINT NOT NULL REFERENCES public.app(id),
|
||||||
|
api_key TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__auth__user_id_app_id UNIQUE (user_id, app_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__auth__user_id ON public.auth (user_id);
|
||||||
|
CREATE INDEX idx__auth__app_id ON public.auth (app_id);
|
||||||
|
CREATE INDEX idx__auth__api_key ON public.auth (api_key);
|
||||||
|
CREATE INDEX idx__auth__created_ts ON public.auth (created_ts);
|
||||||
|
CREATE INDEX idx__auth__updated_ts ON public.auth (updated_ts);
|
||||||
42
src/db/migrations/000001 - archive.sql
Normal file
42
src/db/migrations/000001 - archive.sql
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
CREATE TYPE HASH_TYPE AS ENUM ('SHA256');
|
||||||
|
CREATE TYPE SIGNATURE_TYPE AS ENUM ('JWS/JCS');
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.audit CASCADE;
|
||||||
|
CREATE TABLE public.audit (
|
||||||
|
audit_id BIGSERIAL PRIMARY KEY,
|
||||||
|
version SMALLINT NOT NULL,
|
||||||
|
event_id UUID,
|
||||||
|
agreement_id UUID,
|
||||||
|
hash_type HASH_TYPE NOT NULL,
|
||||||
|
digest TEXT NOT NULL,
|
||||||
|
created BIGINT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__audit__event_id ON public.audit (event_id);
|
||||||
|
CREATE INDEX idx__public__audit__agreement_id ON public.audit (agreement_id);
|
||||||
|
CREATE INDEX idx__public__audit__created ON public.audit (created);
|
||||||
|
CREATE INDEX idx__public__audit__created_ts ON public.audit (created_ts);
|
||||||
|
CREATE INDEX idx__public__audit__updated_ts ON public.audit (updated_ts);
|
||||||
|
ALTER TABLE public.audit
|
||||||
|
ADD CONSTRAINT cnst__audit__event_or_agreement
|
||||||
|
CHECK (num_nonnulls(event_id, agreement_id) = 1);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.audit_signature CASCADE;
|
||||||
|
CREATE TABLE public.audit_signature (
|
||||||
|
signature_id BIGSERIAL PRIMARY KEY,
|
||||||
|
audit_id BIGINT NOT NULL REFERENCES public.audit(audit_id),
|
||||||
|
version SMALLINT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
signedOn BIGINT NOT NULL,
|
||||||
|
type SIGNATURE_TYPE NOT NULL,
|
||||||
|
jws TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__audit_signature__id ON public.audit_signature (id);
|
||||||
|
CREATE INDEX idx__public__audit_signature__signedOn ON public.audit_signature (signedOn);
|
||||||
|
CREATE INDEX idx__public__audit_signature__type ON public.audit_signature (type);
|
||||||
|
CREATE INDEX idx__public__audit_signature__created_ts ON public.audit_signature (created_ts);
|
||||||
|
CREATE INDEX idx__public__audit_signature__updated_ts ON public.audit_signature (updated_ts);
|
||||||
28
src/db/migrations/000002 - usage.sql
Normal file
28
src/db/migrations/000002 - usage.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-- DROP TABLE IF EXISTS public.usage_url CASCADE;
|
||||||
|
CREATE TABLE public.usage_url (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
module TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__usage_url__url_module UNIQUE (url, module)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__usage_url__url ON public.usage_url (url);
|
||||||
|
CREATE INDEX idx__public__usage_url__module ON public.usage_url (module);
|
||||||
|
CREATE INDEX idx__public__usage_url__created_ts ON public.usage_url (created_ts);
|
||||||
|
CREATE INDEX idx__public__usage_url__updated_ts ON public.usage_url (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.usage CASCADE;
|
||||||
|
CREATE TABLE public.usage (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
usage_url_id BIGINT NOT NULL REFERENCES public.usage_url(id),
|
||||||
|
success BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__usage__user_id ON public.usage (user_id);
|
||||||
|
CREATE INDEX idx__public__usage__usage_url_id ON public.usage (usage_url_id);
|
||||||
|
CREATE INDEX idx__public__usage__success ON public.usage (success);
|
||||||
|
CREATE INDEX idx__public__usage__created_ts ON public.usage (created_ts);
|
||||||
|
CREATE INDEX idx__public__usage__updated_ts ON public.usage (updated_ts);
|
||||||
220
src/db/migrations/000003 - data.sql
Normal file
220
src/db/migrations/000003 - data.sql
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
-- Agreements
|
||||||
|
-- DROP TABLE IF EXISTS public.agreement CASCADE;
|
||||||
|
CREATE TABLE public.agreement (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT REFERENCES public.user(id),
|
||||||
|
version SMALLINT NOT NULL,
|
||||||
|
parent TEXT NOT NULL,
|
||||||
|
agreement_id_uuid UUID UNIQUE NOT NULL,
|
||||||
|
created BIGINT NOT NULL,
|
||||||
|
created_as_ts TIMESTAMPTZ NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__agreement__user_id ON public.agreement (user_id);
|
||||||
|
CREATE INDEX idx__public__agreement__version ON public.agreement (version);
|
||||||
|
CREATE INDEX idx__public__agreement__parent ON public.agreement (parent);
|
||||||
|
CREATE INDEX idx__public__agreement__agreement_id_uuid ON public.agreement (agreement_id_uuid);
|
||||||
|
CREATE INDEX idx__public__agreement__created ON public.agreement (created);
|
||||||
|
CREATE INDEX idx__public__agreement__created_as_ts ON public.agreement (created_as_ts);
|
||||||
|
CREATE INDEX idx__public__agreement__created_ts ON public.agreement (created_ts);
|
||||||
|
CREATE INDEX idx__public__agreement__updated_ts ON public.agreement (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.agreement_required_id CASCADE;
|
||||||
|
CREATE TABLE public.agreement_required_id (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
|
||||||
|
required_id TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__agreement_required_id UNIQUE (user_id, agreement_id, required_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__agreement_required_id__user_id ON public.agreement_required_id (user_id);
|
||||||
|
CREATE INDEX idx__public__agreement_required_id__agreement_id ON public.agreement_required_id (agreement_id);
|
||||||
|
CREATE INDEX idx__public__agreement_required_id__created_ts ON public.agreement_required_id (created_ts);
|
||||||
|
CREATE INDEX idx__public__agreement_required_id__updated_ts ON public.agreement_required_id (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.purpose CASCADE;
|
||||||
|
CREATE TABLE public.purpose (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__purpose UNIQUE (user_id, value)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__purpose__user_id ON public.purpose (user_id);
|
||||||
|
CREATE INDEX idx__public__purpose__value ON public.purpose (value);
|
||||||
|
CREATE INDEX idx__public__purpose__created_ts ON public.purpose (created_ts);
|
||||||
|
CREATE INDEX idx__public__purpose__updated_ts ON public.purpose (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.agreement_purpose CASCADE;
|
||||||
|
CREATE TABLE public.agreement_purpose (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
|
||||||
|
purpose_id BIGINT NOT NULL REFERENCES public.purpose(id),
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__agreement_purpose UNIQUE (user_id, agreement_id, purpose_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__agreement_purpose__user_id ON public.agreement_purpose (user_id);
|
||||||
|
CREATE INDEX idx__public__agreement_purpose__agreement_id ON public.agreement_purpose (agreement_id);
|
||||||
|
CREATE INDEX idx__public__agreement_purpose__purpose_id ON public.agreement_purpose (purpose_id);
|
||||||
|
CREATE INDEX idx__public__agreement_purpose__created_ts ON public.agreement_purpose (created_ts);
|
||||||
|
CREATE INDEX idx__public__agreement_purpose__updated_ts ON public.agreement_purpose (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.caveat CASCADE;
|
||||||
|
CREATE TABLE public.caveat (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__caveat UNIQUE (user_id, value)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__caveat__user_id ON public.caveat (user_id);
|
||||||
|
CREATE INDEX idx__public__caveat__value ON public.caveat (value);
|
||||||
|
CREATE INDEX idx__public__caveat__created_ts ON public.caveat (created_ts);
|
||||||
|
CREATE INDEX idx__public__caveat__updated_ts ON public.caveat (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.agreement_caveat CASCADE;
|
||||||
|
CREATE TABLE public.agreement_caveat (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
|
||||||
|
caveat_id BIGINT NOT NULL REFERENCES public.caveat(id),
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__agreement_caveat UNIQUE (user_id, agreement_id, caveat_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__agreement_caveat__user_id ON public.agreement_caveat (user_id);
|
||||||
|
CREATE INDEX idx__public__agreement_caveat__agreement_id ON public.agreement_caveat (agreement_id);
|
||||||
|
CREATE INDEX idx__public__agreement_caveat__caveat_id ON public.agreement_caveat (caveat_id);
|
||||||
|
CREATE INDEX idx__public__agreement_caveat__created_ts ON public.agreement_caveat (created_ts);
|
||||||
|
CREATE INDEX idx__public__agreement_caveat__updated_ts ON public.agreement_caveat (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.role CASCADE;
|
||||||
|
CREATE TABLE public.role (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__role UNIQUE (user_id, value)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__role__user_id ON public.role (user_id);
|
||||||
|
CREATE INDEX idx__public__role__value ON public.role (value);
|
||||||
|
CREATE INDEX idx__public__role__created_ts ON public.role (created_ts);
|
||||||
|
CREATE INDEX idx__public__role__updated_ts ON public.role (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.agreement_role CASCADE;
|
||||||
|
CREATE TABLE public.agreement_role (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
|
||||||
|
role_id BIGINT NOT NULL REFERENCES public.role(id),
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__agreement_role UNIQUE (user_id, agreement_id, role_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__agreement_role__user_id ON public.agreement_role (user_id);
|
||||||
|
CREATE INDEX idx__public__agreement_role__agreement_id ON public.agreement_role (agreement_id);
|
||||||
|
CREATE INDEX idx__public__agreement_role__role_id ON public.agreement_role (role_id);
|
||||||
|
CREATE INDEX idx__public__agreement_role__created_ts ON public.agreement_role (created_ts);
|
||||||
|
CREATE INDEX idx__public__agreement_role__updated_ts ON public.agreement_role (updated_ts);
|
||||||
|
|
||||||
|
|
||||||
|
-- Events
|
||||||
|
-- DROP TABLE IF EXISTS public.event_type CASCADE;
|
||||||
|
CREATE TABLE public.event_type (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__event_type__value ON public.event_type (value);
|
||||||
|
CREATE INDEX idx__public__event_type__created_ts ON public.event_type (created_ts);
|
||||||
|
CREATE INDEX idx__public__event_type__updated_ts ON public.event_type (updated_ts);
|
||||||
|
|
||||||
|
INSERT INTO public.event_type (value) VALUES ('data');
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.event CASCADE;
|
||||||
|
CREATE TABLE public.event (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
version SMALLINT NOT NULL,
|
||||||
|
event_id_uuid UUID UNIQUE NOT NULL,
|
||||||
|
event_type_id BIGINT NOT NULL REFERENCES public.event_type(id),
|
||||||
|
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
|
||||||
|
sender_id TEXT NOT NULL,
|
||||||
|
recipient_id TEXT NOT NULL,
|
||||||
|
created BIGINT NOT NULL,
|
||||||
|
created_as_ts TIMESTAMPTZ NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__event__user_id ON public.event (user_id);
|
||||||
|
CREATE INDEX idx__public__event__version ON public.event (version);
|
||||||
|
CREATE INDEX idx__public__event__event_id_uuid ON public.event (event_id_uuid);
|
||||||
|
CREATE INDEX idx__public__event__event_type_id ON public.event (event_type_id);
|
||||||
|
CREATE INDEX idx__public__event__agreement_id ON public.event (agreement_id);
|
||||||
|
CREATE INDEX idx__public__event__sender_id ON public.event (sender_id);
|
||||||
|
CREATE INDEX idx__public__event__recipient_id ON public.event (recipient_id);
|
||||||
|
CREATE INDEX idx__public__event__created_ts ON public.event (created_ts);
|
||||||
|
CREATE INDEX idx__public__event__updated_ts ON public.event (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.event_data CASCADE;
|
||||||
|
-- DROP SEQUENCE IF EXISTS seq__event_data__id;
|
||||||
|
-- CREATE SEQUENCE seq__event_data__id;
|
||||||
|
-- CREATE TABLE public.event_data (
|
||||||
|
-- id BIGINT NOT NULL DEFAULT nextval('seq__event_data__id'),
|
||||||
|
-- user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
-- event_id BIGINT NOT NULL REFERENCES public.event(id),
|
||||||
|
-- data TEXT NOT NULL,
|
||||||
|
-- created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
-- updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
-- PRIMARY KEY (user_id, id)
|
||||||
|
-- ) PARTITION BY LIST (user_id);
|
||||||
|
CREATE TABLE public.event_data (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
event_id BIGINT NOT NULL REFERENCES public.event(id),
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__event_data__event_id ON public.event_data (event_id);
|
||||||
|
CREATE INDEX idx__public__event_data__user_id ON public.event_data (user_id);
|
||||||
|
CREATE INDEX idx__public__event_data__created_ts ON public.event_data (created_ts);
|
||||||
|
CREATE INDEX idx__public__event_data__updated_ts ON public.event_data (updated_ts);
|
||||||
|
|
||||||
|
-- Signatures
|
||||||
|
-- DROP TABLE IF EXISTS public.signature CASCADE;
|
||||||
|
CREATE TABLE public.signature (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
version SMALLINT NOT NULL,
|
||||||
|
signer_id TEXT NOT NULL,
|
||||||
|
signed_on BIGINT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
jws TEXT NOT NULL,
|
||||||
|
role_id BIGINT REFERENCES public.role(id),
|
||||||
|
agreement_id BIGINT REFERENCES public.agreement(id),
|
||||||
|
event_id BIGINT REFERENCES public.event(id),
|
||||||
|
signed_on_as_ts TIMESTAMPTZ NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__signature__agreement UNIQUE (user_id, signer_id, agreement_id),
|
||||||
|
CONSTRAINT uniq__signature__event UNIQUE (user_id, signer_id, event_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__signature__user_id ON public.signature (user_id);
|
||||||
|
CREATE INDEX idx__public__signature__version ON public.signature (version);
|
||||||
|
CREATE INDEX idx__public__signature__signed_on ON public.signature (signed_on);
|
||||||
|
CREATE INDEX idx__public__signature__role_id ON public.signature (role_id);
|
||||||
|
CREATE INDEX idx__public__signature__agreement_id ON public.signature (agreement_id);
|
||||||
|
CREATE INDEX idx__public__signature__event_id ON public.signature (event_id);
|
||||||
|
CREATE INDEX idx__public__signature__signed_on_as_ts ON public.signature (signed_on_as_ts);
|
||||||
|
CREATE INDEX idx__public__signature__created_ts ON public.signature (created_ts);
|
||||||
|
CREATE INDEX idx__public__signature__updated_ts ON public.signature (updated_ts);
|
||||||
19
src/db/migrations/000004 - entity.sql
Normal file
19
src/db/migrations/000004 - entity.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-- DROP TABLE IF EXISTS public.entity CASCADE;
|
||||||
|
CREATE TABLE public.entity (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
fedid_url TEXT NOT NULL,
|
||||||
|
short_name TEXT NOT NULL,
|
||||||
|
did_id TEXT NOT NULL,
|
||||||
|
control_private_key_b64u TEXT NOT NULL,
|
||||||
|
recovery_private_key_b64u TEXT NOT NULL,
|
||||||
|
did_doc JSONB NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__entity__user_id ON public.entity (user_id);
|
||||||
|
CREATE INDEX idx__public__entity__fedid_url ON public.entity (fedid_url);
|
||||||
|
CREATE INDEX idx__public__entity__short_name ON public.entity (short_name);
|
||||||
|
CREATE INDEX idx__public__entity__did_id ON public.entity (did_id);
|
||||||
|
CREATE INDEX idx__public__entity__created_ts ON public.entity (created_ts);
|
||||||
|
CREATE INDEX idx__public__entity__updated_ts ON public.entity (updated_ts);
|
||||||
14
src/db/migrations/000005 - agreement-content.sql
Normal file
14
src/db/migrations/000005 - agreement-content.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
-- DROP TABLE IF EXISTS public.agreement_content CASCADE;
|
||||||
|
CREATE TABLE public.agreement_content (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT REFERENCES public.user(id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
markdown TEXT NOT NULL,
|
||||||
|
hash TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uniq__agreement_content UNIQUE NULLS NOT DISTINCT (user_id, title)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__agreement_content__user_id ON public.agreement_content (user_id);
|
||||||
|
CREATE INDEX idx__public__agreement_content__created_ts ON public.agreement_content (created_ts);
|
||||||
|
CREATE INDEX idx__public__agreement_content__updated_ts ON public.agreement_content (updated_ts);
|
||||||
1
src/db/migrations/000006 - audit-index.sql
Normal file
1
src/db/migrations/000006 - audit-index.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX idx__public__audit_signature__audit_id ON public.audit_signature (audit_id);
|
||||||
31
src/db/migrations/000007 - meta.sql
Normal file
31
src/db/migrations/000007 - meta.sql
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-- DROP TABLE IF EXISTS public.event_meta CASCADE;
|
||||||
|
CREATE TABLE public.event_meta (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES public.user(id),
|
||||||
|
event_id BIGINT NOT NULL REFERENCES public.event(id),
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__event_meta__event_id ON public.event_meta (event_id);
|
||||||
|
CREATE INDEX idx__public__event_meta__user_id ON public.event_meta (user_id);
|
||||||
|
CREATE INDEX idx__public__event_meta__key ON public.event_meta (key);
|
||||||
|
CREATE INDEX idx__public__event_meta__value ON public.event_meta (value);
|
||||||
|
CREATE INDEX idx__public__event_meta__created_ts ON public.event_meta (created_ts);
|
||||||
|
CREATE INDEX idx__public__event_meta__updated_ts ON public.event_meta (updated_ts);
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS public.audit_meta CASCADE;
|
||||||
|
CREATE TABLE public.audit_meta (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
audit_id BIGINT NOT NULL REFERENCES public.audit(audit_id),
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx__public__audit_meta__audit_id ON public.audit_meta (audit_id);
|
||||||
|
CREATE INDEX idx__public__audit_meta__key ON public.audit_meta (key);
|
||||||
|
CREATE INDEX idx__public__audit_meta__value ON public.audit_meta (value);
|
||||||
|
CREATE INDEX idx__public__audit_meta__created_ts ON public.audit_meta (created_ts);
|
||||||
|
CREATE INDEX idx__public__audit_meta__updated_ts ON public.audit_meta (updated_ts);
|
||||||
25
src/eslint.config.js
Normal file
25
src/eslint.config.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import globals from "globals";
|
||||||
|
import pluginJs from "@eslint/js";
|
||||||
|
import babelParser from "@babel/eslint-parser";
|
||||||
|
import eslintConfigPrettier from "eslint-config-prettier";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parser: babelParser, // Reference the @babel/eslint-parser directly
|
||||||
|
parserOptions: {
|
||||||
|
requireConfigFile: false, // Optional: set to false if you don't have a babel config file
|
||||||
|
babelOptions: {
|
||||||
|
plugins: ["@babel/plugin-syntax-import-assertions"], // Add Babel plugin for import assertions
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node, // Include Node.js global variables here
|
||||||
|
...globals.jest, // Jest globals
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pluginJs.configs.recommended, // ESLint recommended rules
|
||||||
|
eslintConfigPrettier, // Enable Prettier integration
|
||||||
|
];
|
||||||
47
src/http/agreements.js
Normal file
47
src/http/agreements.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { getPool } from "../db/index.js";
|
||||||
|
import { marked } from "marked";
|
||||||
|
|
||||||
|
async function getAgreementContent(userId, hash) {
|
||||||
|
let content = '';
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
let sql = `
|
||||||
|
SELECT markdown
|
||||||
|
FROM agreement_content
|
||||||
|
WHERE hash = $1
|
||||||
|
`;
|
||||||
|
let values = [hash];
|
||||||
|
if (userId) {
|
||||||
|
sql += `AND user_id = $2`;
|
||||||
|
values.push(userId);
|
||||||
|
} else {
|
||||||
|
sql += `AND user_id IS NULL`;
|
||||||
|
}
|
||||||
|
const res = await client.query(sql, values);
|
||||||
|
if (res.rows.length > 0 && res.rows[0].markdown) {
|
||||||
|
content = res.rows[0].markdown;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return marked(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function routeAgreements(app) {
|
||||||
|
|
||||||
|
app.get('/agreements/:hash', async (req, res) => {
|
||||||
|
const { hash } = req.params;
|
||||||
|
const agreement = await getAgreementContent(null, hash);
|
||||||
|
res.send(agreement);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route for /agreements/:userid/:hash
|
||||||
|
app.get('/agreements/:userId/:hash', async (req, res) => {
|
||||||
|
const { userId, hash } = req.params;
|
||||||
|
const agreement = await getAgreementContent(userId, hash);
|
||||||
|
res.send(agreement);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
108
src/http/api/v1/swagger.json
Normal file
108
src/http/api/v1/swagger.json
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"version": "1",
|
||||||
|
"title": "JLINC API",
|
||||||
|
"description": "Version 1 API for the JLINC server."
|
||||||
|
},
|
||||||
|
"basePath": "/api/v1",
|
||||||
|
"schemes": ["https"],
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Synchronization",
|
||||||
|
"description": "Operations related to the synchronization"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/sync/update": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["Synchronization"],
|
||||||
|
"summary": "Update device settings",
|
||||||
|
"description": "Updates the settings of a specified device.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Content-Type",
|
||||||
|
"in": "header",
|
||||||
|
"required": true,
|
||||||
|
"type": "string",
|
||||||
|
"default": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the device."
|
||||||
|
},
|
||||||
|
"savedTs": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "Timestamp when the settings were saved, in ISO 8601 format."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "String representing the device settings."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["deviceName", "savedTs", "settings"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful update",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates if the update was successful",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The most up to date settings data",
|
||||||
|
"example": "eyAic2V0dGluZ..."
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What action should be taken",
|
||||||
|
"enum": ["created", "none", "existingNewer", "incomingNewer"],
|
||||||
|
"example": "created"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Error in the request, such as invalid signature",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates if the update was successful",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Error message explaining what went wrong"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src/http/auth.js
Normal file
184
src/http/auth.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { getConfig } from "../common/config.js";
|
||||||
|
import { getPool } from "../db/index.js";
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
export async function apiMiddleware(req, res, next) {
|
||||||
|
let success = false;
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
const config = getConfig();
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
if (authHeader) {
|
||||||
|
const apiKey = authHeader.split(' ')[1];
|
||||||
|
if (apiKey) {
|
||||||
|
const validRes = await client.query(`
|
||||||
|
SELECT
|
||||||
|
au.user_id,
|
||||||
|
ap.type
|
||||||
|
FROM public.auth au
|
||||||
|
INNER JOIN public.app ap ON ap.id = au.app_id
|
||||||
|
WHERE au.api_key = $1
|
||||||
|
`, [
|
||||||
|
apiKey,
|
||||||
|
]);
|
||||||
|
if (validRes.rowCount > 0) {
|
||||||
|
// if (config.debug) console.log({apiKey});
|
||||||
|
req.session.user_id = validRes.rows[0].user_id;
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
if (!success)
|
||||||
|
return res.status(401).json({ error: 'API key is invalid' });
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNewKey(user) {
|
||||||
|
const seed = `${user.issuer}:${user.identifier}:${user.id}:${crypto.randomBytes(16).toString('hex')}`;
|
||||||
|
const hash = crypto.createHash('sha256').update(seed).digest();
|
||||||
|
const apiKey = hash.toString('hex');
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createApiKeys(client, user) {
|
||||||
|
const config = getConfig();
|
||||||
|
for (const type in config.appModules) {
|
||||||
|
const apiKey = getNewKey(user);
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO public.auth (
|
||||||
|
user_id,
|
||||||
|
app_id,
|
||||||
|
api_key
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
(SELECT id FROM public.app WHERE type = $2),
|
||||||
|
$3
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
|
`, [
|
||||||
|
user.id,
|
||||||
|
type,
|
||||||
|
apiKey,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkIfUserExists(client, issuer, identifier) {
|
||||||
|
return await client.query(`
|
||||||
|
SELECT
|
||||||
|
u.id,
|
||||||
|
u.photo,
|
||||||
|
u.username
|
||||||
|
FROM public.user u
|
||||||
|
WHERE u.identifier = $1
|
||||||
|
AND u.issuer = $2
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
identifier,
|
||||||
|
issuer,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUser(client, issuer, identifier) {
|
||||||
|
const res = await client.query(`
|
||||||
|
SELECT
|
||||||
|
u.id,
|
||||||
|
u.username,
|
||||||
|
u.photo,
|
||||||
|
u.issuer,
|
||||||
|
u.identifier,
|
||||||
|
json_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'id', a.id,
|
||||||
|
'type', a.type,
|
||||||
|
'apiKey', au.api_key
|
||||||
|
)
|
||||||
|
) AS apps
|
||||||
|
FROM public.user u
|
||||||
|
INNER JOIN public.auth au ON u.id = au.user_id
|
||||||
|
INNER JOIN public.app a ON au.app_id = a.id
|
||||||
|
WHERE u.identifier = $1
|
||||||
|
AND u.issuer = $2
|
||||||
|
GROUP BY
|
||||||
|
u.id,
|
||||||
|
u.username,
|
||||||
|
u.photo,
|
||||||
|
u.issuer,
|
||||||
|
u.identifier
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
identifier,
|
||||||
|
issuer,
|
||||||
|
]);
|
||||||
|
if (res.rowCount > 0) {
|
||||||
|
const user = res.rows[0];
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkUser(type, issuer, identifier, username, photo) {
|
||||||
|
const client = await getPool();
|
||||||
|
let userExists = await checkIfUserExists(client, issuer, identifier);
|
||||||
|
if (userExists.rowCount === 0) {
|
||||||
|
if (!username || username === '') {
|
||||||
|
username = identifier;
|
||||||
|
}
|
||||||
|
const res = await client.query(`
|
||||||
|
INSERT INTO public.user (
|
||||||
|
username,
|
||||||
|
issuer,
|
||||||
|
identifier,
|
||||||
|
photo,
|
||||||
|
type
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5
|
||||||
|
) RETURNING id;
|
||||||
|
`, [
|
||||||
|
username,
|
||||||
|
issuer,
|
||||||
|
identifier,
|
||||||
|
photo,
|
||||||
|
type,
|
||||||
|
]);
|
||||||
|
console.log(res.rows[0])
|
||||||
|
if (res.rowCount === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await createApiKeys(client, res.rows[0]);
|
||||||
|
} else {
|
||||||
|
if (photo !== userExists.rows[0].photo || username != userExists.rows[0].username) {
|
||||||
|
await client.query(`
|
||||||
|
UPDATE public.user SET
|
||||||
|
photo = $1,
|
||||||
|
username = $2,
|
||||||
|
updated_ts = NOW()
|
||||||
|
WHERE id = $3
|
||||||
|
`, [
|
||||||
|
userExists.rows[0].photo,
|
||||||
|
userExists.rows[0].username,
|
||||||
|
userExists.rows[0].id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await createApiKeys(client, userExists.rows[0]);
|
||||||
|
}
|
||||||
|
const user = await getUser(client, issuer, identifier);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initModules(app, passport) {
|
||||||
|
const config = getConfig();
|
||||||
|
for (const authModule of Object.keys(config.authModules)) {
|
||||||
|
const authModulePath = `../modules/auth/${authModule}.js`;
|
||||||
|
const { initModule } = await import(authModulePath);
|
||||||
|
await initModule(app, passport);
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/http/index.js
Normal file
91
src/http/index.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import bodyParser from "body-parser";
|
||||||
|
import { initModules, apiMiddleware } from "./auth.js";
|
||||||
|
import swaggerUi from "swagger-ui-express";
|
||||||
|
import swaggerDocument from "./api/v1/swagger.json" assert { type: "json" };
|
||||||
|
import { getConfig } from "../common/config.js";
|
||||||
|
import { core } from "../modules/core/index.js"
|
||||||
|
|
||||||
|
import express from "express";
|
||||||
|
import session from "express-session";
|
||||||
|
import MemoryStore from "memorystore";
|
||||||
|
import passport from "passport";
|
||||||
|
import { refresh } from "./refresh.js";
|
||||||
|
import { logout } from "./logout.js";
|
||||||
|
import { logRequest } from "./logging.js";
|
||||||
|
import { routeAgreements } from "./agreements.js";
|
||||||
|
import { getUsage } from "../modules/core/usage.js";
|
||||||
|
|
||||||
|
async function render(view, res, config) {
|
||||||
|
try {
|
||||||
|
res.render(view, {
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderPrivate(view, req, res, config) {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
return res.redirect('/');
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
const begin = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||||
|
const usage = await getUsage(req.user, begin, end);
|
||||||
|
res.render(view, {
|
||||||
|
config,
|
||||||
|
user: req.user,
|
||||||
|
usage,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initHTTP(app) {
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
passport.serializeUser(function (user, done) {
|
||||||
|
done(null, user);
|
||||||
|
});
|
||||||
|
passport.deserializeUser(function (user, done) {
|
||||||
|
done(null, user);
|
||||||
|
});
|
||||||
|
|
||||||
|
const memoryStore = MemoryStore(session);
|
||||||
|
const sess = {
|
||||||
|
secret: config.secureSecret,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: new memoryStore({
|
||||||
|
checkPeriod: 86400000 // prune expired entries every 24h
|
||||||
|
}),
|
||||||
|
cookie: {},
|
||||||
|
}
|
||||||
|
if (app.get('env') === 'production') {
|
||||||
|
app.set('trust proxy', 1) // trust first proxy
|
||||||
|
sess.cookie.secure = true // serve secure cookies
|
||||||
|
}
|
||||||
|
app.use(session(sess));
|
||||||
|
app.use(passport.initialize());
|
||||||
|
app.use(passport.session());
|
||||||
|
app.use(express.static("./http/public"));
|
||||||
|
|
||||||
|
app.set('views', './http/views');
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
|
await initModules(app, passport);
|
||||||
|
logRequest(app);
|
||||||
|
routeAgreements(app);
|
||||||
|
|
||||||
|
app.get("/", (req, res) => render('login', res, config));
|
||||||
|
app.get("/dashboard", (req, res) => renderPrivate('dashboard', req, res, config));
|
||||||
|
app.get("/refresh", refresh);
|
||||||
|
app.post('/logout', logout);
|
||||||
|
|
||||||
|
app.post("/api/v1/*", bodyParser.json(), apiMiddleware, core.post);
|
||||||
|
|
||||||
|
// app.use("/api/v1", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
||||||
|
}
|
||||||
40
src/http/logging.js
Normal file
40
src/http/logging.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { getConfig } from "../common/config.js";
|
||||||
|
|
||||||
|
export function logRequest(app) {
|
||||||
|
const config = getConfig();
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const originalSend = res.send;
|
||||||
|
res.send = function (body) {
|
||||||
|
res.body = body; // Store the response body for logging
|
||||||
|
try {
|
||||||
|
return originalSend.apply(res, arguments); // Proceed with sending the response
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
res.on("finish", () => {
|
||||||
|
let output = `${req.method} - ${res.statusCode} - ${req.url}`;
|
||||||
|
if (req.apiMessage) {
|
||||||
|
output += ` - ${req.apiMessage}`;
|
||||||
|
delete req.apiMessage;
|
||||||
|
}
|
||||||
|
console.log(output);
|
||||||
|
if (res.statusCode != 200 || config.debug) {
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = JSON.parse(res.body);
|
||||||
|
} catch (e) {
|
||||||
|
body = {};
|
||||||
|
}
|
||||||
|
if (body?.error) {
|
||||||
|
console.log(`${req.method} - ${res.statusCode} - ${req.url} - ERROR: ${body.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (config.debug) {
|
||||||
|
console.log(JSON.stringify(body, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
22
src/http/logout.js
Normal file
22
src/http/logout.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { getConfig } from "../common/config.js";
|
||||||
|
|
||||||
|
export async function logout(req, res, next) {
|
||||||
|
const strategy = req.session.authStrategy ?`${req.session.authStrategy}` : null;
|
||||||
|
req.logout(function (err) {
|
||||||
|
if (err) { return next(err); }
|
||||||
|
req.session.destroy(function (err) {
|
||||||
|
if (err) { return next(err); }
|
||||||
|
if (strategy) {
|
||||||
|
const config = getConfig();
|
||||||
|
const strategyConfig = config.authModules[strategy];
|
||||||
|
if (strategyConfig && strategyConfig.logoutURL) {
|
||||||
|
res.redirect(strategyConfig.logoutURL);
|
||||||
|
} else {
|
||||||
|
res.redirect('/');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.redirect('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
18
src/http/public/images/icon.svg
Normal file
18
src/http/public/images/icon.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100.47 100.47">
|
||||||
|
<defs>
|
||||||
|
<style>.cls-1{fill:#fff;}.cls-2{fill:#31a9ba;}</style>
|
||||||
|
</defs>
|
||||||
|
<title>JLINC Icon</title>
|
||||||
|
<g id="Layer_2" data-name="Layer 2">
|
||||||
|
<g id="Layer_1-2" data-name="Layer 1">
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M8.52,41.22l9,9L36.46,69.16l7.76-7.76L25.3,42.48l-9.42-9.42q-4.5-4.52-5.14-9.26c-.48-3.11,1-6.36,4.35-9.73Q19.22,9.95,23.64,10t9.26,4.83l4,4,7.2-7.21L38.52,6a20.93,20.93,0,0,0-7.91-4.9A17.1,17.1,0,0,0,20,.61Q14,2,8.05,8a28.56,28.56,0,0,0-6.33,9.5,18.2,18.2,0,0,0-.79,11.4Q2.34,35,8.52,41.22Z" />
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M61.4,56.25,42.48,75.17l-9.42,9.42q-4.52,4.51-9.26,5.14t-9.73-4.35Q9.95,81.27,10,76.83t4.83-9.26l4-3.95-7.21-7.21L6,62a21,21,0,0,0-4.9,7.92A17.09,17.09,0,0,0,.61,80.48q1.43,6,7.36,12a28.87,28.87,0,0,0,9.5,6.33,18.2,18.2,0,0,0,11.4.79Q35,98.13,41.22,92l9-9L69.16,64Z" />
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M92,59.26l-9-9L64,31.31l-7.76,7.76L75.17,58l9.42,9.42q4.51,4.52,5.14,9.26t-4.35,9.74c-2.74,2.74-5.59,4.12-8.55,4.11s-6-1.55-9.26-4.83l-3.95-4-7.21,7.2L62,94.48a21.11,21.11,0,0,0,7.92,4.91,17.06,17.06,0,0,0,10.6.47q6-1.42,12-7.36A28.87,28.87,0,0,0,98.76,83a18.16,18.16,0,0,0,.79-11.39Q98.13,65.43,92,59.26Z" />
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M39.07,44.22,58,25.3l9.42-9.42q4.52-4.5,9.26-5.14c3.12-.48,6.36,1,9.74,4.35,2.74,2.75,4.11,5.59,4.11,8.55s-1.55,6-4.83,9.26l-4,4,7.2,7.2,5.54-5.54a21.07,21.07,0,0,0,4.91-7.91A17.17,17.17,0,0,0,99.86,20Q98.44,14,92.5,8.05A28.56,28.56,0,0,0,83,1.72,18.19,18.19,0,0,0,71.6.93Q65.44,2.34,59.26,8.52l-9,9L31.31,36.46Z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
28
src/http/refresh.js
Normal file
28
src/http/refresh.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { getPool } from "../db/index.js";
|
||||||
|
import { getNewKey, getUser } from "./auth.js";
|
||||||
|
|
||||||
|
export async function refresh(req, res) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const client = await getPool();
|
||||||
|
const apiKey = getNewKey(req.user);
|
||||||
|
await client.query(`
|
||||||
|
UPDATE public.auth SET
|
||||||
|
api_key = $1
|
||||||
|
WHERE user_id = $2
|
||||||
|
AND app_id = (
|
||||||
|
SELECT id FROM public.app WHERE type = $3
|
||||||
|
);
|
||||||
|
`, [
|
||||||
|
apiKey,
|
||||||
|
req.user.id,
|
||||||
|
req.query.app,
|
||||||
|
]);
|
||||||
|
const user = await getUser(client, req.user.issuer, req.user.identifier);
|
||||||
|
req.session.passport.user = user;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
res.redirect('/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/http/views/dashboard.ejs
Normal file
15
src/http/views/dashboard.ejs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<%- include('./include/header.ejs', { title: 'JLINC - Dashboard' }) %>
|
||||||
|
|
||||||
|
<% for (const type in config.appModules) { %>
|
||||||
|
<%-
|
||||||
|
include('./include/app.ejs', {
|
||||||
|
app: config.appModules[type],
|
||||||
|
usage: usage ? usage[type] : null
|
||||||
|
})
|
||||||
|
%>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<%- include('./include/footer.ejs') %>
|
||||||
163
src/http/views/include/app.ejs
Normal file
163
src/http/views/include/app.ejs
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<% const cardStyle=`"background: linear-gradient(333deg, ${app.background.color1} 0%, ${app.background.color2} 100%);
|
||||||
|
margin-bottom: 30px;"`; const userApp=user.apps.find(ua=> app.type === ua.type);
|
||||||
|
const buttonStyle = `"padding: 8px 10px 8px 10px; border-radius: 16px !important; background-color:
|
||||||
|
${app.button.color} !important; border: none !important;"`
|
||||||
|
%>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyToClipboard(label, text) {
|
||||||
|
if (window.clipboardData && window.clipboardData.setData) {
|
||||||
|
// Internet Explorer specific code path to prevent textarea being shown while dialog is visible.
|
||||||
|
return clipboardData.setData('Text', text);
|
||||||
|
} else if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
|
||||||
|
var textarea = document.createElement("textarea");
|
||||||
|
textarea.textContent = text;
|
||||||
|
// Prevent scrolling to bottom of page in Microsoft Edge.
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
const flash = document.createElement('div');
|
||||||
|
flash.style.position = 'fixed';
|
||||||
|
flash.style.top = '0';
|
||||||
|
flash.style.left = '0';
|
||||||
|
flash.style.width = '100%';
|
||||||
|
flash.style.backgroundColor = '#000';
|
||||||
|
flash.style.color = '#ggg';
|
||||||
|
flash.style.opacity = '0.7';
|
||||||
|
flash.style.textAlign = 'center';
|
||||||
|
flash.style.fontSize = '12px';
|
||||||
|
flash.style.padding = '10px';
|
||||||
|
try {
|
||||||
|
const cmd = document.execCommand('copy'); // Security exception may be thrown by some browsers.
|
||||||
|
flash.innerHTML = `${label} copied to clipboard`;
|
||||||
|
document.body.appendChild(flash);
|
||||||
|
setTimeout(function () {
|
||||||
|
document.body.removeChild(flash);
|
||||||
|
}, 1500);
|
||||||
|
return cmd;
|
||||||
|
} catch (ex) {
|
||||||
|
flash.style.backgroundColor = '#f00';
|
||||||
|
flash.innerHTML = 'API key copy failed';
|
||||||
|
document.body.appendChild(flash);
|
||||||
|
setTimeout(function () {
|
||||||
|
document.body.removeChild(flash);
|
||||||
|
}, 1500);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mdc-card mdc-theme--dark" style=<%- cardStyle %>>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<div style="width: 30px">
|
||||||
|
<%- app.logo %>
|
||||||
|
</div>
|
||||||
|
<h2 style="margin-left: 10px; padding-bottom: 10px;">
|
||||||
|
<%= app.title %>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="mdc-text-field mdc-text-field--outlined mdc-text-field--focused">
|
||||||
|
<span class="mdc-notched-outline" style="--mdc-theme-primary: rgba(255, 255, 255, 0.3)">
|
||||||
|
<span class="mdc-notched-outline__leading"></span>
|
||||||
|
<span class="mdc-notched-outline__trailing"></span>
|
||||||
|
</span>
|
||||||
|
<input style="color: #fff; text-overflow: ellipsis;" type="text" id="endpoint-input"
|
||||||
|
aria-describedby="api-key-helper" class="mdc-text-field__input" disabled type="text"
|
||||||
|
value="<%= app.endpoint %>">
|
||||||
|
<i style="color: #fff" class="material-icons mdc-text-field__icon mdc-text-field__icon--trailing"
|
||||||
|
tabindex="0" role="button" onclick="copyToClipboard('API Endpoint', '<%= app.endpoint %>')">
|
||||||
|
content_copy
|
||||||
|
</i>
|
||||||
|
</label>
|
||||||
|
<div class="mdc-text-field-helper-line" style="padding-bottom: 20px">
|
||||||
|
<div style="color: #fff" class="mdc-text-field-helper-text" id="endpoint-helper" aria-hidden="false">API
|
||||||
|
Endpoint
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="mdc-text-field mdc-text-field--outlined mdc-text-field--focused">
|
||||||
|
<span class="mdc-notched-outline" style="--mdc-theme-primary: rgba(255, 255, 255, 0.3)">
|
||||||
|
<span class="mdc-notched-outline__leading"></span>
|
||||||
|
<span class="mdc-notched-outline__trailing"></span>
|
||||||
|
</span>
|
||||||
|
<input style="color: #fff; text-overflow: ellipsis;" type="text" id="api-key-input"
|
||||||
|
aria-describedby="api-key-helper" class="mdc-text-field__input" disabled type="text"
|
||||||
|
value="<%= userApp.apiKey %>">
|
||||||
|
<i style="color: #fff" class="material-icons mdc-text-field__icon mdc-text-field__icon--trailing"
|
||||||
|
tabindex="0" role="button" onclick="copyToClipboard('API Key', '<%= userApp.apiKey %>')">
|
||||||
|
content_copy
|
||||||
|
</i>
|
||||||
|
</label>
|
||||||
|
<div class="mdc-text-field-helper-line">
|
||||||
|
<div style="color: #fff" class="mdc-text-field-helper-text" id="api-key-helper" aria-hidden="false">API Key
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: flex-end;">
|
||||||
|
<button style="width: 140px; --mdc-theme-primary: <%= app.button.color %>"
|
||||||
|
class="mdc-button mdc-button--raised mdc-button--leading"
|
||||||
|
onclick="window.location.href='/refresh?app=<%= app.type %>'">
|
||||||
|
<span class="mdc-button__ripple"></span>
|
||||||
|
<i class="material-icons mdc-button__icon" aria-hidden="true">refresh</i>
|
||||||
|
<span class="mdc-button__label">Refresh</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (usage) { %>
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<h3 style="margin: 0; display: flex; align-items: center; margin-right: 16px;">Hits this month:</h3>
|
||||||
|
<span class="mdc-evolution-chip-set" role="grid">
|
||||||
|
<span class="mdc-evolution-chip-set__chips" role="presentation">
|
||||||
|
<span class="mdc-evolution-chip" role="row">
|
||||||
|
<span class="mdc-evolution-chip__cell mdc-evolution-chip__cell--primary" role="gridcell">
|
||||||
|
<button style=<%- buttonStyle %>
|
||||||
|
class="mdc-evolution-chip__action mdc-evolution-chip__action--primary" type="button"
|
||||||
|
tabindex="0">
|
||||||
|
<span class="mdc-evolution-chip__ripple mdc-evolution-chip__ripple--primary"></span>
|
||||||
|
<span class="mdc-evolution-chip__text-label">
|
||||||
|
<%= usage %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
<% if (userApp.devices?.length> 0) { %>
|
||||||
|
<h3 style="margin-bottom: 0px">Synced devices</h3>
|
||||||
|
<span class="mdc-evolution-chip-set" role="grid" style="padding-top: 16px">
|
||||||
|
<span class="mdc-evolution-chip-set__chips" role="presentation">
|
||||||
|
|
||||||
|
<% for (const device of userApp.devices.sort((a, b)=>
|
||||||
|
a.identifier.localeCompare(b.identifier))) {
|
||||||
|
%>
|
||||||
|
<span style="padding-right: 10px" class="mdc-evolution-chip" role="row"
|
||||||
|
id="device-<%- device.id %>">
|
||||||
|
<span class="mdc-evolution-chip__cell mdc-evolution-chip__cell--primary"
|
||||||
|
role="gridcell">
|
||||||
|
<button style=<%- buttonStyle %> class="mdc-evolution-chip__action
|
||||||
|
mdc-evolution-chip__action--primary"
|
||||||
|
type="button" tabindex="0">
|
||||||
|
<span
|
||||||
|
class="mdc-evolution-chip__ripple mdc-evolution-chip__ripple--primary"></span>
|
||||||
|
<span class="mdc-evolution-chip__text-label">
|
||||||
|
<%= device.identifier %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
4
src/http/views/include/footer.ejs
Normal file
4
src/http/views/include/footer.ejs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
118
src/http/views/include/header.ejs
Normal file
118
src/http/views/include/header.ejs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<head>
|
||||||
|
<title>
|
||||||
|
<%= title %>
|
||||||
|
</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="/images/icon.svg" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
<script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--md-ref-typeface-brand: 'Open Sans', sans-serif;
|
||||||
|
--md-ref-typeface-plain: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--md-ref-typeface-plain);
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
background-image: linear-gradient(135deg, #31A9BA 10%, #0E0618 10%, #231641 90%, #31A9BA 90%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
font-family: var(--md-ref-typeface-plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
font-family: var(--md-ref-typeface-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-card {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
background-color: rgb(68, 62, 77);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-button {
|
||||||
|
--mdc-theme-primary: rgb(255, 255, 255, .85);
|
||||||
|
--mdc-theme-on-primary: rgb(68, 62, 77);
|
||||||
|
|
||||||
|
/* @include button.ink-color(#84565E); */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="profile-icon-container" style="position: absolute; top: 16px; right: 16px;">
|
||||||
|
<% if (typeof user !=='undefined' && user) { %>
|
||||||
|
<div style="position: relative;">
|
||||||
|
<button id="profileButton" class="profile-icon"
|
||||||
|
style="border: none; border-radius: 50%; width: 48px; height: 48px; display: flex; justify-content: center; align-items: center; background-color: #31A9BA; color: #fff; cursor: pointer;">
|
||||||
|
<span style="font-size: 30px">
|
||||||
|
<%= user.username.charAt(0).toUpperCase() %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="profileMenu"
|
||||||
|
style="display: none; position: absolute; top: 60px; right: 0; background-color: #4F378B; box-shadow: 0 2px 8px rgba(0,0,0,0.15); border-radius: 8px; padding: 10px; min-width: 160px; z-index: 100;">
|
||||||
|
<div style="padding: 8px; font-weight: bold;">
|
||||||
|
<span style="display: block; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||||
|
<%= user.username %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="/logout">
|
||||||
|
<button type="submit"
|
||||||
|
style="width: 100%; padding: 8px; background-color: #31A9BA; color: white; border: none; border-radius: 4px; cursor: pointer;">Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const profileButton = document.getElementById('profileButton');
|
||||||
|
const profileMenu = document.getElementById('profileMenu');
|
||||||
|
|
||||||
|
profileButton.addEventListener('click', () => {
|
||||||
|
profileMenu.style.display = profileMenu.style.display === 'block' ? 'none' : 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optional: Hide menu if clicking outside
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
if (!profileButton.contains(event.target) && !profileMenu.contains(event.target)) {
|
||||||
|
profileMenu.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<div style="width: 90%; max-width: 600px;">
|
||||||
|
<div
|
||||||
|
style="display: flex; align-items: center; justify-content: center; gap: 0px; text-align: center; transform: translateX(-0px);">
|
||||||
|
<div style="width: 200px; padding-bottom: 26px">
|
||||||
|
<%- include('./logo-white.svg') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
1
src/http/views/include/logo-white.svg
Normal file
1
src/http/views/include/logo-white.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 362.08 100.47"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#31a9ba;}</style></defs><title>JLINC Logo H White</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M157.32,29.36h8.35V57.2q0,13.9-13.92,13.91H137.84q-14.22,0-14-13.91l0-1.4h8.35v.25q0,6.72,6.68,6.71h11.66q6.7,0,6.71-6.73Z"/><path class="cls-1" d="M223.12,62.76v8.35H181.37V29.36h8.35v33.4Z"/><path class="cls-1" d="M247.18,71.11h-8.35V29.36h8.35Z"/><path class="cls-1" d="M271.23,42V71.11h-8.35V29.36h8.51l24.89,29.09V29.36h8.35V71.11h-8.49Z"/><path class="cls-1" d="M362.08,62.76v8.35H334.25q-13.92,0-13.92-13.91V43.28q0-13.92,13.92-13.92h27.83v8.35H335.39q-6.7,0-6.71,6.68V56.05q0,6.72,6.74,6.71Z"/><path class="cls-2" d="M8.52,41.22l9,9L36.46,69.16l7.76-7.76L25.3,42.48l-9.42-9.42q-4.5-4.52-5.14-9.26c-.48-3.11,1-6.36,4.35-9.73Q19.22,9.95,23.64,10t9.26,4.83l4,4,7.2-7.21L38.52,6a20.93,20.93,0,0,0-7.91-4.9A17.1,17.1,0,0,0,20,.61Q14,2,8.05,8a28.56,28.56,0,0,0-6.33,9.5,18.2,18.2,0,0,0-.79,11.4Q2.34,35,8.52,41.22Z"/><path class="cls-2" d="M61.4,56.25,42.48,75.17l-9.42,9.42q-4.52,4.51-9.26,5.14t-9.73-4.35Q9.95,81.27,10,76.83t4.83-9.26l4-3.95-7.21-7.21L6,62a21,21,0,0,0-4.9,7.92A17.09,17.09,0,0,0,.61,80.48q1.43,6,7.36,12a28.87,28.87,0,0,0,9.5,6.33,18.2,18.2,0,0,0,11.4.79Q35,98.13,41.22,92l9-9L69.16,64Z"/><path class="cls-2" d="M92,59.26l-9-9L64,31.31l-7.76,7.76L75.17,58l9.42,9.42q4.51,4.52,5.14,9.26t-4.35,9.74c-2.74,2.74-5.59,4.12-8.55,4.11s-6-1.55-9.26-4.83l-3.95-4-7.21,7.2L62,94.48a21.11,21.11,0,0,0,7.92,4.91,17.06,17.06,0,0,0,10.6.47q6-1.42,12-7.36A28.87,28.87,0,0,0,98.76,83a18.16,18.16,0,0,0,.79-11.39Q98.13,65.43,92,59.26Z"/><path class="cls-2" d="M39.07,44.22,58,25.3l9.42-9.42q4.52-4.5,9.26-5.14c3.12-.48,6.36,1,9.74,4.35,2.74,2.75,4.11,5.59,4.11,8.55s-1.55,6-4.83,9.26l-4,4,7.2,7.2,5.54-5.54a21.07,21.07,0,0,0,4.91-7.91A17.17,17.17,0,0,0,99.86,20Q98.44,14,92.5,8.05A28.56,28.56,0,0,0,83,1.72,18.19,18.19,0,0,0,71.6.93Q65.44,2.34,59.26,8.52l-9,9L31.31,36.46Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
21
src/http/views/login.ejs
Normal file
21
src/http/views/login.ejs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<%- include('./include/header.ejs', { title: 'JLINC - Login' }) %>
|
||||||
|
|
||||||
|
<div class="mdc-card" style="background: linear-gradient(333deg, rgb(0, 0, 0) 0%, rgb(79, 55, 139) 100%);">
|
||||||
|
|
||||||
|
<h3>Login with:</h3>
|
||||||
|
<% for (const type in config.authModules) { %>
|
||||||
|
<div style="width: 100%; padding-bottom: 16px">
|
||||||
|
<button style="width: 100%" class="mdc-button mdc-button--raised mdc-button--leading" onclick="window.location.href='/login/<%= type %>'">
|
||||||
|
<span class="mdc-button__ripple"></span>
|
||||||
|
<i class="material-icons mdc-button__icon" aria-hidden="true"><%= config.authModules[type].icon %></i>
|
||||||
|
<span class="mdc-button__label"><%= config.authModules[type].title %></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%- include('./include/footer.ejs') %>
|
||||||
24
src/index.js
Normal file
24
src/index.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import express from "express";
|
||||||
|
import { init, close, migrate, populateAgreements } from "./db/index.js";
|
||||||
|
import { loadConfig } from "./common/config.js";
|
||||||
|
import { loadApps } from "./apps.js";
|
||||||
|
import { initHTTP } from "./http/index.js";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.set("env", "development");
|
||||||
|
app.use(express.static("./public"));
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await loadConfig();
|
||||||
|
await init();
|
||||||
|
await migrate();
|
||||||
|
await populateAgreements();
|
||||||
|
await loadApps();
|
||||||
|
await initHTTP(app);
|
||||||
|
const server = await app.listen(9090, () => {
|
||||||
|
console.log(`Listening on 0.0.0.0:9090`);
|
||||||
|
});
|
||||||
|
server.on("beforeExit", close);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
41
src/modules/app/archive.js
Normal file
41
src/modules/app/archive.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { getConfig } from "../../common/config.js";
|
||||||
|
|
||||||
|
const key = 'archive';
|
||||||
|
|
||||||
|
const logo = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100.47 100.47">
|
||||||
|
<defs>
|
||||||
|
<style>.cls-1{fill:#fff;}.cls-2{fill:#31a9ba;}</style>
|
||||||
|
</defs>
|
||||||
|
<title>JLINC Icon</title>
|
||||||
|
<g id="Layer_2" data-name="Layer 2">
|
||||||
|
<g id="Layer_1-2" data-name="Layer 1">
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M8.52,41.22l9,9L36.46,69.16l7.76-7.76L25.3,42.48l-9.42-9.42q-4.5-4.52-5.14-9.26c-.48-3.11,1-6.36,4.35-9.73Q19.22,9.95,23.64,10t9.26,4.83l4,4,7.2-7.21L38.52,6a20.93,20.93,0,0,0-7.91-4.9A17.1,17.1,0,0,0,20,.61Q14,2,8.05,8a28.56,28.56,0,0,0-6.33,9.5,18.2,18.2,0,0,0-.79,11.4Q2.34,35,8.52,41.22Z" />
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M61.4,56.25,42.48,75.17l-9.42,9.42q-4.52,4.51-9.26,5.14t-9.73-4.35Q9.95,81.27,10,76.83t4.83-9.26l4-3.95-7.21-7.21L6,62a21,21,0,0,0-4.9,7.92A17.09,17.09,0,0,0,.61,80.48q1.43,6,7.36,12a28.87,28.87,0,0,0,9.5,6.33,18.2,18.2,0,0,0,11.4.79Q35,98.13,41.22,92l9-9L69.16,64Z" />
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M92,59.26l-9-9L64,31.31l-7.76,7.76L75.17,58l9.42,9.42q4.51,4.52,5.14,9.26t-4.35,9.74c-2.74,2.74-5.59,4.12-8.55,4.11s-6-1.55-9.26-4.83l-3.95-4-7.21,7.2L62,94.48a21.11,21.11,0,0,0,7.92,4.91,17.06,17.06,0,0,0,10.6.47q6-1.42,12-7.36A28.87,28.87,0,0,0,98.76,83a18.16,18.16,0,0,0,.79-11.39Q98.13,65.43,92,59.26Z" />
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M39.07,44.22,58,25.3l9.42-9.42q4.52-4.5,9.26-5.14c3.12-.48,6.36,1,9.74,4.35,2.74,2.75,4.11,5.59,4.11,8.55s-1.55,6-4.83,9.26l-4,4,7.2,7.2,5.54-5.54a21.07,21.07,0,0,0,4.91-7.91A17.17,17.17,0,0,0,99.86,20Q98.44,14,92.5,8.05A28.56,28.56,0,0,0,83,1.72,18.19,18.19,0,0,0,71.6.93Q65.44,2.34,59.26,8.52l-9,9L31.31,36.46Z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
export function getModuleConfig() {
|
||||||
|
const config = getConfig();
|
||||||
|
return {
|
||||||
|
title: 'JLINC Archive/Audit',
|
||||||
|
logo,
|
||||||
|
type: key,
|
||||||
|
endpoint: config.publicArchiveUrl,
|
||||||
|
background: {
|
||||||
|
color1: 'rgb(79, 55, 139)',
|
||||||
|
color2: 'rgb(19, 2, 28)',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
color: 'rgb(255, 205, 57)',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/modules/app/core.js
Normal file
41
src/modules/app/core.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { getConfig } from "../../common/config.js";
|
||||||
|
|
||||||
|
const key = 'core';
|
||||||
|
|
||||||
|
const logo = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100.47 100.47">
|
||||||
|
<defs>
|
||||||
|
<style>.cls-1{fill:#fff;}.cls-2{fill:#31a9ba;}</style>
|
||||||
|
</defs>
|
||||||
|
<title>JLINC Icon</title>
|
||||||
|
<g id="Layer_2" data-name="Layer 2">
|
||||||
|
<g id="Layer_1-2" data-name="Layer 1">
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M8.52,41.22l9,9L36.46,69.16l7.76-7.76L25.3,42.48l-9.42-9.42q-4.5-4.52-5.14-9.26c-.48-3.11,1-6.36,4.35-9.73Q19.22,9.95,23.64,10t9.26,4.83l4,4,7.2-7.21L38.52,6a20.93,20.93,0,0,0-7.91-4.9A17.1,17.1,0,0,0,20,.61Q14,2,8.05,8a28.56,28.56,0,0,0-6.33,9.5,18.2,18.2,0,0,0-.79,11.4Q2.34,35,8.52,41.22Z" />
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M61.4,56.25,42.48,75.17l-9.42,9.42q-4.52,4.51-9.26,5.14t-9.73-4.35Q9.95,81.27,10,76.83t4.83-9.26l4-3.95-7.21-7.21L6,62a21,21,0,0,0-4.9,7.92A17.09,17.09,0,0,0,.61,80.48q1.43,6,7.36,12a28.87,28.87,0,0,0,9.5,6.33,18.2,18.2,0,0,0,11.4.79Q35,98.13,41.22,92l9-9L69.16,64Z" />
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M92,59.26l-9-9L64,31.31l-7.76,7.76L75.17,58l9.42,9.42q4.51,4.52,5.14,9.26t-4.35,9.74c-2.74,2.74-5.59,4.12-8.55,4.11s-6-1.55-9.26-4.83l-3.95-4-7.21,7.2L62,94.48a21.11,21.11,0,0,0,7.92,4.91,17.06,17.06,0,0,0,10.6.47q6-1.42,12-7.36A28.87,28.87,0,0,0,98.76,83a18.16,18.16,0,0,0,.79-11.39Q98.13,65.43,92,59.26Z" />
|
||||||
|
<path class="cls-2"
|
||||||
|
d="M39.07,44.22,58,25.3l9.42-9.42q4.52-4.5,9.26-5.14c3.12-.48,6.36,1,9.74,4.35,2.74,2.75,4.11,5.59,4.11,8.55s-1.55,6-4.83,9.26l-4,4,7.2,7.2,5.54-5.54a21.07,21.07,0,0,0,4.91-7.91A17.17,17.17,0,0,0,99.86,20Q98.44,14,92.5,8.05A28.56,28.56,0,0,0,83,1.72,18.19,18.19,0,0,0,71.6.93Q65.44,2.34,59.26,8.52l-9,9L31.31,36.46Z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
export function getModuleConfig() {
|
||||||
|
const config = getConfig();
|
||||||
|
return {
|
||||||
|
title: 'JLINC Protocol',
|
||||||
|
logo,
|
||||||
|
type: key,
|
||||||
|
endpoint: config.publicCoreUrl,
|
||||||
|
background: {
|
||||||
|
color1: 'rgb(79, 55, 139)',
|
||||||
|
color2: 'rgb(19, 2, 28)',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
color: 'rgb(255, 205, 57)',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/modules/auth/github.js
Normal file
46
src/modules/auth/github.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import GitHubStrategy from "passport-github";
|
||||||
|
import { checkUser } from "../../http/auth.js";
|
||||||
|
import { getConfig } from "../../common/config.js";
|
||||||
|
|
||||||
|
const key = 'github';
|
||||||
|
|
||||||
|
export function getModuleConfig() {
|
||||||
|
const config = getConfig();
|
||||||
|
return {
|
||||||
|
title: 'GitHub',
|
||||||
|
icon: 'code',
|
||||||
|
clientID: process.env.GITHUB_CLIENT_ID,
|
||||||
|
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||||
|
callbackURL: `${config.publicCallbackUrl}/callback/${key}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initModule(app, passport) {
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
passport.use(new GitHubStrategy(
|
||||||
|
config.authModules[key],
|
||||||
|
function (accessToken, refreshToken, profile, cb) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const user = await checkUser(key, 'https://github.com', profile.id, profile.username);
|
||||||
|
return cb(null, user);
|
||||||
|
} catch (e) {
|
||||||
|
if (config.debug)
|
||||||
|
console.error(e);
|
||||||
|
return cb(null, null);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
app.get(`/login/${key}`, passport.authenticate('github'));
|
||||||
|
app.get(`/callback/${key}`,
|
||||||
|
passport.authenticate('github', {
|
||||||
|
failureRedirect: '/',
|
||||||
|
failureMessage: true
|
||||||
|
}),
|
||||||
|
function (req, res) {
|
||||||
|
res.redirect('/dashboard');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/modules/auth/google.js
Normal file
46
src/modules/auth/google.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import GoogleStrategy from "passport-google-oauth20";
|
||||||
|
import { checkUser } from "../../http/auth.js";
|
||||||
|
import { getConfig } from "../../common/config.js";
|
||||||
|
|
||||||
|
const key = 'google';
|
||||||
|
|
||||||
|
export function getModuleConfig() {
|
||||||
|
const config = getConfig();
|
||||||
|
return {
|
||||||
|
title: 'Google',
|
||||||
|
icon: 'language',
|
||||||
|
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
callbackURL: `${config.publicCallbackUrl}/callback/${key}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initModule(app, passport) {
|
||||||
|
const config = getConfig();
|
||||||
|
passport.use(new GoogleStrategy.Strategy(
|
||||||
|
config.authModules[key],
|
||||||
|
function (accessToken, refreshToken, profile, cb) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const photo = profile.photos?.length > 0 ? profile.photos[0].value : null;
|
||||||
|
const user = await checkUser(key, 'https://google.com', profile.id, profile.displayName, photo);
|
||||||
|
return cb(null, user);
|
||||||
|
} catch (e) {
|
||||||
|
if (config.debug)
|
||||||
|
console.error(e);
|
||||||
|
return cb(null, null);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
app.get(`/login/${key}`, passport.authenticate('google', { scope: ['profile'] }));
|
||||||
|
app.get(`/callback/${key}`,
|
||||||
|
passport.authenticate('google', {
|
||||||
|
failureRedirect: '/',
|
||||||
|
failureMessage: true
|
||||||
|
}),
|
||||||
|
function (req, res) {
|
||||||
|
res.redirect('/dashboard');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/modules/auth/oidc.js
Normal file
52
src/modules/auth/oidc.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import OpenIDConnectStrategy from "passport-openidconnect";
|
||||||
|
import { checkUser } from "../../http/auth.js";
|
||||||
|
import { getConfig } from "../../common/config.js";
|
||||||
|
|
||||||
|
const key = 'oidc';
|
||||||
|
|
||||||
|
export function getModuleConfig() {
|
||||||
|
const config = getConfig();
|
||||||
|
return {
|
||||||
|
title: 'FedID',
|
||||||
|
icon: 'login',
|
||||||
|
issuer: process.env.OIDC_ISSUER,
|
||||||
|
authorizationURL: process.env.OIDC_AUTHORIZATION_URL,
|
||||||
|
tokenURL: process.env.OIDC_TOKEN_URL,
|
||||||
|
userInfoURL: process.env.OIDC_USERINFO_URL,
|
||||||
|
logoutURL: process.env.OIDC_LOGOUT_URL,
|
||||||
|
clientID: process.env.OIDC_CLIENT_ID,
|
||||||
|
clientSecret: process.env.OIDC_CLIENT_SECRET,
|
||||||
|
callbackURL: `${config.publicCallbackUrl}/callback/${key}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initModule(app, passport) {
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
passport.use(new OpenIDConnectStrategy(
|
||||||
|
config.authModules[key],
|
||||||
|
function verify(issuer, profile, cb) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const user = await checkUser(key, issuer, profile.id, profile.username);
|
||||||
|
return cb(null, user);
|
||||||
|
} catch (e) {
|
||||||
|
if (config.debug)
|
||||||
|
console.error(e);
|
||||||
|
return cb(null, null);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
app.get(`/login/${key}`,passport.authenticate('openidconnect'));
|
||||||
|
app.get(`/callback/${key}`,
|
||||||
|
passport.authenticate('openidconnect', {
|
||||||
|
failureRedirect: '/',
|
||||||
|
failureMessage: true
|
||||||
|
}),
|
||||||
|
function (req, res) {
|
||||||
|
req.session.authStrategy = 'oidc';
|
||||||
|
res.redirect('/dashboard');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/modules/core/agreement.js
Normal file
36
src/modules/core/agreement.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import pkg from '@jlinc/core';
|
||||||
|
const { JlincAgreement } = pkg;
|
||||||
|
|
||||||
|
|
||||||
|
async function create(input) {
|
||||||
|
const data = await JlincAgreement.create(input);
|
||||||
|
const message = data?.agreementId;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sign(input) {
|
||||||
|
const data = await JlincAgreement.sign(input);
|
||||||
|
const message = data?.agreement?.agreementId;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send(input) {
|
||||||
|
const data = await JlincAgreement.send(input);
|
||||||
|
const message = data?.agreement?.agreementId;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const agreement = {
|
||||||
|
create,
|
||||||
|
sign,
|
||||||
|
send,
|
||||||
|
}
|
||||||
433
src/modules/core/archive.js
Normal file
433
src/modules/core/archive.js
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
import { getPool } from "../../db/index.js";
|
||||||
|
|
||||||
|
function isUuid(uuid) {
|
||||||
|
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuditErrors(audit) {
|
||||||
|
if (typeof audit !== 'object') {
|
||||||
|
return 'invalid audit object format';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audit.version != 1) {
|
||||||
|
return 'audit version invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audit.hashType != 'SHA256') {
|
||||||
|
return 'audit hash type invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!audit.digest) {
|
||||||
|
return 'audit digest should exist';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(audit.created)) {
|
||||||
|
return 'audit created type invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!audit.eventId && !audit.agreementId) {
|
||||||
|
return 'eventId or agreementId should exist';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audit.eventId && !isUuid(audit.eventId)) {
|
||||||
|
return 'eventId invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audit.agreementId && !isUuid(audit.agreementId)) {
|
||||||
|
return 'agreementId invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processAudit(data) {
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
let type = data.audit.eventId ? 'event' : 'agreement';
|
||||||
|
const digest = data.audit.digest;
|
||||||
|
let id = data.audit.eventId ? data.audit.eventId : data.audit.agreementId;
|
||||||
|
await client.query(`BEGIN`);
|
||||||
|
let res = await client.query(`
|
||||||
|
SELECT 1
|
||||||
|
FROM audit a
|
||||||
|
WHERE a.${type}_id = $1
|
||||||
|
AND digest = $2
|
||||||
|
LIMIT 1;
|
||||||
|
`, [
|
||||||
|
id,
|
||||||
|
digest,
|
||||||
|
]);
|
||||||
|
if (res.rows.length > 0) {
|
||||||
|
throw new Error('audit record with id and digest exists');
|
||||||
|
}
|
||||||
|
res = await client.query(`
|
||||||
|
INSERT INTO audit (
|
||||||
|
version,
|
||||||
|
event_id,
|
||||||
|
agreement_id,
|
||||||
|
hash_type,
|
||||||
|
digest,
|
||||||
|
created
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6
|
||||||
|
) RETURNING audit_id;
|
||||||
|
`, [
|
||||||
|
data.audit.version,
|
||||||
|
data.audit.eventId,
|
||||||
|
data.audit.agreementId,
|
||||||
|
data.audit.hashType,
|
||||||
|
data.audit.digest,
|
||||||
|
data.audit.created,
|
||||||
|
]);
|
||||||
|
for (const signature of data.signatures) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO audit_signature (
|
||||||
|
audit_id,
|
||||||
|
version,
|
||||||
|
id,
|
||||||
|
signedOn,
|
||||||
|
type,
|
||||||
|
jws
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6
|
||||||
|
);
|
||||||
|
`, [
|
||||||
|
res.rows[0].audit_id,
|
||||||
|
signature.version,
|
||||||
|
signature.id,
|
||||||
|
signature.signedOn,
|
||||||
|
signature.type,
|
||||||
|
signature.jws,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (data.meta) {
|
||||||
|
for await (const [key, value] of Object.entries(data.meta)) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO audit_meta (
|
||||||
|
audit_id,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3
|
||||||
|
);
|
||||||
|
`, [
|
||||||
|
res.rows[0].audit_id,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await client.query('COMMIT');
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuditsByAgreementId(client, id, offset, limit) {
|
||||||
|
let ret = await client.query(`
|
||||||
|
WITH origAudits AS (
|
||||||
|
SELECT
|
||||||
|
audit_id,
|
||||||
|
version,
|
||||||
|
agreement_id,
|
||||||
|
hash_type,
|
||||||
|
digest,
|
||||||
|
created
|
||||||
|
FROM audit
|
||||||
|
WHERE agreement_id = $1
|
||||||
|
ORDER BY created DESC
|
||||||
|
OFFSET $2
|
||||||
|
LIMIT $3
|
||||||
|
),
|
||||||
|
audits AS (
|
||||||
|
SELECT
|
||||||
|
a.*,
|
||||||
|
JSON_AGG(json_build_object(
|
||||||
|
'version', s.version,
|
||||||
|
'id', s.id,
|
||||||
|
'signedOn', s.signedOn,
|
||||||
|
'type', s.type,
|
||||||
|
'jws', s.jws
|
||||||
|
)) AS signatures
|
||||||
|
FROM
|
||||||
|
origAudits a,
|
||||||
|
audit_signature s
|
||||||
|
WHERE s.audit_id = a.audit_id
|
||||||
|
GROUP BY
|
||||||
|
a.audit_id,
|
||||||
|
a.version,
|
||||||
|
a.agreement_id,
|
||||||
|
a.hash_type,
|
||||||
|
a.digest,
|
||||||
|
a.created
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
JSON_AGG(json_build_object(
|
||||||
|
'audit', json_build_object(
|
||||||
|
'version', a.version,
|
||||||
|
'agreementId', a.agreement_id,
|
||||||
|
'hashType', a.hash_type,
|
||||||
|
'digest', a.digest,
|
||||||
|
'created', a.created
|
||||||
|
),
|
||||||
|
'signatures', a.signatures
|
||||||
|
)) AS records
|
||||||
|
FROM audits a
|
||||||
|
`, [
|
||||||
|
id,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
]);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuditsByEventId(client, id, offset, limit) {
|
||||||
|
let ret = await client.query(`
|
||||||
|
WITH origAudits AS (
|
||||||
|
SELECT
|
||||||
|
audit_id,
|
||||||
|
version,
|
||||||
|
event_id,
|
||||||
|
hash_type,
|
||||||
|
digest,
|
||||||
|
created
|
||||||
|
FROM audit
|
||||||
|
WHERE event_id = $1
|
||||||
|
ORDER BY created DESC
|
||||||
|
OFFSET $2
|
||||||
|
LIMIT $3
|
||||||
|
),
|
||||||
|
audits AS (
|
||||||
|
SELECT
|
||||||
|
a.*,
|
||||||
|
JSON_AGG(json_build_object(
|
||||||
|
'version', s.version,
|
||||||
|
'id', s.id,
|
||||||
|
'signedOn', s.signedOn,
|
||||||
|
'type', s.type,
|
||||||
|
'jws', s.jws
|
||||||
|
)) AS signatures
|
||||||
|
FROM
|
||||||
|
origAudits a,
|
||||||
|
audit_signature s
|
||||||
|
WHERE s.audit_id = a.audit_id
|
||||||
|
GROUP BY
|
||||||
|
a.audit_id,
|
||||||
|
a.version,
|
||||||
|
a.event_id,
|
||||||
|
a.hash_type,
|
||||||
|
a.digest,
|
||||||
|
a.created
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
JSON_AGG(json_build_object(
|
||||||
|
'audit', json_build_object(
|
||||||
|
'version', a.version,
|
||||||
|
'eventId', a.event_id,
|
||||||
|
'hashType', a.hash_type,
|
||||||
|
'digest', a.digest,
|
||||||
|
'created', a.created
|
||||||
|
),
|
||||||
|
'signatures', a.signatures
|
||||||
|
)) AS records
|
||||||
|
FROM audits a
|
||||||
|
`, [
|
||||||
|
id,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
]);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuditsByMeta(client, meta, offset, limit) {
|
||||||
|
const fields = [offset, limit];
|
||||||
|
let count = fields.length + 1;
|
||||||
|
let whereInVals = ``
|
||||||
|
for await (const [key, value] of Object.entries(meta)) {
|
||||||
|
if (whereInVals != ``)
|
||||||
|
whereInVals = ` AND `
|
||||||
|
whereInVals += `(key = $${count++} AND value = $${count++})`;
|
||||||
|
fields.push(key);
|
||||||
|
fields.push(value);
|
||||||
|
}
|
||||||
|
let ret = await client.query(`
|
||||||
|
WITH origAudits AS (
|
||||||
|
SELECT
|
||||||
|
audit_id,
|
||||||
|
version,
|
||||||
|
event_id,
|
||||||
|
hash_type,
|
||||||
|
digest,
|
||||||
|
created
|
||||||
|
FROM audit
|
||||||
|
WHERE audit_id IN (
|
||||||
|
SELECT audit_id
|
||||||
|
FROM audit_meta
|
||||||
|
WHERE ${whereInVals}
|
||||||
|
)
|
||||||
|
ORDER BY created DESC
|
||||||
|
OFFSET $1
|
||||||
|
LIMIT $2
|
||||||
|
),
|
||||||
|
audits AS (
|
||||||
|
SELECT
|
||||||
|
a.*,
|
||||||
|
JSON_AGG(json_build_object(
|
||||||
|
'version', s.version,
|
||||||
|
'id', s.id,
|
||||||
|
'signedOn', s.signedOn,
|
||||||
|
'type', s.type,
|
||||||
|
'jws', s.jws
|
||||||
|
)) AS signatures
|
||||||
|
FROM
|
||||||
|
origAudits a,
|
||||||
|
audit_signature s
|
||||||
|
WHERE s.audit_id = a.audit_id
|
||||||
|
GROUP BY
|
||||||
|
a.audit_id,
|
||||||
|
a.version,
|
||||||
|
a.event_id,
|
||||||
|
a.hash_type,
|
||||||
|
a.digest,
|
||||||
|
a.created
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
JSON_AGG(json_build_object(
|
||||||
|
'audit', json_build_object(
|
||||||
|
'version', a.version,
|
||||||
|
'eventId', a.event_id,
|
||||||
|
'hashType', a.hash_type,
|
||||||
|
'digest', a.digest,
|
||||||
|
'created', a.created
|
||||||
|
),
|
||||||
|
'signatures', a.signatures
|
||||||
|
)) AS records
|
||||||
|
FROM audits a
|
||||||
|
`, fields);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAudits(eventId, agreementId, meta, offset) {
|
||||||
|
let ret;
|
||||||
|
let res;
|
||||||
|
const limit = 100;
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
if (eventId) {
|
||||||
|
res = await getAuditsByEventId(client, eventId, offset, limit);
|
||||||
|
} else if (agreementId) {
|
||||||
|
res = await getAuditsByAgreementId(client, agreementId, offset, limit);
|
||||||
|
} else {
|
||||||
|
res = await getAuditsByMeta(client, meta, offset, limit);
|
||||||
|
}
|
||||||
|
if (res.rows.length > 0 && res.rows[0].records) {
|
||||||
|
ret = {
|
||||||
|
auditRecords: res.rows[0].records
|
||||||
|
};
|
||||||
|
if (ret.auditRecords.length === limit) {
|
||||||
|
ret.nextPageToken = Buffer.from(`${offset + limit}`).toString("base64");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function put(input) {
|
||||||
|
let res = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const { audit, meta } = input;
|
||||||
|
const error = getAuditErrors(audit);
|
||||||
|
if (error) {
|
||||||
|
res.error = error;
|
||||||
|
console.log(`${prefix} - Bad Audit: ${error}`);
|
||||||
|
} else {
|
||||||
|
const id = audit.eventId ? audit.eventId : audit.agreementId;
|
||||||
|
if (audit.eventId) {
|
||||||
|
res.message = `event: ${audit.eventId}`;
|
||||||
|
} else {
|
||||||
|
res.message = `agreement: ${audit.agreementId}`;
|
||||||
|
}
|
||||||
|
await processAudit(input);
|
||||||
|
res.success = true;
|
||||||
|
delete res.error;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message != 'audit record with id and digest exists')
|
||||||
|
console.error(e);
|
||||||
|
res.error = e.message;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(input) {
|
||||||
|
let res = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
let error;
|
||||||
|
let id;
|
||||||
|
let type;
|
||||||
|
if (input.eventId) {
|
||||||
|
id = input.eventId;
|
||||||
|
type = 'event';
|
||||||
|
error = isUuid(input.eventId) ? null : 'eventId invalid';
|
||||||
|
} else if (input.agreementId) {
|
||||||
|
id = input.agreementId;
|
||||||
|
type = 'agreement';
|
||||||
|
error = isUuid(input.agreementId) ? null : 'agreementId invalid';
|
||||||
|
} else if (input.meta) {
|
||||||
|
id = `<meta key/value>`;
|
||||||
|
type = 'meta';
|
||||||
|
} else {
|
||||||
|
error = 'eventId, agreementId, or meta required';
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
res.error = error;
|
||||||
|
res.message = `bad request: ${error}`;
|
||||||
|
} else {
|
||||||
|
res.message = `requested ${type}: ${id}`;
|
||||||
|
const offset = input.pageToken ? parseInt(Buffer.from(`${input.pageToken}`, "base64").toString('ascii')) : 0;
|
||||||
|
let ret = await getAudits(input.eventId, input.agreementId, input.meta, offset);
|
||||||
|
if (ret) {
|
||||||
|
res.data = ret;
|
||||||
|
res.success = true;
|
||||||
|
delete res.error;
|
||||||
|
} else {
|
||||||
|
res.error = "not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message != 'audit record with id and digest exists')
|
||||||
|
console.error(e);
|
||||||
|
res.error = e.message;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const archive = {
|
||||||
|
put,
|
||||||
|
get,
|
||||||
|
}
|
||||||
51
src/modules/core/audit.js
Normal file
51
src/modules/core/audit.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import pkg from '@jlinc/core';
|
||||||
|
const { JlincAudit } = pkg;
|
||||||
|
|
||||||
|
async function create(input) {
|
||||||
|
const data = await JlincAudit.create(input);
|
||||||
|
const message = data?.eventId
|
||||||
|
? data?.eventId
|
||||||
|
: data?.agreementId;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sign(input) {
|
||||||
|
const data = await JlincAudit.sign(input);
|
||||||
|
const message = data?.audit?.eventId
|
||||||
|
? data?.audit?.eventId
|
||||||
|
: data?.audit?.agreementId;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send(input) {
|
||||||
|
const data = await JlincAudit.send(input);
|
||||||
|
const message = data?.audit?.eventId
|
||||||
|
? data?.audit?.eventId
|
||||||
|
: data?.audit?.agreementId;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(input) {
|
||||||
|
const data = await JlincAudit.get(input);
|
||||||
|
const message = 'auditGet';
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const audit = {
|
||||||
|
create,
|
||||||
|
sign,
|
||||||
|
send,
|
||||||
|
get,
|
||||||
|
}
|
||||||
460
src/modules/core/data/agreement.js
Normal file
460
src/modules/core/data/agreement.js
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
import pkg from '@jlinc/core';
|
||||||
|
const { JlincAgreement, JlincAudit } = pkg;
|
||||||
|
import { getPool } from "../../../db/index.js";
|
||||||
|
import { entity } from "./entity.js";
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
async function getAgreement(client, userId, id) {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
JSON_BUILD_OBJECT(
|
||||||
|
'version', a.version,
|
||||||
|
'parent', a.parent,
|
||||||
|
'agreementId', a.agreement_id_uuid,
|
||||||
|
'created', a.created,
|
||||||
|
'ids', (
|
||||||
|
SELECT JSON_AGG(ari.required_id)
|
||||||
|
FROM agreement_required_id ari
|
||||||
|
WHERE ari.agreement_id = a.id
|
||||||
|
),
|
||||||
|
'purposes', (
|
||||||
|
SELECT JSON_AGG(p.value ORDER BY p.value)
|
||||||
|
FROM purpose p
|
||||||
|
LEFT JOIN agreement_purpose ap
|
||||||
|
ON p.id = ap.purpose_id
|
||||||
|
AND ap.agreement_id = a.id
|
||||||
|
AND p.user_id = a.user_id
|
||||||
|
AND ap.user_id = a.user_id
|
||||||
|
),
|
||||||
|
'caveats', (
|
||||||
|
SELECT JSON_AGG(c.value ORDER BY c.value)
|
||||||
|
FROM caveat c
|
||||||
|
LEFT JOIN agreement_caveat ac
|
||||||
|
ON c.id = ac.caveat_id
|
||||||
|
AND ac.agreement_id = a.id
|
||||||
|
AND c.user_id = a.user_id
|
||||||
|
AND ac.user_id = a.user_id
|
||||||
|
),
|
||||||
|
'validRoles', (
|
||||||
|
SELECT JSON_AGG(r.value ORDER BY r.value)
|
||||||
|
FROM role r
|
||||||
|
LEFT JOIN agreement_role ar
|
||||||
|
ON r.id = ar.role_id
|
||||||
|
AND ar.agreement_id = a.id
|
||||||
|
AND r.user_id = a.user_id
|
||||||
|
AND ar.user_id = a.user_id
|
||||||
|
)
|
||||||
|
) AS record
|
||||||
|
FROM agreement a
|
||||||
|
WHERE a.agreement_id_uuid = $1
|
||||||
|
AND a.user_id = $2;
|
||||||
|
`
|
||||||
|
const res = await client.query(sql, [
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
if (res.rows.length > 0 && res.rows[0].record) {
|
||||||
|
return res.rows[0].record;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSignatures(client, userId, id) {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
JSON_AGG(
|
||||||
|
JSON_BUILD_OBJECT(
|
||||||
|
'version', s.version,
|
||||||
|
'id', s.signer_id,
|
||||||
|
'signedOn', s.signed_on,
|
||||||
|
'type', s.type,
|
||||||
|
'jws', s.jws,
|
||||||
|
'role', (
|
||||||
|
SELECT r.value
|
||||||
|
FROM role r
|
||||||
|
WHERE r.id = s.role_id
|
||||||
|
AND r.user_id = $2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) AS records
|
||||||
|
FROM signature s
|
||||||
|
INNER JOIN agreement a ON s.agreement_id = a.id
|
||||||
|
WHERE a.agreement_id_uuid = $1
|
||||||
|
AND a.user_id = $2;
|
||||||
|
`
|
||||||
|
const res = await client.query(sql, [
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
if (res.rows.length > 0 && res.rows[0].records) {
|
||||||
|
return res.rows[0].records;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(input, userId) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
const existingAgreement = await getAgreement(client, userId, input.agreementId)
|
||||||
|
if (!existingAgreement) {
|
||||||
|
response.error = 'agreement not found'
|
||||||
|
} else {
|
||||||
|
const data = {
|
||||||
|
agreement: existingAgreement
|
||||||
|
}
|
||||||
|
if (input.includeSignatures) {
|
||||||
|
data.signatures = await getSignatures(client, userId, input.agreementId)
|
||||||
|
for (let x = 0; x < data.signatures.length; x++) {
|
||||||
|
if (data.signatures[x].role === null) {
|
||||||
|
delete data.signatures[x].role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = {
|
||||||
|
message: `retrieved: ${input.agreementId}`,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(client, userId, agreement) {
|
||||||
|
await client.query(`BEGIN`);
|
||||||
|
const res = await client.query(`
|
||||||
|
INSERT INTO agreement (
|
||||||
|
user_id,
|
||||||
|
version,
|
||||||
|
parent,
|
||||||
|
agreement_id_uuid,
|
||||||
|
created,
|
||||||
|
created_as_ts
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6
|
||||||
|
) RETURNING id;
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
agreement.version,
|
||||||
|
agreement.parent,
|
||||||
|
agreement.agreementId,
|
||||||
|
agreement.created,
|
||||||
|
new Date(agreement.created).toISOString(),
|
||||||
|
]);
|
||||||
|
for (const id of agreement.ids) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO agreement_required_id (
|
||||||
|
user_id,
|
||||||
|
agreement_id,
|
||||||
|
required_id
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3
|
||||||
|
);
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
res.rows[0].id,
|
||||||
|
id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
for (const purpose of agreement.purposes) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO purpose (
|
||||||
|
user_id,
|
||||||
|
value
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
purpose,
|
||||||
|
]);
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO agreement_purpose (
|
||||||
|
user_id,
|
||||||
|
agreement_id,
|
||||||
|
purpose_id
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM purpose
|
||||||
|
WHERE value = $3
|
||||||
|
AND user_id = $1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
res.rows[0].id,
|
||||||
|
purpose,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
for (const caveat of agreement.caveats) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO caveat (
|
||||||
|
user_id,
|
||||||
|
value
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
caveat,
|
||||||
|
]);
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO agreement_caveat (
|
||||||
|
user_id,
|
||||||
|
agreement_id,
|
||||||
|
caveat_id
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM caveat
|
||||||
|
WHERE value = $3
|
||||||
|
AND user_id = $1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
res.rows[0].id,
|
||||||
|
caveat,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
for (const role of agreement.validRoles) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO role (
|
||||||
|
user_id,
|
||||||
|
value
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
role,
|
||||||
|
]);
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO agreement_role (
|
||||||
|
user_id,
|
||||||
|
agreement_id,
|
||||||
|
role_id
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM role
|
||||||
|
WHERE value = $3
|
||||||
|
AND user_id = $1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
res.rows[0].id,
|
||||||
|
role,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await client.query('COMMIT');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSignatures(client, userId, agreementId, signatures) {
|
||||||
|
await client.query(`BEGIN`);
|
||||||
|
for (const signature of signatures) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO signature (
|
||||||
|
user_id,
|
||||||
|
version,
|
||||||
|
signer_id,
|
||||||
|
signed_on,
|
||||||
|
type,
|
||||||
|
jws,
|
||||||
|
role_id,
|
||||||
|
agreement_id,
|
||||||
|
event_id,
|
||||||
|
signed_on_as_ts
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6,
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM role
|
||||||
|
WHERE value = $7
|
||||||
|
AND user_id = $1
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM agreement
|
||||||
|
WHERE agreement_id_uuid = $8
|
||||||
|
AND user_id = $1
|
||||||
|
),
|
||||||
|
$9,
|
||||||
|
$10
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
signature.version,
|
||||||
|
signature.id,
|
||||||
|
signature.signedOn,
|
||||||
|
signature.type,
|
||||||
|
signature.jws,
|
||||||
|
signature.role,
|
||||||
|
agreementId,
|
||||||
|
null,
|
||||||
|
new Date(signature.signedOn).toISOString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await client.query('COMMIT');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(input, userId, _client, uuid) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = _client || await getPool();
|
||||||
|
try {
|
||||||
|
input.didDocs = [];
|
||||||
|
for (const shortName of input.shortNames) {
|
||||||
|
input.didDocs.push((await entity.getEntity(client, userId, shortName)).didDoc)
|
||||||
|
}
|
||||||
|
delete input.shortNames;
|
||||||
|
const agreement = await JlincAgreement.create(input);
|
||||||
|
if (uuid) {
|
||||||
|
agreement.agreementId = uuid;
|
||||||
|
}
|
||||||
|
await save(client, userId, agreement);
|
||||||
|
response = {
|
||||||
|
message: `created and saved: ${agreement.agreementId}`,
|
||||||
|
data: agreement,
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
if (!_client)
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function process(input, userId, _client, _agreement) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = _client || await getPool();
|
||||||
|
try {
|
||||||
|
const existingAgreement = _agreement || await getAgreement(client, userId, input.agreementId)
|
||||||
|
if (!existingAgreement) {
|
||||||
|
throw new Error('agreement does not exist')
|
||||||
|
}
|
||||||
|
const inputEntity = input.shortName ? await entity.getEntity(client, userId, input.shortName) : null;
|
||||||
|
const didDoc = inputEntity ? inputEntity.didDoc : input.didDoc;
|
||||||
|
const signingKey = inputEntity ? inputEntity.controlPrivateKeyB64U : input.signingKey;
|
||||||
|
const signingPublicKey = inputEntity ? inputEntity.didDoc.verificationMethod[0].key : input.signingPublicKey;
|
||||||
|
const signingInput = {
|
||||||
|
agreement: existingAgreement,
|
||||||
|
didDoc,
|
||||||
|
signingKey,
|
||||||
|
signingPublicKey,
|
||||||
|
role: input.role,
|
||||||
|
}
|
||||||
|
const agreementData = await JlincAgreement.sign(signingInput);
|
||||||
|
await saveSignatures(client, userId, existingAgreement.agreementId, agreementData.signatures);
|
||||||
|
const audit = await JlincAudit.create(agreementData);
|
||||||
|
const auditInput = {
|
||||||
|
audit,
|
||||||
|
didDoc,
|
||||||
|
signingKey,
|
||||||
|
signingPublicKey,
|
||||||
|
}
|
||||||
|
const auditData = await JlincAudit.sign(auditInput);
|
||||||
|
response = {
|
||||||
|
message: `signed and saved: ${agreementData?.agreement?.agreementId}`,
|
||||||
|
data: {
|
||||||
|
auditData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (input.archive) {
|
||||||
|
await axios.post(
|
||||||
|
`${input.archive.url}/api/v1/audit/put`,
|
||||||
|
response.data.auditData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${input.archive.key}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
if (!_client)
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function produce(input, userId) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
const created = (await create(input.data, userId, client)).data;
|
||||||
|
const processed = (await process(
|
||||||
|
{
|
||||||
|
agreementId: created.agreementId,
|
||||||
|
shortName: input.shortName,
|
||||||
|
role: input.role,
|
||||||
|
archive: input.archive,
|
||||||
|
},
|
||||||
|
userId,
|
||||||
|
client,
|
||||||
|
created,
|
||||||
|
)).data;
|
||||||
|
response = {
|
||||||
|
message: `created and processed: ${created.agreementId}`,
|
||||||
|
data: {
|
||||||
|
created,
|
||||||
|
processed,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const agreement = {
|
||||||
|
getAgreement,
|
||||||
|
getSignatures,
|
||||||
|
get,
|
||||||
|
create,
|
||||||
|
process,
|
||||||
|
produce,
|
||||||
|
}
|
||||||
203
src/modules/core/data/audit.js
Normal file
203
src/modules/core/data/audit.js
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { getPool } from "../../../db/index.js";
|
||||||
|
import { agreement } from "./agreement.js";
|
||||||
|
import { event } from "./event.js";
|
||||||
|
import { stringify, configure } from "safe-stable-stringify";
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
import { entity } from "./entity.js";
|
||||||
|
import sodium from "sodium-native";
|
||||||
|
|
||||||
|
|
||||||
|
function splitJws(jws) {
|
||||||
|
const sections = jws.split('.');
|
||||||
|
if (sections.length !== 3) {
|
||||||
|
throw ('Input must be a JWS.');
|
||||||
|
}
|
||||||
|
const jwt = JSON.parse(Buffer.from(sections[0], 'base64url').toString());
|
||||||
|
if (jwt.alg !== 'EdDSA') {
|
||||||
|
throw ('JWT does not indicate EdDSA');
|
||||||
|
}
|
||||||
|
const payload = JSON.parse(Buffer.from(sections[1], 'base64url').toString());
|
||||||
|
const wasSigned = Buffer.from(sections[0] + '.' + sections[1]);
|
||||||
|
const signature = Buffer.from(sections[2], 'base64url');
|
||||||
|
return {
|
||||||
|
jwt,
|
||||||
|
payload,
|
||||||
|
wasSigned,
|
||||||
|
signature,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyJws(input) {
|
||||||
|
let ret = false;
|
||||||
|
try {
|
||||||
|
if (!input.jws) {
|
||||||
|
throw ('No JWS provided.');
|
||||||
|
}
|
||||||
|
if (!input.publicKey) {
|
||||||
|
throw ('No publicKey provided.');
|
||||||
|
}
|
||||||
|
const publicKey = Buffer.from(input.publicKey, 'base64url');
|
||||||
|
if (publicKey.length !== sodium.crypto_sign_PUBLICKEYBYTES) {
|
||||||
|
throw ('publicKey length must be crypto_sign_PUBLICKEYBYTES (32).');
|
||||||
|
}
|
||||||
|
const providedPublicKey = Buffer.from(input.jws.jwt.jwk.x, 'base64url');
|
||||||
|
if (publicKey.compare(providedPublicKey) !== 0) {
|
||||||
|
ret = false;
|
||||||
|
} else {
|
||||||
|
ret = sodium.crypto_sign_verify_detached(input.jws.signature, input.jws.wasSigned, Buffer.from(input.publicKey, 'base64url'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateSignatures(item, signatures, didDocs) {
|
||||||
|
let res = false;
|
||||||
|
try {
|
||||||
|
for (const signature of signatures) {
|
||||||
|
const didDoc = didDocs.find((didDoc) => didDoc.id === signature.id);
|
||||||
|
if (!didDoc) throw ('DID Document not provided');
|
||||||
|
const split = splitJws(signature.jws);
|
||||||
|
if (stringify(item) !== stringify(split.payload)) throw ('Payload does not match');
|
||||||
|
const validJws = verifyJws({
|
||||||
|
jws: split,
|
||||||
|
publicKey: didDoc.verificationMethod[0].key,
|
||||||
|
});
|
||||||
|
if (!validJws) throw ('Signature is not valid');
|
||||||
|
res = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDigest(content, length) {
|
||||||
|
if (typeof content === 'object') {
|
||||||
|
content = stringify(content);
|
||||||
|
}
|
||||||
|
const hash = createHash('sha256')
|
||||||
|
.update(content)
|
||||||
|
.digest('hex')
|
||||||
|
.slice(0, length);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verify(input, userId) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
valid: [],
|
||||||
|
invalid: [],
|
||||||
|
};
|
||||||
|
input.didDocs = [];
|
||||||
|
for (const shortName of input.shortNames) {
|
||||||
|
input.didDocs.push((await entity.getEntity(client, userId, shortName)).didDoc)
|
||||||
|
}
|
||||||
|
delete input.shortNames;
|
||||||
|
const organizedById = [];
|
||||||
|
for (const item of input.audits) {
|
||||||
|
const found = organizedById.find((obi) =>
|
||||||
|
item.audit.agreementId && obi.agreementId === item.audit.agreementId ||
|
||||||
|
item.audit.eventId && obi.eventId === item.audit.eventId
|
||||||
|
);
|
||||||
|
if (!found) {
|
||||||
|
const newItem = item.audit.agreementId
|
||||||
|
? {
|
||||||
|
agreementId: item.audit.agreementId,
|
||||||
|
auditRecords: [item],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
eventId: item.audit.eventId,
|
||||||
|
auditRecords: [item],
|
||||||
|
}
|
||||||
|
organizedById.push(newItem)
|
||||||
|
} else {
|
||||||
|
found.auditRecords.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const item of organizedById) {
|
||||||
|
const existingItem = item.eventId
|
||||||
|
? await event.getEvent(client, userId, item.eventId, true)
|
||||||
|
: await agreement.getAgreement(client, userId, item.agreementId)
|
||||||
|
const existingSignatures = item.eventId
|
||||||
|
? await event.getSignatures(client, userId, item.eventId)
|
||||||
|
: await agreement.getSignatures(client, userId, item.agreementId)
|
||||||
|
// Does the agreement signature verify?
|
||||||
|
let validSignature = false;
|
||||||
|
if (validateSignatures(existingItem, existingSignatures, input.didDocs)) {
|
||||||
|
validSignature = true;
|
||||||
|
}
|
||||||
|
for (const auditRecord of item.auditRecords) {
|
||||||
|
const res = {
|
||||||
|
audit: auditRecord,
|
||||||
|
results: {
|
||||||
|
validId: false,
|
||||||
|
validSignature,
|
||||||
|
validAuditHash: false,
|
||||||
|
validAuditSignature: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Do the agreement IDs match?
|
||||||
|
if (
|
||||||
|
(item.agreementId !== null && auditRecord.audit.agreementId === item.agreementId) ||
|
||||||
|
(item.eventId !== null && auditRecord.audit.eventId === item.eventId)
|
||||||
|
)
|
||||||
|
res.results.validId = true;
|
||||||
|
// Does the audit hash match?
|
||||||
|
// The digest was created from whichever signatures this audit record has
|
||||||
|
const signatures = [];
|
||||||
|
for (const s of auditRecord.signatures) {
|
||||||
|
const existingSignature = existingSignatures.find((es) => es.id === s.id);
|
||||||
|
if (existingSignature) {
|
||||||
|
signatures.push(existingSignature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const check = item.eventId
|
||||||
|
? {
|
||||||
|
event: existingItem,
|
||||||
|
signatures,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
agreement: existingItem,
|
||||||
|
signatures,
|
||||||
|
}
|
||||||
|
const digest = generateDigest(check);
|
||||||
|
if (digest === auditRecord.audit.digest)
|
||||||
|
res.results.validAuditHash = true;
|
||||||
|
// Does the audit signature verify?
|
||||||
|
if (validateSignatures(auditRecord.audit, auditRecord.signatures, input.didDocs)) {
|
||||||
|
res.results.validAuditSignature = true;
|
||||||
|
}
|
||||||
|
const isValid =
|
||||||
|
res.results.validId === true &&
|
||||||
|
res.results.validSignature === true &&
|
||||||
|
res.results.validAuditHash === true &&
|
||||||
|
res.results.validAuditSignature === true;
|
||||||
|
if (isValid) {
|
||||||
|
data.valid.push(res);
|
||||||
|
} else {
|
||||||
|
data.invalid.push(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = {
|
||||||
|
message: 'validation complete',
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const audit = {
|
||||||
|
verify,
|
||||||
|
}
|
||||||
172
src/modules/core/data/entity.js
Normal file
172
src/modules/core/data/entity.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { getPool } from "../../../db/index.js";
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
import { getConfig } from "../../../common/config.js";
|
||||||
|
import axios from "axios";
|
||||||
|
import sodium from "sodium-native";
|
||||||
|
|
||||||
|
async function getEntity(client, userId, shortName) {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
JSON_BUILD_OBJECT(
|
||||||
|
'fedidUrl', e.fedid_url,
|
||||||
|
'shortName', e.short_name,
|
||||||
|
'didId', e.did_id,
|
||||||
|
'controlPrivateKeyB64U', e.control_private_key_b64u,
|
||||||
|
'recoveryPrivateKeyB64U', e.recovery_private_key_b64u,
|
||||||
|
'didDoc', e.did_doc
|
||||||
|
) AS record
|
||||||
|
FROM entity e
|
||||||
|
WHERE LOWER(e.short_name) = LOWER($1)
|
||||||
|
AND e.user_id = $2;
|
||||||
|
`
|
||||||
|
const res = await client.query(sql, [
|
||||||
|
shortName,
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
if (res.rows.length > 0 && res.rows[0].record) {
|
||||||
|
return res.rows[0].record;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(input, userId) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
const existingEntity = await getEntity(client, userId, input.shortName)
|
||||||
|
if (!existingEntity) {
|
||||||
|
response.error = 'entity not found'
|
||||||
|
} else {
|
||||||
|
const data = {
|
||||||
|
didDoc: existingEntity.didDoc,
|
||||||
|
}
|
||||||
|
response = {
|
||||||
|
message: `retrieved: ${input.shortName}`,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDomains(input, userId) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const config = getConfig();
|
||||||
|
const fedidUrl = input.fedidUrl ?? config.defaultFedidUrl;
|
||||||
|
const data = (await axios.get(
|
||||||
|
`${fedidUrl}/api/v2/domains`,
|
||||||
|
)).data.data.domains;
|
||||||
|
response = {
|
||||||
|
message: `retrieved domains`,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(client, userId, entity) {
|
||||||
|
const res = await client.query(`
|
||||||
|
INSERT INTO entity (
|
||||||
|
user_id,
|
||||||
|
fedid_url,
|
||||||
|
short_name,
|
||||||
|
did_id,
|
||||||
|
control_private_key_b64u,
|
||||||
|
recovery_private_key_b64u,
|
||||||
|
did_doc
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6,
|
||||||
|
$7
|
||||||
|
) RETURNING id;
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
entity.fedidUrl,
|
||||||
|
entity.shortName,
|
||||||
|
entity.didDoc.id,
|
||||||
|
entity.controlPrivateKeyB64U,
|
||||||
|
entity.recoveryPrivateKeyB64U,
|
||||||
|
entity.didDoc,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(input, userId) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
const config = getConfig();
|
||||||
|
const fedidUrl = input.fedidUrl ?? config.defaultFedidUrl;
|
||||||
|
const domains = (await axios.get(
|
||||||
|
`${fedidUrl}/api/v2/domains`,
|
||||||
|
)).data.data.domains;
|
||||||
|
const shortName = input.shortName.split('@')
|
||||||
|
const domain = shortName[1]
|
||||||
|
if (!domains.includes(domain)) {
|
||||||
|
throw new Error('domain is not available');
|
||||||
|
}
|
||||||
|
const entity = {
|
||||||
|
shortName: input.shortName,
|
||||||
|
fedidUrl,
|
||||||
|
}
|
||||||
|
// Generate a control key
|
||||||
|
entity.controlPublicKey = Buffer.alloc(sodium.crypto_sign_PUBLICKEYBYTES);
|
||||||
|
entity.controlPrivateKey = Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES);
|
||||||
|
sodium.crypto_sign_keypair(entity.controlPublicKey, entity.controlPrivateKey);
|
||||||
|
entity.controlPublicKeyB64U = entity.controlPublicKey.toString("base64url");
|
||||||
|
entity.controlPrivateKeyB64U = entity.controlPrivateKey.toString("base64url");
|
||||||
|
// Generate a recovery key
|
||||||
|
entity.recoveryPublicKey = Buffer.alloc(sodium.crypto_sign_PUBLICKEYBYTES);
|
||||||
|
entity.recoveryPrivateKey = Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES);
|
||||||
|
entity.recoveryPrivateKeyB64U = entity.recoveryPrivateKey.toString("base64url");
|
||||||
|
sodium.crypto_sign_keypair(entity.recoveryPublicKey, entity.recoveryPrivateKey);
|
||||||
|
entity.recoveryHash = createHash("sha256").update(entity.recoveryPublicKey).digest("hex").slice(0, 48);
|
||||||
|
entity.didDoc = (await axios.post(
|
||||||
|
`${fedidUrl}/api/v2/did/create`,
|
||||||
|
{
|
||||||
|
shortName: entity.shortName,
|
||||||
|
control: entity.controlPublicKeyB64U,
|
||||||
|
recoveryHash: entity.recoveryHash,
|
||||||
|
},
|
||||||
|
)).data.data.didDoc;
|
||||||
|
await save(client, userId, entity);
|
||||||
|
response = {
|
||||||
|
message: `created and saved: ${entity.didDoc.id}`,
|
||||||
|
data: {
|
||||||
|
didDoc: entity.didDoc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
response.error = e.message;
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const entity = {
|
||||||
|
getEntity,
|
||||||
|
getDomains,
|
||||||
|
get,
|
||||||
|
create,
|
||||||
|
}
|
||||||
406
src/modules/core/data/event.js
Normal file
406
src/modules/core/data/event.js
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
import pkg from '@jlinc/core';
|
||||||
|
const { JlincEvent, JlincAudit } = pkg;
|
||||||
|
import { getPool } from "../../../db/index.js";
|
||||||
|
import { entity } from "./entity.js";
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
async function getEvent(client, userId, id, includeData, meta) {
|
||||||
|
const dataSql = includeData
|
||||||
|
? `
|
||||||
|
'data', (
|
||||||
|
SELECT ed.data
|
||||||
|
FROM event_data ed
|
||||||
|
WHERE ed.event_id = e.id
|
||||||
|
),
|
||||||
|
`
|
||||||
|
: ``;
|
||||||
|
let fields = [userId];
|
||||||
|
let count = fields.length + 1;
|
||||||
|
let whereAnd = ``
|
||||||
|
if (id) {
|
||||||
|
whereAnd += ` AND e.event_id_uuid = $${count++}`;
|
||||||
|
fields.push(id);
|
||||||
|
}
|
||||||
|
if (meta) {
|
||||||
|
let whereInVals = ``
|
||||||
|
for await (const [key, value] of Object.entries(meta)) {
|
||||||
|
if (whereInVals != ``)
|
||||||
|
whereInVals = ` AND `
|
||||||
|
whereInVals += `(em.key = $${count++} AND em.value = $${count++})`;
|
||||||
|
fields.push(key);
|
||||||
|
fields.push(value);
|
||||||
|
}
|
||||||
|
whereAnd += `
|
||||||
|
AND e.id IN (
|
||||||
|
SELECT em.event_id
|
||||||
|
FROM event_meta em
|
||||||
|
WHERE em.user_id = $1
|
||||||
|
AND ${whereInVals}
|
||||||
|
)
|
||||||
|
`
|
||||||
|
}
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
JSON_BUILD_OBJECT(
|
||||||
|
'version', e.version,
|
||||||
|
'eventId', e.event_id_uuid,
|
||||||
|
'type', (
|
||||||
|
SELECT et.value
|
||||||
|
FROM event_type et
|
||||||
|
WHERE et.id = e.event_type_id
|
||||||
|
),
|
||||||
|
'senderId', e.sender_id,
|
||||||
|
'recipientId', e.recipient_id,
|
||||||
|
'created', e.created,
|
||||||
|
'agreementId', (
|
||||||
|
SELECT a.agreement_id_uuid
|
||||||
|
FROM agreement a
|
||||||
|
WHERE a.id = e.agreement_id
|
||||||
|
AND a.user_id = $1
|
||||||
|
),
|
||||||
|
${dataSql}
|
||||||
|
'created', e.created
|
||||||
|
) AS record
|
||||||
|
FROM event e
|
||||||
|
WHERE e.user_id = $1
|
||||||
|
${whereAnd};
|
||||||
|
`
|
||||||
|
const res = await client.query(sql, fields);
|
||||||
|
if (res.rows.length > 0 && res.rows[0].record) {
|
||||||
|
const ret = res.rows[0].record;
|
||||||
|
if (ret.data) {
|
||||||
|
try {
|
||||||
|
ret.data = JSON.parse(ret.data);
|
||||||
|
} catch(e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSignatures(client, userId, id) {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
JSON_AGG(
|
||||||
|
JSON_BUILD_OBJECT(
|
||||||
|
'version', s.version,
|
||||||
|
'id', s.signer_id,
|
||||||
|
'signedOn', s.signed_on,
|
||||||
|
'type', s.type,
|
||||||
|
'jws', s.jws
|
||||||
|
)
|
||||||
|
) AS records
|
||||||
|
FROM signature s
|
||||||
|
INNER JOIN event e ON s.event_id = e.id
|
||||||
|
WHERE e.event_id_uuid = $1
|
||||||
|
AND e.user_id = $2;
|
||||||
|
`
|
||||||
|
const res = await client.query(sql, [
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
if (res.rows.length > 0 && res.rows[0].records) {
|
||||||
|
return res.rows[0].records;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(input, userId) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
const existingEvent = await getEvent(client, userId, input.eventId, true, input.meta)
|
||||||
|
if (!existingEvent) {
|
||||||
|
response.error = 'event not found'
|
||||||
|
} else {
|
||||||
|
const data = {
|
||||||
|
event: existingEvent
|
||||||
|
}
|
||||||
|
if (input.includeSignatures) {
|
||||||
|
data.signatures = await getSignatures(client, userId, input.eventId)
|
||||||
|
}
|
||||||
|
response = {
|
||||||
|
message: `retrieved: ${existingEvent.eventId}`,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(client, userId, event, meta) {
|
||||||
|
await client.query(`BEGIN`);
|
||||||
|
const res = await client.query(`
|
||||||
|
INSERT INTO event (
|
||||||
|
user_id,
|
||||||
|
version,
|
||||||
|
event_id_uuid,
|
||||||
|
event_type_id,
|
||||||
|
agreement_id,
|
||||||
|
sender_id,
|
||||||
|
recipient_id,
|
||||||
|
created,
|
||||||
|
created_as_ts
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM event_type
|
||||||
|
WHERE value = $4
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM agreement
|
||||||
|
WHERE agreement_id_uuid = $5
|
||||||
|
),
|
||||||
|
$6,
|
||||||
|
$7,
|
||||||
|
$8,
|
||||||
|
$9
|
||||||
|
) RETURNING id;
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
event.version,
|
||||||
|
event.eventId,
|
||||||
|
event.type,
|
||||||
|
event.agreementId,
|
||||||
|
event.senderId,
|
||||||
|
event.recipientId,
|
||||||
|
event.created,
|
||||||
|
new Date(event.created).toISOString(),
|
||||||
|
]);
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO event_data (
|
||||||
|
user_id,
|
||||||
|
event_id,
|
||||||
|
data
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3
|
||||||
|
);
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
res.rows[0].id,
|
||||||
|
event.data,
|
||||||
|
]);
|
||||||
|
if (meta) {
|
||||||
|
for await (const [key, value] of Object.entries(meta)) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO event_meta (
|
||||||
|
user_id,
|
||||||
|
event_id,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4
|
||||||
|
);
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
res.rows[0].id,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await client.query('COMMIT');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSignatures(client, userId, eventId, signatures) {
|
||||||
|
await client.query(`BEGIN`);
|
||||||
|
for (const signature of signatures) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO signature (
|
||||||
|
user_id,
|
||||||
|
version,
|
||||||
|
signer_id,
|
||||||
|
signed_on,
|
||||||
|
type,
|
||||||
|
jws,
|
||||||
|
role_id,
|
||||||
|
agreement_id,
|
||||||
|
event_id,
|
||||||
|
signed_on_as_ts
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6,
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM role
|
||||||
|
WHERE value = $7
|
||||||
|
AND user_id = $1
|
||||||
|
),
|
||||||
|
$8,
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM event
|
||||||
|
WHERE event_id_uuid = $9
|
||||||
|
AND user_id = $1
|
||||||
|
),
|
||||||
|
$10
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
signature.version,
|
||||||
|
signature.id,
|
||||||
|
signature.signedOn,
|
||||||
|
signature.type,
|
||||||
|
signature.jws,
|
||||||
|
signature.role,
|
||||||
|
null,
|
||||||
|
eventId,
|
||||||
|
new Date(signature.signedOn).toISOString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await client.query('COMMIT');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(input, userId, _client, _sender) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = _client || await getPool();
|
||||||
|
try {
|
||||||
|
input.senderId = _sender ? _sender.didDoc.id : (await entity.getEntity(client, userId, input.senderShortName)).didDoc.id
|
||||||
|
input.recipientId = (await entity.getEntity(client, userId, input.recipientShortName)).didDoc.id
|
||||||
|
delete input.senderShortName
|
||||||
|
delete input.recipientShortName
|
||||||
|
const event = await JlincEvent.create(input);
|
||||||
|
await save(client, userId, event, input.meta);
|
||||||
|
response = {
|
||||||
|
message: `created and saved: ${event.eventId}`,
|
||||||
|
data: event,
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
if (!_client)
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function process(input, userId, _client, _event, _sender, meta) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = _client || await getPool();
|
||||||
|
try {
|
||||||
|
const existingEvent = _event || await getEvent(client, userId, input.eventId, true)
|
||||||
|
if (!existingEvent) {
|
||||||
|
throw new Error('event does not exist')
|
||||||
|
}
|
||||||
|
const inputEntity = _sender || input.shortName ? await entity.getEntity(client, userId, input.shortName) : null;
|
||||||
|
const didDoc = inputEntity ? inputEntity.didDoc : input.didDoc;
|
||||||
|
const signingKey = inputEntity ? inputEntity.controlPrivateKeyB64U : input.signingKey;
|
||||||
|
const signingPublicKey = inputEntity ? inputEntity.didDoc.verificationMethod[0].key : input.signingPublicKey;
|
||||||
|
const signingInput = {
|
||||||
|
event: existingEvent,
|
||||||
|
didDoc,
|
||||||
|
signingKey,
|
||||||
|
signingPublicKey,
|
||||||
|
}
|
||||||
|
const eventData = await JlincEvent.sign(signingInput);
|
||||||
|
await saveSignatures(client, userId, existingEvent.eventId, eventData.signatures);
|
||||||
|
const audit = await JlincAudit.create(eventData);
|
||||||
|
const auditInput = {
|
||||||
|
audit,
|
||||||
|
didDoc,
|
||||||
|
signingKey,
|
||||||
|
signingPublicKey,
|
||||||
|
}
|
||||||
|
const auditData = await JlincAudit.sign(auditInput);
|
||||||
|
response = {
|
||||||
|
message: `signed and saved: ${eventData?.event?.eventId}`,
|
||||||
|
data: {
|
||||||
|
auditData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (input.archive) {
|
||||||
|
if (meta) {
|
||||||
|
response.data.auditData.meta = meta;
|
||||||
|
}
|
||||||
|
await axios.post(
|
||||||
|
`${input.archive.url}/api/v1/audit/put`,
|
||||||
|
response.data.auditData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${input.archive.key}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
if (!_client)
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function produce(input, userId) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
const shortName = input.senderShortName;
|
||||||
|
const sender = (await entity.getEntity(client, userId, input.senderShortName))
|
||||||
|
const created = (await create(input, userId, client, sender)).data;
|
||||||
|
const processed = (await process(
|
||||||
|
{
|
||||||
|
eventId: created.eventId,
|
||||||
|
shortName,
|
||||||
|
archive: input.archive,
|
||||||
|
},
|
||||||
|
userId,
|
||||||
|
client,
|
||||||
|
created,
|
||||||
|
sender,
|
||||||
|
input.meta,
|
||||||
|
)).data;
|
||||||
|
response = {
|
||||||
|
message: `created and processed: ${created.eventId}`,
|
||||||
|
data: {
|
||||||
|
created,
|
||||||
|
processed,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const event = {
|
||||||
|
getEvent,
|
||||||
|
getSignatures,
|
||||||
|
get,
|
||||||
|
create,
|
||||||
|
process,
|
||||||
|
produce,
|
||||||
|
}
|
||||||
12
src/modules/core/data/index.js
Normal file
12
src/modules/core/data/index.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { agreement } from "./agreement.js";
|
||||||
|
import { event } from "./event.js";
|
||||||
|
import { entity } from "./entity.js";
|
||||||
|
import { audit } from "./audit.js";
|
||||||
|
|
||||||
|
|
||||||
|
export const data = {
|
||||||
|
agreement,
|
||||||
|
event,
|
||||||
|
entity,
|
||||||
|
audit,
|
||||||
|
}
|
||||||
55
src/modules/core/did.js
Normal file
55
src/modules/core/did.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import pkg from '@jlinc/core';
|
||||||
|
const { JlincDid } = pkg;
|
||||||
|
|
||||||
|
async function create(input) {
|
||||||
|
const data = await JlincDid.create(input);
|
||||||
|
const message = data?.didDoc?.id;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createKeys() {
|
||||||
|
const data = await JlincDid.createKeys();
|
||||||
|
const message = data?.didDoc?.id;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rotate(input) {
|
||||||
|
const data = await JlincDid.rotate(input);
|
||||||
|
const message = data?.didDoc?.id;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send(input) {
|
||||||
|
const data = await JlincDid.send(input);
|
||||||
|
const message = data?.didDoc?.id;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolve(input) {
|
||||||
|
const data = await JlincDid.resolve(input);
|
||||||
|
const message = data?.didDoc?.id;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const did = {
|
||||||
|
create,
|
||||||
|
createKeys,
|
||||||
|
rotate,
|
||||||
|
send,
|
||||||
|
resolve,
|
||||||
|
}
|
||||||
35
src/modules/core/event.js
Normal file
35
src/modules/core/event.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import pkg from '@jlinc/core';
|
||||||
|
const { JlincEvent } = pkg;
|
||||||
|
|
||||||
|
async function create(input) {
|
||||||
|
const data = await JlincEvent.create(input);
|
||||||
|
const message = data?.eventId;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sign(input) {
|
||||||
|
const data = await JlincEvent.sign(input);
|
||||||
|
const message = data?.event?.eventId;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send(input) {
|
||||||
|
const data = await JlincEvent.send(input);
|
||||||
|
const message = data?.event?.eventId;
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const event = {
|
||||||
|
create,
|
||||||
|
sign,
|
||||||
|
send,
|
||||||
|
}
|
||||||
171
src/modules/core/index.js
Normal file
171
src/modules/core/index.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { did } from "./did.js";
|
||||||
|
import { agreement } from "./agreement.js";
|
||||||
|
import { event } from "./event.js";
|
||||||
|
import { audit } from "./audit.js";
|
||||||
|
import { data } from "./data/index.js";
|
||||||
|
import { archive } from "./archive.js";
|
||||||
|
import { trackUsage } from "./usage.js";
|
||||||
|
import { getConfig } from "../../common/config.js";
|
||||||
|
|
||||||
|
async function post(req, res) {
|
||||||
|
let response = {
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error',
|
||||||
|
};
|
||||||
|
let type;
|
||||||
|
const prefix = `${req.method} ${req.url}`;
|
||||||
|
try {
|
||||||
|
const input = req.body;
|
||||||
|
const config = getConfig();
|
||||||
|
if (Object.keys(config.appModules).includes('core')) {
|
||||||
|
switch (req.url) {
|
||||||
|
case '/api/v1/did/create':
|
||||||
|
response = await did.create(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/did/rotate':
|
||||||
|
response = await did.rotate(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/did/updateServices':
|
||||||
|
response = await did.updateServices(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/did/send':
|
||||||
|
response = await did.send(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/did/resolve':
|
||||||
|
response = await did.resolve(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/agreement/create':
|
||||||
|
response = await agreement.create(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/agreement/sign':
|
||||||
|
response = await agreement.sign(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/agreement/send':
|
||||||
|
response = await agreement.send(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/event/create':
|
||||||
|
response = await event.create(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/event/sign':
|
||||||
|
response = await event.sign(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/event/send':
|
||||||
|
response = await event.send(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/audit/create':
|
||||||
|
response = await audit.create(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/audit/sign':
|
||||||
|
response = await audit.sign(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/audit/send':
|
||||||
|
response = await audit.send(input);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/entity/get':
|
||||||
|
response = await data.entity.get(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/entity/domains/get':
|
||||||
|
response = await data.entity.getDomains(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/entity/create':
|
||||||
|
response = await data.entity.create(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/agreement/get':
|
||||||
|
response = await data.agreement.get(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/agreement/create':
|
||||||
|
response = await data.agreement.create(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/agreement/process':
|
||||||
|
response = await data.agreement.process(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/agreement/produce':
|
||||||
|
response = await data.agreement.produce(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/event/get':
|
||||||
|
response = await data.event.get(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/event/create':
|
||||||
|
response = await data.event.create(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/event/process':
|
||||||
|
response = await data.event.process(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/event/produce':
|
||||||
|
response = await data.event.produce(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
case '/api/v1/data/audit/verify':
|
||||||
|
response = await data.audit.verify(input, req.session.user_id);
|
||||||
|
type = 'core';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(config.appModules).includes('archive')) {
|
||||||
|
switch (req.url) {
|
||||||
|
case '/api/v1/audit/put':
|
||||||
|
response = await archive.put(input);
|
||||||
|
type = 'archive';
|
||||||
|
break;
|
||||||
|
case '/api/v1/audit/get':
|
||||||
|
response = await archive.get(input);
|
||||||
|
type = 'archive';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!type) {
|
||||||
|
response.error = 'Page not found';
|
||||||
|
res.status(404).send(response);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
response.error = e.message;
|
||||||
|
} finally {
|
||||||
|
if (response?.message) {
|
||||||
|
req.apiMessage = response.message;
|
||||||
|
} else if (response?.data?.error) {
|
||||||
|
req.apiMessage = `ERROR: ${response.data.error}`;
|
||||||
|
} else {
|
||||||
|
req.apiMessage = `ERROR: unknown error`;
|
||||||
|
}
|
||||||
|
if (response?.data)
|
||||||
|
response = response.data;
|
||||||
|
await trackUsage(req.session.user_id, req.url, type, response?.error ? false : true);
|
||||||
|
res.status(response?.error ? 400 : 200).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const core = {
|
||||||
|
did,
|
||||||
|
agreement,
|
||||||
|
event,
|
||||||
|
audit,
|
||||||
|
archive,
|
||||||
|
post,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
80
src/modules/core/usage.js
Normal file
80
src/modules/core/usage.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { getPool } from "../../db/index.js";
|
||||||
|
|
||||||
|
export async function trackUsage(user_id, url, type, success) {
|
||||||
|
const client = await getPool();
|
||||||
|
try {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO usage_url (
|
||||||
|
url,
|
||||||
|
module
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
|
`, [
|
||||||
|
url,
|
||||||
|
type,
|
||||||
|
]);
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO usage (
|
||||||
|
user_id,
|
||||||
|
usage_url_id,
|
||||||
|
success
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
(
|
||||||
|
SELECT id
|
||||||
|
FROM usage_url
|
||||||
|
WHERE url = $2
|
||||||
|
),
|
||||||
|
$3
|
||||||
|
);
|
||||||
|
`, [
|
||||||
|
user_id,
|
||||||
|
url,
|
||||||
|
success,
|
||||||
|
]);
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUsage(user, begin, end) {
|
||||||
|
const client = await getPool();
|
||||||
|
let ret = [];
|
||||||
|
try {
|
||||||
|
let res = await client.query(`
|
||||||
|
WITH base AS (
|
||||||
|
SELECT
|
||||||
|
uu.module,
|
||||||
|
COUNT(*) AS num
|
||||||
|
FROM usage u
|
||||||
|
JOIN usage_url uu ON uu.id = u.usage_url_id
|
||||||
|
WHERE u.created_ts >= $1
|
||||||
|
AND u.created_ts < $2
|
||||||
|
AND u.user_id = $3
|
||||||
|
GROUP BY uu.module
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
json_object_agg(
|
||||||
|
module,
|
||||||
|
num
|
||||||
|
) AS usage_counts
|
||||||
|
FROM base
|
||||||
|
`, [
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
user.id,
|
||||||
|
]);
|
||||||
|
if (res.rows.length > 0) {
|
||||||
|
ret = res.rows[0].usage_counts;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await client.release();
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
11381
src/package-lock.json
generated
Normal file
11381
src/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
src/package.json
Normal file
50
src/package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "jlinc-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "JLINC Server",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
||||||
|
"e2e": "node testing/e2e.js",
|
||||||
|
"start": "node index.js",
|
||||||
|
"start-dev": "./node_modules/.bin/nodemon index.js",
|
||||||
|
"start-dev-debug": "./node_modules/.bin/nodemon --inspect=0.0.0.0:9694 index.js",
|
||||||
|
"lint": "npx eslint .",
|
||||||
|
"lint:fix": "npm run lint -- --fix",
|
||||||
|
"prettier": "npx prettier . --check",
|
||||||
|
"prettier:fix": "npm run prettier -- --write",
|
||||||
|
"format": "npm run lint:fix && npm run prettier:fix"
|
||||||
|
},
|
||||||
|
"author": "JLINC <nospam@jlinc.com>",
|
||||||
|
"license": "SSPLv1",
|
||||||
|
"dependencies": {
|
||||||
|
"@jlinc/core": "file:./packages/core",
|
||||||
|
"axios": "^1.10.0",
|
||||||
|
"body-parser": "^1.20.3",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"express-session": "^1.18.1",
|
||||||
|
"marked": "^16.1.1",
|
||||||
|
"memorystore": "^1.6.7",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-github": "^1.1.0",
|
||||||
|
"passport-google-oauth20": "^2.0.0",
|
||||||
|
"passport-openidconnect": "^0.1.2",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
"safe-stable-stringify": "^2.5.0",
|
||||||
|
"sodium-native": "^5.0.6",
|
||||||
|
"swagger-ui-express": "^5.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/eslint-parser": "^7.25.9",
|
||||||
|
"@babel/plugin-syntax-import-assertions": "^7.26.0",
|
||||||
|
"@eslint/js": "^9.17.0",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"nodemon": "^3.1.9",
|
||||||
|
"prettier": "^3.4.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user