├── .dockerignore
├── .editorconfig
├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── NOTICE
├── README.md
├── assets
├── images
│ └── copyright-vs-license.png
└── template.html
├── browser
├── app.tsx
├── components
│ ├── App.tsx
│ ├── Landing.tsx
│ ├── NotFound.tsx
│ ├── projects
│ │ ├── ProjectRouter.tsx
│ │ ├── acl
│ │ │ ├── GroupSelect.tsx
│ │ │ └── ProjectAclEditor.tsx
│ │ ├── admin
│ │ │ ├── PackageVerification.tsx
│ │ │ └── PackageVerificationQueue.tsx
│ │ ├── browse
│ │ │ ├── ProjectList.tsx
│ │ │ ├── ProjectListItem.tsx
│ │ │ └── Projects.tsx
│ │ ├── editor
│ │ │ ├── DetatchButton.tsx
│ │ │ ├── PackageEditor.tsx
│ │ │ ├── PackageFields.tsx
│ │ │ ├── ProjectOnboardingForm.tsx
│ │ │ ├── ProjectPackage.tsx
│ │ │ ├── ProjectView.tsx
│ │ │ ├── UsageFields.tsx
│ │ │ ├── questions
│ │ │ │ ├── QuestionWidget.tsx
│ │ │ │ ├── RadioWidget.tsx
│ │ │ │ ├── SelectWidget.tsx
│ │ │ │ ├── TextWidget.tsx
│ │ │ │ └── index.ts
│ │ │ └── refs
│ │ │ │ ├── AddRelatedProjectModal.tsx
│ │ │ │ └── ProjectRefInfo.tsx
│ │ ├── packages
│ │ │ ├── PackageCard.tsx
│ │ │ ├── PackageCardUsage.tsx
│ │ │ └── PackageVerificationMark.tsx
│ │ ├── refs
│ │ │ └── CloneProject.tsx
│ │ └── render
│ │ │ ├── AttributionDocBuilder.tsx
│ │ │ ├── AttributionDocWarning.tsx
│ │ │ ├── TextAnnotator.tsx
│ │ │ └── TextLine.tsx
│ └── util
│ │ ├── EditableText.tsx
│ │ ├── FreeformSelect.tsx
│ │ ├── Modal.tsx
│ │ └── ToggleLink.tsx
├── ext.ts
├── extensions
│ ├── DemoFooter.ext.tsx
│ └── README.md
├── history.ts
├── modules
│ ├── common.ts
│ ├── licenses.ts
│ ├── packages.ts
│ └── projects.ts
├── reducers.ts
├── store.ts
├── tsconfig.json
└── util
│ ├── ExtensionPoint.tsx
│ ├── debounce.ts
│ ├── download.ts
│ ├── index.ts
│ └── viewport.ts
├── config
├── default.js
└── dev.js
├── docker-compose.dev.yml
├── docker-compose.selenium.yml
├── docker-compose.yml
├── docs
├── for-admins.md
├── for-users.md
├── openapi.yaml
└── schema.sql
├── package-lock.json
├── package.json
├── server
├── api
│ ├── routes-v1.ts
│ ├── routes.ts
│ └── v1
│ │ ├── licenses
│ │ ├── index.ts
│ │ └── interfaces.ts
│ │ ├── packages
│ │ ├── auth.ts
│ │ ├── index.ts
│ │ └── interfaces.ts
│ │ └── projects
│ │ ├── attribution.ts
│ │ ├── auth.spec.ts
│ │ ├── auth.ts
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ ├── interfaces.ts
│ │ └── validators.ts
├── app.ts
├── auth
│ ├── base.ts
│ ├── impl
│ │ └── nullauth.ts
│ ├── index.ts
│ ├── util.spec.ts
│ └── util.ts
├── config.ts
├── custom.ts
├── db
│ ├── attribution_documents.ts
│ ├── index.ts
│ ├── packages.ts
│ ├── projects.ts
│ └── projects_audit.ts
├── errors
│ └── index.ts
├── licenses
│ ├── index.ts
│ ├── interfaces.ts
│ ├── known.spec.ts
│ ├── known
│ │ ├── Apache-2.0.ts
│ │ ├── MIT.ts
│ │ ├── MyCustomLicense.ts
│ │ └── README.md
│ ├── tags.spec.ts
│ └── tags
│ │ ├── README.md
│ │ ├── all.ts
│ │ ├── fixed-text.ts
│ │ ├── linkage.ts
│ │ ├── modified.ts
│ │ ├── notice.ts
│ │ ├── popular.ts
│ │ ├── spdx.ts
│ │ ├── unknown.ts
│ │ ├── user-supplied.ts
│ │ └── validation-demo.ts
├── localserver.ts
└── util
│ ├── credentials.spec.ts
│ ├── credentials.ts
│ ├── idgen.ts
│ └── middleware.ts
├── spec
├── helpers
│ └── output.js
├── selenium
│ ├── driver.ts
│ ├── landing.spec.ts
│ ├── projects-auth.spec.ts
│ └── projects.spec.ts
└── support
│ └── jasmine.json
├── styles
├── bootstrap-overrides.scss
├── site.scss
└── style.scss
├── tsconfig.json
├── tslint.json
└── webpack.config.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | trim_trailing_whitespace = true
7 |
8 | [*.{css,js,jsx,ts,tsx,json,html}]
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *Issue #, if available:*
2 |
3 | *Description of changes:*
4 |
5 |
6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | node_modules/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | language: node_js
3 | services: docker
4 | node_js:
5 | - "12"
6 |
7 | before_install:
8 | - sudo apt-get update
9 | - sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
10 |
11 | script:
12 | - npm test
13 | - npm run test-ui
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html), as much as
6 | a website reasonably can. Backwards incompatible changes (requiring a major version bump) will be
7 | described here, and may involve database changes, significant workflow changes, or changes that
8 | require manual edits to pluggable interfaces.
9 |
10 | ## Unreleased
11 |
12 | ### Added
13 | - Added extension points to customize the look and behavior of client-side components. See
14 | the README in `browser/extensions` for info.
15 | - A global ACL is now available and can be set in your configuration as `globalACL`. Entries added
16 | to this list will implicitly apply to all projects. This will eventually replace the current
17 | admin functionality.
18 | - An API is available to fetch rendered/stored documents for projects.
19 |
20 | ### Changed
21 | - Now uses tiny-attribution-generator to build documents.
22 |
23 | ## 0.9.0 - 2017-12-11
24 |
25 | ### Added
26 | - Auth backends can now specify how a user should be authenticated, via Passport. They should
27 | provide an `initialize` method that is called during app start-up. This can be used to register
28 | Passport strategies, login URLs, or any other session activities.
29 | - SPDX license texts are now shipped with the attribution builder.
30 | - License tags can now specify presentation options to influence how they appear in the package
31 | editor. They can be sorted first, annotated with text (both in menu and below), and control
32 | whether users are asked for the full license text.
33 | - License tags can also specify "questions" to ask a user when adding a package. This is useful
34 | to gather context-sensitive info. For example, you could only ask for "dynamic/static linking"
35 | if relevant for a given license.
36 | - Added a user interface for editing project access lists. This can be accessed by clicking on
37 | the owner on the top right side of the projcet editor.
38 | - It is now possible to edit a package and usage information in a project. New package revisions
39 | will be created as necessary, and previous entries will be correctly cleaned up.
40 |
41 | ### Removed
42 | - JWT sessions are no longer in use. See the above addition about auth backends for an alternative.
43 | - The build process no longer requires Gulp.
44 |
45 | ### Changed
46 | - Project ACLs are now sanely validated, with levels of "owner", "editor", and "viewer". A viewer
47 | can only view a project. An editor can change project details, except for the ACL. An owner can
48 | change everything about a project.
49 | - Users on a project contact list implicitly have "viewer" permissions unless otherwire specified.
50 | - The format of `/api/licenses` changed. Instead of a list, it returns a {licenses, tags}
51 | structure. The license list is included in the `license` key.
52 |
53 | ### Fixed
54 | - Some lingering Bootstrap CSS issues were cleaned up.
55 | - The `validateUsage` function (used in tags) was incorrectly documented.
56 | - `extractRequestUser` is now consistently used, making custom auth backends more reliable.
57 |
58 | ### Security
59 | - Users who weren't configured to access package validation systems could still do so, due to
60 | a dangling `Promise`. Additional type checks and lints have been enabled to prevent this in the
61 | future.
62 |
63 | ## 0.8.0 - 2017-08-04
64 |
65 | - Initial release.
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Bugs
2 |
3 | Bug reports and feature suggestions are welcome. When filing a bug, try to include as much information as you can. Details like these are incredibly useful:
4 |
5 | * A reproducible test case or series of steps
6 | * The date/commit of the code you're running
7 | * Any modifications you've made relevant to the bug
8 | * Anything unusual about your environment or deployment
9 |
10 | # Pull Requests
11 |
12 | Pull requests are welcome!
13 |
14 | You should open an issue to discuss your pull request, unless it's a trivial change. It's best to ensure that your proposed change would be accepted so that you don't waste your own time.
15 |
16 | Pull requests should generally be opened against **master**.
17 |
18 | ## Tests
19 |
20 | Please ensure that your change still passes unit tests, and ideally integration/UI tests. It's OK if you're still working on tests at the time that you submit, but be prepared to be asked about them.
21 |
22 | ## Code Style
23 |
24 | Generally, match the style of the surrounding code. We ship an EditorConfig file for indentation and TSLint configuration for TypeScript code. Please ensure your changes don't wildly deviate from those rules. You can run `npm run lint` to identify and automatically fix most style issues.
25 |
26 | ## Code of Conduct
27 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
28 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
29 | opensource-codeofconduct@amazon.com with any additional questions or comments.
30 |
31 |
32 | ## Security issue notifications
33 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
34 |
35 |
36 | ## Licensing
37 |
38 | See the [LICENSE](https://github.com/amzn/oss-attribution-builder/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
39 |
40 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
41 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12 as build
2 | WORKDIR /build
3 |
4 | COPY ./package.json ./package-lock.json ./
5 | RUN NPM_CONFIG_LOGLEVEL=warn npm install
6 |
7 | COPY ./ ./
8 | RUN NODE_ENV=production npm run build
9 |
10 |
11 | FROM node:12
12 | ENV NODE_ENV production
13 | CMD ["node", "./server/localserver.js"]
14 | WORKDIR /opt/app
15 |
16 | COPY ./package.json ./package-lock.json ./
17 | RUN NPM_CONFIG_LOGLEVEL=warn npm install --production
18 | COPY --from=build /build/build/ ./
19 |
20 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | OSS Attribution Builder
2 | Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OSS Attribution Builder
2 | [](https://travis-ci.org/amzn/oss-attribution-builder)
3 |
4 | OSS Attribution Builder is a website that helps teams create attribution documents for software products. An attribution document is a text file, web page, or screen in just about every software application that lists the software components used and their licenses. They're often in the About screens, and are sometimes labeled "Open Source Notices", "Credits", or other similar jargon.
5 |
6 | [Screenshot](https://raw.github.com/amzn/oss-attribution-builder/screenshots/attribution-builder-project-example.png)
7 |
8 | ## Quickstart
9 |
10 | 1. Install [Docker](https://www.docker.com/)
11 | 2. Clone this repository
12 | 3. Run `docker-compose up`
13 | 4. Visit http://localhost:8000/
14 | * The demo uses HTTP basic auth. Enter any username and password. Use `admin` to test out admin functionality.
15 |
16 | ## Using the Website
17 |
18 | See documentation:
19 |
20 | * [For users](docs/for-users.md)
21 | * [For administrators](docs/for-admins.md)
22 |
23 | ## Caveats
24 |
25 | The attribution builder was originally an Amazon-internal tool. Some portions had to be removed to make this a sensible open source project. As such, there are some warts:
26 |
27 | * Projects have contact lists, but at the moment the UI only supports one contact (the legal contact).
28 |
29 | These will all be fixed in time, but be aware that some things might be weird for a while.
30 |
31 | ## Custom deployment
32 |
33 | If you're ready to integrate the attribution builder into your own environment, there are some things to set up:
34 |
35 | ### Configuration
36 |
37 | Open up [config/default.js](config/default.js) and poke around. This configuration launches when you run `docker-compose` or otherwise launch the application.
38 |
39 | ### Licenses
40 |
41 | The attribution builder has support for two types of license definitions:
42 |
43 | * SPDX identifiers
44 | * "Known" license texts and tags
45 |
46 | SPDX identifiers are just used for pre-filling the license selector, but do not (currently) have texts. The more useful type of license is a "known" license, where **you** (the administrator) supply the text of the license and any tags you'd like to apply.
47 |
48 | For information on adding your own "known" licenses, see [the license README](server/licenses/known/README.md). There are two existing licenses in the same directory you can look at for examples.
49 |
50 | #### Tags
51 |
52 | Tags allow you to add arbitrary validation rules to a license. They can be useful for:
53 |
54 | * Verifying a license is being used in the right way (e.g., LGPL and how a package was linked)
55 | * Annotating a particular license as needing follow up, if your business has special processes
56 | * Providing guidance on attribution for licenses with many variants
57 | * Modifying the how a license is displayed in an attribution document
58 |
59 | For information on what tags can do and how to create your own, see [the tags README](server/licenses/tags/README.md).
60 |
61 | ### Extensions
62 |
63 | The attribution builder offers some form of extensions that allow you to alter client-side site behavior and appearance, without needing to patch internals. This can make upgrades easier.
64 |
65 | See [the extensions README](browser/extensions/README.md) for details.
66 |
67 | ### Authentication module
68 |
69 | The attribution builder supports being able to restrict access to certain people or groups using project ACLs. These can also be used for administration and to "verify" packages (details on that in a later section). The default implementation `nullauth` is not very useful for most environments; you will want to write your own when launching more broadly.
70 |
71 | See [the base auth interface](server/auth/base.ts) for implementation details.
72 |
73 | ### Running
74 |
75 | To start up the server, you should run `build/server/localserver.js` after building with `npm run build`. There are some environment variables you'll probably want to set when running:
76 |
77 | * `NODE_ENV` should most likely be set to `production`
78 | * `CONFIG_NAME` should be set to the basename (no extension) of your configuration file you created above. The default is "default".
79 |
80 | The server runs in HTTP only. You probably want to put a thin HTTPS web server or proxy in front of it.
81 |
82 | ## Contributing
83 |
84 | See [CONTRIBUTING](CONTRIBUTING.md) for information.
85 |
86 | ### Development
87 |
88 | `npm install` and then `npm run dev` will get you off the ground for local development. This will start a Docker container for PostgreSQL, but will use a local copy of tsc, webpack, node, etc so you can iterate quickly.
89 |
90 | Once things have started up, you can open http://0.0.0.0:2425/webpack-dev-server/. This will automatically reload on browser changes, and the backend will also automatically restart on server-side changes.
91 |
92 | Handy environment variables:
93 |
94 | * `NODE_ENV`: when unset or `development`, you'll get full source maps & debug logs
95 | * `DEBUG_SQL`: when set (to anything), this will show SQL queries on the terminal as they execute
96 |
97 | #### Testing
98 |
99 | `npm test` will run unit tests. These are primarily server focused.
100 |
101 | `npm run test-ui` will run Selenium tests. You can set the environment variable `SELENIUM_DRIVER` if you want a custom driver -- by default, it'll try to use Chrome, and if that's not available it'll fall back to PhantomJS.
102 |
103 | When debugging UI tests, it may be easier to change `standalone-chrome` to `standalone-chrome-debug` in `docker-compose.selenium.yml`, and then connect to the container via VNC (port 5900, password "secret"). Run the container and your tests separately:
104 |
105 | * `docker-compose -f docker-compose.selenium.yml up --build`
106 | * `tsc && jasmine --stop-on-failure=true 'build/selenium/*.spec.js'`
107 |
108 | Tests failing for seemingly no reason? `driver.sleep` not working? Make sure your Jasmine timeout on your test is high enough.
109 |
--------------------------------------------------------------------------------
/assets/images/copyright-vs-license.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amzn/oss-attribution-builder/c8bc98e1d278a588324a02c8909cca2819f282d6/assets/images/copyright-vs-license.png
--------------------------------------------------------------------------------
/assets/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Attribution Builder
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/browser/app.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import 'bootstrap';
5 | import 'core-js/shim';
6 |
7 | import * as React from 'react';
8 | import { render } from 'react-dom';
9 | import { Provider } from 'react-redux';
10 | import { Route, Router } from 'react-router-dom';
11 |
12 | import App from './components/App';
13 | import history from './history';
14 | import store from './store';
15 |
16 | // routes listed here should point to redux-enabled containers
17 | window.addEventListener('DOMContentLoaded', () => {
18 | // see components/App.tsx for the rest of the routes
19 | render(
20 |
21 |
22 |
23 |
24 | ,
25 | document.getElementById('content')
26 | );
27 | });
28 |
29 | // @ts-ignore
30 | // load up extensions (webpack hook)
31 | const extCtx = require.context('./extensions', false, /.ext.[jt]sx?$/);
32 | extCtx.keys().forEach(extCtx);
33 |
--------------------------------------------------------------------------------
/browser/components/App.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { connect } from 'react-redux';
6 | import { NavLink, Route, Switch } from 'react-router-dom';
7 |
8 | import {
9 | fetchSiteInfo,
10 | setAdminMode,
11 | setGeneralError,
12 | } from '../modules/common';
13 | import ExtensionPoint from '../util/ExtensionPoint';
14 | import Landing from './Landing';
15 | import NotFound from './NotFound';
16 | import PackageVerification from './projects/admin/PackageVerification';
17 | import PackageVerificationQueue from './projects/admin/PackageVerificationQueue';
18 | import Projects from './projects/browse/Projects';
19 | import ProjectOnboardingForm from './projects/editor/ProjectOnboardingForm';
20 | import ProjectRouter from './projects/ProjectRouter';
21 | import Modal from './util/Modal';
22 | import ToggleLink from './util/ToggleLink';
23 |
24 | interface Props {
25 | dispatch: (action: any) => any;
26 | generalError: any;
27 | canAdmin: boolean;
28 | admin: boolean;
29 | }
30 |
31 | class App extends React.Component {
32 | componentWillMount() {
33 | const { dispatch } = this.props;
34 | dispatch(fetchSiteInfo());
35 | }
36 |
37 | dismissError = (actionName) => {
38 | const { dispatch } = this.props;
39 | dispatch(setGeneralError(undefined));
40 | };
41 |
42 | mapError(err) {
43 | let title = '';
44 | let explain = '';
45 |
46 | switch (err.code) {
47 | case 403:
48 | title = 'You might not have access to this resource';
49 | explain =
50 | 'If you think you need access to this item, contact the site administrator.';
51 | break;
52 |
53 | default:
54 | title = 'Something went wrong';
55 | explain = 'Please try that again.';
56 | break;
57 | }
58 |
59 | return (
60 |
61 | {(buttonAction) => (
62 | <>
63 |
64 |
65 | There was a problem:
66 |
67 | {err.message}
68 |
69 |
{explain}
70 |
71 |
72 |
76 | Close
77 |
78 |
79 | >
80 | )}
81 |
82 | );
83 | }
84 |
85 | toggleAdmin = () => {
86 | const { dispatch, admin } = this.props;
87 | dispatch(setAdminMode(!admin));
88 | };
89 |
90 | render() {
91 | const { generalError, canAdmin, admin } = this.props;
92 | return (
93 | <>
94 |
95 |
96 |
97 |
103 |
104 | Attribution Builder
105 |
106 |
107 |
108 |
109 |
110 |
116 | My Projects
117 |
118 |
119 |
120 |
126 | New Project
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | {generalError != undefined && this.mapError(generalError)}
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
147 |
148 |
153 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | {canAdmin && (
165 |
166 |
167 | Admin
168 |
169 |
170 | )}
171 |
172 |
173 |
174 |
175 |
176 |
177 | >
178 | );
179 | }
180 | }
181 |
182 | export default connect((state: any) => ({
183 | generalError: state.common.generalError,
184 | canAdmin:
185 | state.common.info.permissions && state.common.info.permissions.admin,
186 | admin: state.common.admin,
187 | }))(App);
188 |
--------------------------------------------------------------------------------
/browser/components/Landing.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { connect } from 'react-redux';
6 | import { Link } from 'react-router-dom';
7 |
8 | import { fetchSiteInfo } from '../modules/common';
9 | import ExtensionPoint from '../util/ExtensionPoint';
10 |
11 | interface Props {
12 | dispatch: any;
13 | admin?: boolean;
14 | displayName?: string;
15 | }
16 |
17 | class Landing extends React.Component {
18 | componentWillMount() {
19 | const { dispatch } = this.props;
20 | // this is a little dumb... but in order to display the current user,
21 | // we need to authenticate.
22 | dispatch(fetchSiteInfo());
23 | }
24 |
25 | render() {
26 | const { admin, displayName } = this.props;
27 |
28 | return (
29 | <>
30 |
31 |
{displayName ? `Hello, ${displayName}` : 'Hello'}
32 |
33 |
34 | This tool helps you build an attribution document to use in a
35 | distributed product.
36 |
37 |
38 | We organize attribution documents by project. You can create a new
39 | project or browse your projects below. We'll ask you for some
40 | basic details about your product, such as who your legal contact
41 | is and when you plan to distribute or launch. Then you'll build a
42 | list of all of the open source packages you use and their
43 | licenses. These packages and their licenses will form your
44 | attribution document.
45 |
46 |
47 |
48 |
49 | New Project
50 | {' '}
51 |
52 | My Projects
53 | {' '}
54 | {admin ? (
55 |
56 | All Projects
57 |
58 | ) : (
59 | ''
60 | )}
61 |
62 |
63 |
64 | >
65 | );
66 | }
67 | }
68 |
69 | export default connect((state: any) => ({
70 | displayName: state.common.info.displayName,
71 | admin: state.common.admin,
72 | }))(Landing as any);
73 |
--------------------------------------------------------------------------------
/browser/components/NotFound.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { match } from 'react-router';
6 |
7 | import ExtensionPoint from '../util/ExtensionPoint';
8 |
9 | interface Props {
10 | match: match;
11 | }
12 |
13 | const Landing: React.SFC = (props) => {
14 | return (
15 |
16 |
Not Found
17 |
18 |
19 | Page not found
20 |
21 | The resource you were looking for doesn't exist here. Check your
22 | location for typos and try again.
23 |
24 |
25 |
26 |
27 | );
28 | };
29 | export default Landing;
30 |
--------------------------------------------------------------------------------
/browser/components/projects/ProjectRouter.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { connect } from 'react-redux';
6 | import { Route, Switch } from 'react-router';
7 | import { Link } from 'react-router-dom';
8 |
9 | import { WebProject } from '../../../server/api/v1/projects/interfaces';
10 | import * as ProjectActions from '../../modules/projects';
11 | import ProjectAclEditor from './acl/ProjectAclEditor';
12 | import ProjectView from './editor/ProjectView';
13 | import CloneProject from './refs/CloneProject';
14 | import AttributionDocBuilder from './render/AttributionDocBuilder';
15 |
16 | interface Props {
17 | dispatch: (action: any) => any;
18 | match: any;
19 | project: WebProject;
20 | }
21 |
22 | class ProjectRouter extends React.Component {
23 | componentWillMount() {
24 | const {
25 | dispatch,
26 | match: { params },
27 | } = this.props;
28 | dispatch(ProjectActions.fetchProjectDetail(params.projectId));
29 | }
30 |
31 | componentWillUpdate(nextProps) {
32 | const {
33 | dispatch,
34 | match: { params },
35 | } = this.props;
36 | if (params.projectId === nextProps.match.params.projectId) {
37 | return;
38 | }
39 |
40 | dispatch(
41 | ProjectActions.fetchProjectDetail(nextProps.match.params.projectId)
42 | );
43 | }
44 |
45 | render() {
46 | const {
47 | project,
48 | match: {
49 | params: { projectId },
50 | },
51 | } = this.props;
52 |
53 | if (project == undefined || projectId !== project.projectId) {
54 | return Loading project information...
;
55 | }
56 |
57 | return (
58 |
59 |
60 |
61 | Project Editor
62 |
63 | (
66 |
67 | Attribution Document
68 |
69 | )}
70 | />
71 | (
74 | Access List
75 | )}
76 | />
77 | Clone }
80 | />
81 |
82 |
83 |
88 |
89 |
93 |
94 |
95 |
96 | );
97 | }
98 | }
99 |
100 | export default connect((state: any) => ({
101 | project: state.projects.active,
102 | }))(ProjectRouter);
103 |
--------------------------------------------------------------------------------
/browser/components/projects/acl/GroupSelect.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import Select, { Option } from 'react-select';
6 |
7 | interface Props {
8 | name: string;
9 | value: string;
10 | groups: string[];
11 | onChange: (value: string | undefined) => void;
12 | }
13 |
14 | export default function GroupSelect(props: Props) {
15 | return (
16 |
21 | props.onChange(sel ? (sel.value as string) : undefined)
22 | }
23 | />
24 | );
25 | }
26 |
27 | function mapGroups(groups) {
28 | return groups.map((group) => {
29 | const firstColon = group.indexOf(':');
30 | if (firstColon === -1) {
31 | return {
32 | value: group,
33 | label: group,
34 | };
35 | }
36 |
37 | // if colon-prefixed, assume it's a type of group (e.g., ldap, posix)
38 | const type = group.substring(0, firstColon);
39 | const name = group.substring(firstColon + 1);
40 | return {
41 | value: group,
42 | label: `${name} (${type})`,
43 | };
44 | });
45 | }
46 |
--------------------------------------------------------------------------------
/browser/components/projects/admin/PackageVerification.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 | import { connect } from 'react-redux';
7 |
8 | import * as PackageActions from '../../../modules/packages';
9 | import PackageCard from '../packages/PackageCard';
10 |
11 | interface Props {
12 | dispatch: (action: any) => any;
13 | match: any;
14 | packages: PackageActions.PackageSet;
15 | }
16 |
17 | interface State {
18 | verify_website: boolean;
19 | verify_license: boolean;
20 | verify_copyright: boolean;
21 | comments: string;
22 | }
23 |
24 | class PackageVerification extends Component {
25 | allChecked = false;
26 |
27 | state = {
28 | verify_website: false,
29 | verify_license: false,
30 | verify_copyright: false,
31 | comments: '',
32 | };
33 |
34 | changeEvent = (name: string) => {
35 | return (e: any) => {
36 | // read the value, unless it's a checkbox
37 | let val = e.currentTarget.value;
38 | if (e.currentTarget.checked !== undefined) {
39 | val = e.currentTarget.checked;
40 | }
41 |
42 | this.setState({ [name]: val } as State);
43 | };
44 | };
45 |
46 | renderVerifyOption = (name: string, text: string) => {
47 | const fullName = `verify_${name}`;
48 | return (
49 |
50 |
51 | {' '}
57 | {text}
58 |
59 |
60 | );
61 | };
62 |
63 | submitForm = (e: any) => {
64 | const {
65 | dispatch,
66 | match: {
67 | params: { packageId },
68 | },
69 | } = this.props;
70 | e.preventDefault();
71 | dispatch(
72 | PackageActions.verifyPackage(
73 | packageId,
74 | this.allChecked,
75 | this.state.comments
76 | )
77 | );
78 | };
79 |
80 | validate = () => {
81 | this.allChecked = ['website', 'license', 'copyright']
82 | .map((x) => this.state[`verify_${x}`])
83 | .reduce((a, b) => a && b, true);
84 |
85 | return this.allChecked || this.state.comments.trim().length > 0;
86 | };
87 |
88 | render() {
89 | const {
90 | match: {
91 | params: { packageId },
92 | },
93 | } = this.props;
94 |
95 | const valid = this.validate();
96 |
97 | return (
98 |
142 | );
143 | }
144 | }
145 |
146 | export default connect((state: any) => ({
147 | packages: state.packages.set,
148 | }))(PackageVerification);
149 |
--------------------------------------------------------------------------------
/browser/components/projects/admin/PackageVerificationQueue.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 | import { connect } from 'react-redux';
7 | import { Link } from 'react-router-dom';
8 |
9 | import { WebPackage } from '../../../../server/api/v1/packages/interfaces';
10 | import { fetchVerificationQueue } from '../../../modules/packages';
11 |
12 | interface Props {
13 | dispatch: (action: any) => any;
14 | queue: Array>;
15 | }
16 |
17 | class PackageVerificationQueue extends Component {
18 | componentDidMount() {
19 | const { dispatch } = this.props;
20 | dispatch(fetchVerificationQueue());
21 | }
22 |
23 | render() {
24 | const { queue } = this.props;
25 |
26 | return (
27 |
28 | Packages needing verification, in order of popularity
29 |
30 | {queue.map((pkg) => (
31 |
32 |
33 |
34 | {pkg.name} {pkg.version}
35 |
36 |
37 |
38 | {pkg.extra!.stats!.numProjects} project
39 | {pkg.extra!.stats!.numProjects !== 1 && 's'}
40 |
41 |
42 | ))}
43 |
44 |
45 | );
46 | }
47 | }
48 |
49 | export default connect((state: any) => ({
50 | queue: state.packages.verificationQueue,
51 | }))(PackageVerificationQueue);
52 |
--------------------------------------------------------------------------------
/browser/components/projects/browse/ProjectList.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 | import ProjectListItem from './ProjectListItem';
7 |
8 | interface Props {
9 | projects: any[];
10 | }
11 |
12 | export default class ProjectList extends Component {
13 | render() {
14 | return (
15 |
16 | {this.props.projects.map((project, index) => (
17 |
18 | ))}
19 |
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/browser/components/projects/browse/ProjectListItem.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 | import { Link } from 'react-router-dom';
7 |
8 | interface Props {
9 | projectId: string;
10 | title: string;
11 | version: string;
12 | createdOn: string;
13 | }
14 |
15 | export default class ProjectListItem extends Component {
16 | render() {
17 | return (
18 |
19 |
20 | {this.props.title} {this.props.version}
21 |
22 | created on {this.props.createdOn}
23 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/browser/components/projects/browse/Projects.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 | import { connect } from 'react-redux';
7 |
8 | import * as ProjectActions from '../../../modules/projects';
9 | import ProjectList from './ProjectList';
10 |
11 | interface Props {
12 | dispatch: (action: any) => any;
13 | location?: any;
14 | projects: any[];
15 | }
16 |
17 | class Projects extends Component {
18 | componentWillMount() {
19 | const { dispatch, location } = this.props;
20 | dispatch(ProjectActions.fetchProjects(location.search));
21 | }
22 |
23 | componentWillUpdate(nextProps) {
24 | const { dispatch, location } = this.props;
25 | if (location.search === nextProps.location.search) {
26 | return;
27 | }
28 |
29 | dispatch(ProjectActions.fetchProjects(nextProps.location.search));
30 | }
31 |
32 | render() {
33 | const { projects } = this.props;
34 | return ;
35 | }
36 | }
37 |
38 | export default connect((state: any) => {
39 | return {
40 | projects: state.projects.list,
41 | };
42 | })(Projects);
43 |
--------------------------------------------------------------------------------
/browser/components/projects/editor/DetatchButton.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import React = require('react');
5 |
6 | interface Props {
7 | className?: string;
8 | onClick: (event?: any) => any;
9 | }
10 |
11 | interface State {
12 | mode: ConfirmState;
13 | }
14 |
15 | enum ConfirmState {
16 | Initial,
17 | Lockout,
18 | Confirm,
19 | }
20 |
21 | export default class DetatchButton extends React.Component {
22 | timeout: any;
23 |
24 | constructor(props) {
25 | super(props);
26 |
27 | this.state = {
28 | mode: ConfirmState.Initial,
29 | };
30 | }
31 |
32 | clicked = (e) => {
33 | const { mode } = this.state;
34 | if (mode === ConfirmState.Initial) {
35 | this.setState({ mode: ConfirmState.Lockout });
36 | setTimeout(() => {
37 | this.setState({ mode: ConfirmState.Confirm });
38 | this.timeout = setTimeout(() => {
39 | this.setState({ mode: ConfirmState.Initial });
40 | }, 3000);
41 | }, 500);
42 | } else if (mode === ConfirmState.Confirm) {
43 | clearTimeout(this.timeout);
44 | this.props.onClick();
45 | }
46 | };
47 |
48 | render() {
49 | const { className } = this.props;
50 | const { mode } = this.state;
51 |
52 | return (
53 |
60 | {mode === ConfirmState.Confirm ? (
61 | 'Delete?'
62 | ) : (
63 |
64 | )}
65 |
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/browser/components/projects/editor/ProjectPackage.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import React = require('react');
5 | import { connect } from 'react-redux';
6 |
7 | import { WebTag } from '../../../../server/api/v1/licenses/interfaces';
8 | import { WebPackage } from '../../../../server/api/v1/packages/interfaces';
9 | import { PackageUsage } from '../../../../server/api/v1/projects/interfaces';
10 | import * as PackageActions from '../../../modules/packages';
11 | import * as ProjectActions from '../../../modules/projects';
12 | import PackageCard from '../packages/PackageCard';
13 | import DetatchButton from './DetatchButton';
14 |
15 | const DeltaFields: Array<[keyof WebPackage, string]> = [
16 | ['website', 'Website'],
17 | ['license', 'License'],
18 | ['licenseText', 'License text'],
19 | ['copyright', 'Copyright/NOTICE'],
20 | ];
21 |
22 | interface OwnProps {
23 | usage: PackageUsage;
24 | onEditPackage: () => void;
25 | }
26 |
27 | interface Props extends OwnProps {
28 | dispatch: (action: any) => any;
29 | project: any;
30 | packages: PackageActions.PackageSet;
31 | tags: { [key: string]: WebTag };
32 | }
33 |
34 | interface State {
35 | showDelta: boolean;
36 | }
37 |
38 | class ProjectPackage extends React.Component {
39 | constructor(props) {
40 | super(props);
41 |
42 | this.state = {
43 | showDelta: false,
44 | };
45 | }
46 |
47 | detachPackage = () => {
48 | const { dispatch, project, usage } = this.props;
49 | dispatch(
50 | ProjectActions.detachPackageFromProject(
51 | project.projectId,
52 | usage.packageId
53 | )
54 | );
55 | };
56 |
57 | showDelta = (e) => {
58 | const { dispatch, packages, usage } = this.props;
59 |
60 | // at this point, we have the extra section already, so fetch the lastest revision
61 | const pkg = packages[usage.packageId];
62 | dispatch(PackageActions.fetchPackage(pkg.extra!.latest!));
63 |
64 | this.setState({ showDelta: true });
65 | };
66 |
67 | replacePackage = (newId: number) => {
68 | const { dispatch, project, usage } = this.props;
69 | dispatch(
70 | ProjectActions.replacePackageForProject(
71 | project.projectId,
72 | usage.packageId,
73 | newId
74 | )
75 | );
76 | this.setState({ showDelta: false });
77 | };
78 |
79 | render() {
80 | const { usage, packages, onEditPackage } = this.props;
81 | const { showDelta } = this.state;
82 |
83 | const buttons = [
84 |
89 |
90 | ,
91 | ,
96 | ];
97 |
98 | // this "update" functionality isn't in PackageCard because it relates to usage
99 | // in this project, and isn't applicable to other package views
100 |
101 | // see if the we have newer metadata available
102 | const pkg = packages[usage.packageId];
103 | if (
104 | pkg != undefined &&
105 | pkg.extra != undefined &&
106 | pkg.extra.latest != undefined &&
107 | pkg.extra.latest !== usage.packageId
108 | ) {
109 | // add an update button
110 | buttons.unshift(
111 |
116 | Update
117 |
118 | );
119 | }
120 |
121 | // show a delta of the changes
122 | let child;
123 | if (showDelta) {
124 | child = {this.renderDelta()}
;
125 | }
126 |
127 | return (
128 |
129 | {child}
130 |
131 | );
132 | }
133 |
134 | renderDelta() {
135 | const { usage, packages } = this.props;
136 | const oldPkg = packages[usage.packageId];
137 | const newPkg = packages[oldPkg.extra!.latest!];
138 |
139 | if (newPkg == undefined) {
140 | return 'Loading updated metadata...';
141 | }
142 |
143 | const listElements: JSX.Element[] = [];
144 | for (const [field, label] of DeltaFields) {
145 | if (oldPkg[field] !== newPkg[field]) {
146 | listElements.push(
147 | {label} ,
148 |
149 | {newPkg[field]}
150 |
151 | );
152 | }
153 | }
154 |
155 | return (
156 |
157 |
158 | We have updated information available for{' '}
159 |
160 | {oldPkg.name} {oldPkg.version}
161 |
162 | . These changes include:
163 |
164 |
{listElements}
165 |
166 | If this looks correct, you can apply these changes to your project:
167 |
168 |
this.replacePackage(newPkg.packageId)}
171 | >
172 | Accept changes
173 |
174 |
175 | );
176 | }
177 | }
178 |
179 | export default connect((state: any, props: OwnProps) => ({
180 | project: state.projects.active,
181 | packages: state.packages.set,
182 | tags: state.licenses.tags,
183 | }))(ProjectPackage);
184 |
--------------------------------------------------------------------------------
/browser/components/projects/editor/UsageFields.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import React = require('react');
5 |
6 | import { PackageUsage } from '../../../../server/api/v1/projects/interfaces';
7 | import { TagQuestions } from '../../../../server/licenses/interfaces';
8 | import QuestionWidget from './questions/QuestionWidget';
9 |
10 | interface Props {
11 | initial?: Partial;
12 | onChange: (usage: Partial) => void;
13 | questions: TagQuestions;
14 | }
15 |
16 | interface State {
17 | usage: Partial;
18 | }
19 |
20 | export default class UsageFields extends React.Component {
21 | static defaultProps = {
22 | initial: {},
23 | };
24 |
25 | constructor(props) {
26 | super(props);
27 | this.state = {
28 | usage: { ...this.props.initial },
29 | };
30 | }
31 |
32 | handleChange = (name, val) => {
33 | this.setState(
34 | {
35 | usage: {
36 | ...this.state.usage,
37 | [name]: val,
38 | },
39 | },
40 | () => {
41 | this.props.onChange(this.state.usage);
42 | }
43 | );
44 | };
45 |
46 | renderQuestion = (name: string, i: number) => {
47 | const { questions } = this.props;
48 | return (
49 | this.handleChange(name, val)}
55 | />
56 | );
57 | };
58 |
59 | render() {
60 | const { questions } = this.props;
61 | const { usage } = this.state;
62 |
63 | return (
64 |
65 |
{Object.keys(questions).map(this.renderQuestion)}
66 |
67 |
68 |
69 | Additional comments
70 |
71 |
72 |
80 |
81 |
82 | );
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/browser/components/projects/editor/questions/QuestionWidget.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { BaseProps, BaseWidget } from './index';
6 | import RadioWidget from './RadioWidget';
7 | import SelectWidget from './SelectWidget';
8 | import TextWidget from './TextWidget';
9 |
10 | export default class QuestionWidget extends BaseWidget {
11 | render() {
12 | const { question } = this.props;
13 |
14 | let Widget;
15 | if (question.widget === 'radio') {
16 | Widget = RadioWidget;
17 | } else if (question.widget === 'select') {
18 | Widget = SelectWidget;
19 | } else if (question.widget === 'text') {
20 | Widget = TextWidget;
21 | }
22 |
23 | return ;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/browser/components/projects/editor/questions/RadioWidget.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { BaseProps, BaseWidget } from './index';
6 |
7 | export default class RadioWidget extends BaseWidget {
8 | private handleChange = (e) => {
9 | const val = this.coerceType(e.target.value);
10 | this.props.onChange(val);
11 | };
12 |
13 | private renderOption = (opt: [string | number | boolean, string]) => {
14 | const { name, value } = this.props;
15 | const [optVal, optLabel] = opt;
16 | return (
17 |
18 |
19 | {' '}
27 | {optLabel}
28 |
29 |
30 | );
31 | };
32 |
33 | render() {
34 | const { question } = this.props;
35 |
36 | return (
37 |
38 |
{question.label}
39 |
40 | {question.options && question.options.map(this.renderOption)}
41 |
42 |
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/browser/components/projects/editor/questions/SelectWidget.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { BaseProps, BaseWidget } from './index';
6 |
7 | export default class SelectWidget extends BaseWidget {
8 | private handleChange = (e) => {
9 | const val = this.coerceType(e.target.value);
10 | this.props.onChange(val);
11 | };
12 |
13 | private renderOption = (opt: [string | number | boolean, string]) => {
14 | const [optVal, optLabel] = opt;
15 | return (
16 |
17 | {optLabel}
18 |
19 | );
20 | };
21 |
22 | render() {
23 | const { question, name, value } = this.props;
24 |
25 | return (
26 |
27 |
{question.label}
28 |
29 |
35 | {question.options && question.options.map(this.renderOption)}
36 |
37 |
38 |
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/browser/components/projects/editor/questions/TextWidget.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { BaseProps, BaseWidget } from './index';
6 |
7 | export default class TextWidget extends BaseWidget {
8 | private handleChange = (e) => {
9 | const val = this.coerceType(e.target.value);
10 | this.props.onChange(val);
11 | };
12 |
13 | render() {
14 | const { question, name, value } = this.props;
15 |
16 | return (
17 |
18 |
{question.label}
19 |
20 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/browser/components/projects/editor/questions/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { TagQuestion } from '../../../../../server/licenses/interfaces';
6 |
7 | export interface BaseProps {
8 | name: string;
9 | question: TagQuestion;
10 | value: string | boolean | number;
11 | onChange: (val: string | boolean | number) => any;
12 | }
13 |
14 | export class BaseWidget extends React.Component {
15 | protected coerceType(val: string): any {
16 | const {
17 | question: { type },
18 | } = this.props;
19 |
20 | let out: any;
21 | if (type === 'string') {
22 | out = val;
23 | } else if (type === 'boolean') {
24 | out = val === 'true' || val === '1' || val === 'yes';
25 | } else if (type === 'number') {
26 | out = Number.parseInt(val);
27 | }
28 |
29 | return out;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/browser/components/projects/editor/refs/AddRelatedProjectModal.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { connect } from 'react-redux';
6 | import { createRef } from '../../../../modules/projects';
7 | import Modal from '../../../util/Modal';
8 |
9 | interface OwnProps {
10 | projectId: string;
11 | onDismiss: (action: string) => void;
12 | }
13 |
14 | interface Props extends OwnProps {
15 | dispatch: (action: any) => any;
16 | }
17 |
18 | class AddRelatedProjectModal extends React.Component {
19 | private prefix;
20 |
21 | constructor(props) {
22 | super(props);
23 | this.prefix = `${window.location.origin}/projects/`;
24 | }
25 |
26 | handleAction = (action: string) => {
27 | this.props.onDismiss(action);
28 | };
29 |
30 | submitForm = (addAction) => async (e: React.FormEvent) => {
31 | const { dispatch, projectId } = this.props;
32 | e.preventDefault();
33 |
34 | const url: string = e.target['relate-project-url'].value.trim();
35 | const include: boolean = e.target['relate-project-include'].checked;
36 | const comment: string = e.target['relate-project-comments'].value.trim();
37 | const targetProjectId = url.replace(this.prefix, '').split('/')[0];
38 |
39 | await dispatch(
40 | createRef(
41 | projectId,
42 | targetProjectId,
43 | include ? 'includes' : 'related',
44 | comment
45 | )
46 | );
47 | addAction();
48 | };
49 |
50 | render() {
51 | return (
52 |
53 | {(buttonAction) => (
54 |
121 | )}
122 |
123 | );
124 | }
125 | }
126 |
127 | export default connect((state: any, props: OwnProps) => ({}))(
128 | AddRelatedProjectModal
129 | );
130 |
--------------------------------------------------------------------------------
/browser/components/projects/editor/refs/ProjectRefInfo.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { connect } from 'react-redux';
6 | import { Link } from 'react-router-dom';
7 |
8 | import { WebProject } from '../../../../../server/api/v1/projects/interfaces';
9 | import { DbProjectRef } from '../../../../../server/db/projects';
10 | import * as ProjectActions from '../../../../modules/projects';
11 | import { deleteRef } from '../../../../modules/projects';
12 | import DetatchButton from '../DetatchButton';
13 |
14 | interface Props {
15 | project: WebProject & { refInfo: any };
16 | dispatch: (action: any) => any;
17 | }
18 |
19 | class ProjectRefInfo extends React.Component {
20 | async componentDidMount() {
21 | const { dispatch, project } = this.props;
22 |
23 | await dispatch(ProjectActions.getRefInfo(project.projectId));
24 | }
25 |
26 | detatchProjectRef = (targetProjectId: string) => {
27 | const {
28 | dispatch,
29 | project: { projectId },
30 | } = this.props;
31 | dispatch(deleteRef(projectId, targetProjectId));
32 | };
33 |
34 | prettyRefType(type: DbProjectRef['type']) {
35 | switch (type) {
36 | case 'cloned_from':
37 | return 'cloned from';
38 | case 'related':
39 | return 'related to';
40 | case 'includes':
41 | return 'including packages from';
42 | default:
43 | break;
44 | }
45 | }
46 |
47 | render() {
48 | const { project } = this.props;
49 |
50 | if (project == undefined || project.refInfo == undefined) {
51 | return Loading ;
52 | }
53 |
54 | return Object.keys(project.refs).map((targetProjectId) =>
55 | this.renderRef(targetProjectId)
56 | );
57 | }
58 |
59 | renderRef(targetProjectId) {
60 | const { project } = this.props;
61 |
62 | const targetProject = project.refInfo.refs[targetProjectId];
63 | const ref = project.refs[targetProjectId];
64 |
65 | // not yet loaded
66 | if (targetProject == undefined) {
67 | return (
68 |
69 | {ref.type} #{targetProjectId}
70 |
71 | );
72 | }
73 |
74 | return (
75 |
76 | {this.prettyRefType(ref.type)}{' '}
77 |
78 | '{targetProject.title}' version {targetProject.version}
79 |
80 | {project.access.canEdit && (
81 | <>
82 | {' '}
83 | this.detatchProjectRef(targetProjectId)}
86 | />
87 | >
88 | )}
89 | {ref.comment && (
90 | <>
91 |
92 | {ref.comment}
93 | >
94 | )}
95 |
96 | );
97 | }
98 | }
99 |
100 | export default connect((state: any) => ({
101 | project: state.projects.active,
102 | }))(ProjectRefInfo);
103 |
--------------------------------------------------------------------------------
/browser/components/projects/packages/PackageCard.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 | import { connect } from 'react-redux';
7 |
8 | import { WebPackage } from '../../../../server/api/v1/packages/interfaces';
9 | import { fetchPackage, PackageSet } from '../../../modules/packages';
10 | import { triggerOnVisible } from '../../../util/viewport';
11 | import PackageCardUsage, { Props as UsageProps } from './PackageCardUsage';
12 | import PackageVerificationMark from './PackageVerificationMark';
13 |
14 | interface Props {
15 | dispatch: (action: any) => any;
16 | packageId: number;
17 | packages: PackageSet;
18 | usage?: UsageProps;
19 | buttons?: any[];
20 | preStyle?: React.CSSProperties;
21 | }
22 |
23 | class PackageCard extends Component {
24 | private ref?: HTMLElement;
25 | extendedFetched: boolean = false;
26 |
27 | static defaultProps = {
28 | packages: {},
29 | preStyle: { overflow: 'auto', maxHeight: '150px' },
30 | };
31 |
32 | componentWillMount() {
33 | const { dispatch, packageId } = this.props;
34 | dispatch(fetchPackage(packageId));
35 | }
36 |
37 | componentDidMount() {
38 | triggerOnVisible(this.ref!, () => {
39 | this.fetchExtended();
40 | });
41 | }
42 |
43 | componentWillUpdate(nextProps) {
44 | const { dispatch, packageId } = this.props;
45 | if (nextProps.packageId === packageId) {
46 | return;
47 | }
48 |
49 | dispatch(fetchPackage(nextProps.packageId));
50 | }
51 |
52 | fetchExtended = () => {
53 | const { dispatch, packageId, packages } = this.props;
54 | if (packages[packageId] && this.extendedFetched) {
55 | return;
56 | }
57 | this.extendedFetched = true;
58 | dispatch(fetchPackage(packageId, true));
59 | };
60 |
61 | render() {
62 | const { packageId, packages, usage, buttons, preStyle } = this.props;
63 | const pkg = packages[packageId] || ({} as WebPackage);
64 |
65 | return (
66 | {
69 | if (r) {
70 | this.ref = r;
71 | }
72 | }}
73 | >
74 |
75 |
76 |
77 |
82 |
83 |
{buttons}
84 |
85 |
86 |
87 |
88 | {pkg.name}{' '}
89 |
90 | {pkg.version}
91 | {pkg.license}
92 |
93 |
94 |
95 | {this.props.children}
96 |
97 | {usage != undefined &&
}
98 |
99 |
100 |
101 |
{pkg.copyright}
102 |
103 | {pkg.licenseText != undefined && pkg.licenseText !== '' && (
104 |
{pkg.licenseText}
105 | )}
106 |
107 |
108 | );
109 | }
110 | }
111 |
112 | export default connect((state: any) => ({
113 | packages: state.packages.set,
114 | }))(PackageCard);
115 |
--------------------------------------------------------------------------------
/browser/components/projects/packages/PackageCardUsage.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 |
7 | export interface Props {
8 | notes?: string;
9 | [key: string]: string | boolean | number | undefined;
10 | }
11 |
12 | export default class PackageCardUsage extends Component {
13 | render() {
14 | const { notes } = this.props;
15 |
16 | const usage = Object.keys(this.props)
17 | .filter((prop) => !['packageId', 'notes'].includes(prop))
18 | .map((prop) => `${prop}: ${this.props[prop]}`)
19 | .join('; ');
20 |
21 | return (
22 |
23 | {usage &&
In this project: {usage} }
24 |
25 | {notes}
26 |
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/browser/components/projects/packages/PackageVerificationMark.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 |
7 | import { WebPackage } from '../../../../server/api/v1/packages/interfaces';
8 |
9 | interface Props {
10 | pkg: WebPackage;
11 | }
12 |
13 | // There's a small chance the tooltip will get stuck saying "loading"
14 | // due to the fact that popovers/tooltips are basically fixed once
15 | // created. The logic in componentDidUpdate basically recreates it
16 | // for every update, but it won't update the active popover.
17 | // I know this logic sucks, but bootstrap just doesn't play very well
18 | // with things that need to dynamically update.
19 |
20 | export default class PackageVerificationMark extends Component {
21 | private self?: HTMLElement;
22 |
23 | componentDidMount() {
24 | $(this.self as any).tooltip();
25 | }
26 |
27 | componentDidUpdate() {
28 | $(this.self as any)
29 | .find('[data-toggle="popover"]')
30 | .popover({ placement: 'bottom', container: 'body', trigger: 'click' });
31 | }
32 |
33 | renderInner = () => {
34 | const { pkg } = this.props;
35 |
36 | // don't render if we don't have everything loaded
37 | if (
38 | pkg == undefined ||
39 | pkg.verified == undefined ||
40 | pkg.extra == undefined
41 | ) {
42 | return (
43 | {
45 | if (r) {
46 | this.self = r;
47 | }
48 | }}
49 | />
50 | );
51 | }
52 |
53 | let content = 'Loading details...';
54 | const extra = pkg.extra.verification;
55 | if (extra) {
56 | content = `Reviewed by ${extra.verifiedBy}.`;
57 | if (extra.comments.length > 0) {
58 | content += '\n\nComments: ' + extra.comments;
59 | }
60 | }
61 |
62 | if (pkg.verified) {
63 | return (
64 |
70 |
71 |
72 | );
73 | } else {
74 | return (
75 |
81 |
82 |
83 | );
84 | }
85 | };
86 |
87 | render() {
88 | return (
89 | {
91 | if (r) {
92 | this.self = r;
93 | }
94 | }}
95 | style={{ cursor: 'pointer' }}
96 | data-toggle="tooltip"
97 | data-placement="right"
98 | title="Click for details"
99 | className="mr-2"
100 | >
101 | {this.renderInner()}
102 |
103 | );
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/browser/components/projects/refs/CloneProject.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { connect } from 'react-redux';
6 |
7 | import { Link } from 'react-router-dom';
8 | import {
9 | AccessLevel,
10 | WebProject,
11 | } from '../../../../server/api/v1/projects/interfaces';
12 | import { cloneProject } from '../../../modules/projects';
13 | import GroupSelect from '../acl/GroupSelect';
14 | // import history from '../../../history';
15 | // import * as ProjectActions from '../../../modules/projects';
16 |
17 | interface Props {
18 | dispatch: (action: any) => any;
19 | project: WebProject;
20 | groups: string[];
21 | }
22 |
23 | interface State {
24 | newTitle: string;
25 | newVersion: string;
26 | newOwner?: string;
27 | keepPermissions: boolean;
28 | }
29 |
30 | class CloneProject extends React.Component {
31 | constructor(props: Props) {
32 | super(props);
33 |
34 | const project = props.project || {};
35 |
36 | this.state = {
37 | newTitle: `${project.title} (copy)` || '',
38 | newVersion: project.version || '',
39 | newOwner: undefined,
40 | keepPermissions: true,
41 | };
42 | }
43 |
44 | onTitleChange = (e) => {
45 | this.setState({ newTitle: e.target.value });
46 | };
47 | onVersionChange = (e) => {
48 | this.setState({ newVersion: e.target.value });
49 | };
50 | onKeepPermissionsChange = (e) => {
51 | this.setState({ keepPermissions: e.target.checked });
52 | };
53 |
54 | cloneSubmit = (e) => {
55 | const {
56 | dispatch,
57 | project,
58 | project: { projectId },
59 | } = this.props;
60 | const { newTitle, newVersion, newOwner, keepPermissions } = this.state;
61 |
62 | e.preventDefault();
63 |
64 | const newAcl = keepPermissions ? { ...project.acl } : {};
65 | newAcl[newOwner!] = 'owner' as AccessLevel;
66 |
67 | dispatch(
68 | cloneProject(projectId, {
69 | title: newTitle,
70 | version: newVersion,
71 | acl: newAcl,
72 | })
73 | );
74 | };
75 |
76 | formIsComplete = (): boolean => {
77 | const { newTitle, newVersion, newOwner } = this.state;
78 | return (
79 | newTitle.length > 0 && newVersion.length > 0 && newOwner != undefined
80 | );
81 | };
82 |
83 | render() {
84 | const { project } = this.props;
85 | const { newTitle, newVersion, keepPermissions } = this.state;
86 |
87 | return (
88 |
182 | );
183 | }
184 | }
185 |
186 | export default connect((state: any) => ({
187 | project: state.projects.active,
188 | groups: state.common.info.groups ? state.common.info.groups : [],
189 | }))(CloneProject);
190 |
--------------------------------------------------------------------------------
/browser/components/projects/render/AttributionDocBuilder.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 | import { connect } from 'react-redux';
7 |
8 | import * as ProjectActions from '../../../modules/projects';
9 | import AttributionDocWarning from './AttributionDocWarning';
10 | import TextAnnotator from './TextAnnotator';
11 |
12 | interface Props {
13 | dispatch: (action: any) => any;
14 | attributionDoc: any;
15 | project: any;
16 | }
17 |
18 | interface State {
19 | highlights: any[];
20 | }
21 |
22 | class AttributionDocBuilder extends Component {
23 | state = {
24 | highlights: [],
25 | };
26 | componentWillMount() {
27 | const {
28 | dispatch,
29 | project: { projectId },
30 | } = this.props;
31 | dispatch(ProjectActions.buildAttributionDoc(projectId));
32 | }
33 |
34 | saveAndDownload = async () => {
35 | const {
36 | dispatch,
37 | project: { projectId },
38 | } = this.props;
39 | await dispatch(ProjectActions.storeAttributionDoc(projectId));
40 | };
41 |
42 | /**
43 | * Create an on-click handler that will highlight an annotation
44 | * based on a warning structure.
45 | */
46 | bindWarning = (warning) => {
47 | const { annotations } = this.props.attributionDoc;
48 |
49 | // XXX: this aint hella efficient
50 | let found;
51 | for (const a of annotations) {
52 | if (AttributionDocBuilder.annotationMatch(a, warning)) {
53 | found = a;
54 | break;
55 | }
56 | }
57 |
58 | if (found == undefined) {
59 | return () => undefined;
60 | }
61 |
62 | return (event) => {
63 | this.setState({ highlights: [found] });
64 | };
65 | };
66 |
67 | static annotationMatch(annotation, warning) {
68 | if (annotation.license != undefined) {
69 | return annotation.license === warning.license;
70 | } else if (annotation.uuid != undefined) {
71 | return annotation.uuid === warning.package;
72 | }
73 |
74 | return false;
75 | }
76 |
77 | render() {
78 | const {
79 | attributionDoc: { lines, warnings },
80 | project: { title, version },
81 | } = this.props;
82 | const { highlights } = this.state;
83 |
84 | return (
85 |
86 |
87 | {title} version {version}
88 |
89 |
90 |
Below is a preview of your attribution document.
91 |
92 |
93 |
94 | To store a permanent copy, click the Save button below.
95 | {' '}
96 | You can create as many copies as you want and make edits after storing
97 | a copy. When you have a final version, save it here. You'll get a
98 | download as a text file and we'll store a rendered copy in our
99 | database.
100 |
101 |
106 | Save & Download
107 |
108 |
109 | {warnings.length > 0 ? (
110 |
111 |
Warnings and Notes
112 |
113 | Your document generated some warnings. Review these with your
114 | legal contact. You can click on a warning to highlight the
115 | relevant sections in your document.
116 |
117 | {warnings.map((warning, index) => (
118 |
123 | ))}
124 |
125 | ) : (
126 | ''
127 | )}
128 |
129 |
Document Preview
130 |
131 |
132 | {lines}
133 |
134 |
135 |
136 | );
137 | }
138 | }
139 |
140 | export default connect((state: any) => {
141 | return {
142 | attributionDoc: state.projects.attributionDoc,
143 | project: state.projects.active,
144 | };
145 | })(AttributionDocBuilder);
146 |
--------------------------------------------------------------------------------
/browser/components/projects/render/AttributionDocWarning.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 |
7 | interface Props {
8 | warning?: any;
9 | onClick?: any;
10 | }
11 |
12 | export default class AttributionDocWarning extends Component {
13 | render() {
14 | const { warning, onClick } = this.props;
15 | const { level, message } = warning;
16 |
17 | // figure out what this is all about
18 | let thing;
19 | if (warning.license != undefined) {
20 | thing = 'used license';
21 | } else if (warning.package != undefined) {
22 | thing = `package ${warning.label}`;
23 | } else {
24 | thing = 'project';
25 | }
26 |
27 | // build a message based on severity
28 | let title;
29 | let css;
30 | switch (level) {
31 | case 0:
32 | title = `Problem in ${thing}`;
33 | css = 'danger';
34 | break;
35 | case 1:
36 | title = `Potential issue in ${thing}`;
37 | css = 'warning';
38 | break;
39 | case 2:
40 | title = `Note for ${thing}`;
41 | css = 'info';
42 | break;
43 | }
44 |
45 | return (
46 |
47 |
{title}
48 |
{message}
49 |
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/browser/components/projects/render/TextAnnotator.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 | import TextLine from './TextLine';
7 |
8 | interface Props {
9 | highlights: any[];
10 | }
11 |
12 | export default class TextAnnotator extends Component {
13 | /**
14 | * From selected annotations (highlights), return a set of lines
15 | * that should be highlighted. "Unrolls" line ranges.
16 | */
17 | getHighlightedLines = () => {
18 | const { highlights } = this.props;
19 | const lines = new Set();
20 |
21 | for (const annotation of highlights) {
22 | const [lower, upper] = annotation.lines;
23 | for (let i = lower; i < upper; i++) {
24 | lines.add(i);
25 | }
26 | }
27 |
28 | return lines;
29 | };
30 |
31 | render() {
32 | const { children } = this.props;
33 |
34 | const highlightedLines = this.getHighlightedLines();
35 |
36 | const mapLines = (lines) => {
37 | let firstHighlight = false;
38 |
39 | return lines.map((a, i) => {
40 | const highlight = highlightedLines.has(i);
41 |
42 | // set the scroll marker for the first highlight
43 | if (highlight && !firstHighlight) {
44 | firstHighlight = true;
45 | return (
46 |
47 | {a}
48 |
49 | );
50 | }
51 |
52 | return (
53 |
57 | {a}
58 |
59 | );
60 | });
61 | };
62 |
63 | return {mapLines(children)} ;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/browser/components/projects/render/TextLine.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 |
7 | interface Props {
8 | highlight?: string;
9 | scrollTo?: boolean;
10 | children?: any;
11 | }
12 |
13 | export default class TextLine extends Component {
14 | render() {
15 | const { children, highlight, scrollTo } = this.props;
16 |
17 | const classes = highlight != undefined ? highlight : '';
18 |
19 | function scrollRef(r) {
20 | if (r == undefined) {
21 | return;
22 | }
23 | window.scrollTo(0, r.offsetTop);
24 | }
25 |
26 | return (
27 |
28 | {children.length > 0 ? children : '\n'}
29 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/browser/components/util/EditableText.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 |
7 | interface Props {
8 | editor?: any;
9 | value: string;
10 | onChange: any;
11 | enabled?: boolean;
12 | }
13 |
14 | interface State {
15 | editing: boolean;
16 | }
17 |
18 | export default class EditableText extends Component {
19 | constructor(props) {
20 | super(props);
21 |
22 | this.state = {
23 | editing: false,
24 | };
25 | }
26 |
27 | get enabled() {
28 | if (this.props.enabled != undefined) {
29 | return this.props.enabled;
30 | }
31 |
32 | return true;
33 | }
34 |
35 | edit = () => {
36 | this.setState({ editing: true });
37 | };
38 |
39 | focus = (ele: HTMLElement) => {
40 | if (ele != undefined && ele.focus != undefined) {
41 | ele.focus();
42 | }
43 | };
44 |
45 | save = (e: React.KeyboardEvent) => {
46 | const target = e.target as HTMLInputElement;
47 | // use built-in validity checker for special widgets
48 | if (!target.checkValidity()) {
49 | return;
50 | }
51 |
52 | const val = target.value;
53 |
54 | // don't close/save for empty values
55 | if (val == undefined || val.trim().length === 0) {
56 | return;
57 | }
58 |
59 | this.setState({ editing: false });
60 |
61 | // ignore unchanged fields
62 | if (val === this.props.value) {
63 | return;
64 | }
65 |
66 | this.props.onChange(val);
67 | };
68 |
69 | render() {
70 | if (!this.state.editing) {
71 | if (this.enabled) {
72 | return (
73 |
74 | {this.props.children}
75 |
76 | );
77 | } else {
78 | return {this.props.children} ;
79 | }
80 | }
81 |
82 | let editor = this.props.editor || ;
83 | editor = React.cloneElement(editor, {
84 | defaultValue: this.props.value,
85 | className: 'form-control',
86 | onBlur: this.save,
87 | onKeyPress: (e) => (e.key === 'Enter' ? this.save(e) : undefined),
88 | required: true,
89 | style: {
90 | width: '100%',
91 | },
92 | ref: this.focus,
93 | });
94 | return {editor} ;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/browser/components/util/FreeformSelect.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 | import Select from 'react-select';
7 |
8 | interface Props {
9 | name: string;
10 | value?: any;
11 | options: any[];
12 | placeholder?: string;
13 | onChange: any;
14 | optionRenderer: any;
15 | }
16 |
17 | export default class FreeformSelect extends Component {
18 | onChange = (selected) => {
19 | this.props.onChange(selected);
20 | };
21 |
22 | private filterOptions(options, filter) {
23 | if (!filter || filter.length === 0) {
24 | return options;
25 | }
26 |
27 | const search = filter.toLowerCase();
28 | options = options.filter((o) => o.label.toLowerCase().includes(search));
29 |
30 | return [{ value: filter, label: filter, create: true }, ...options];
31 | }
32 |
33 | private optionRenderer = (option) => {
34 | const { optionRenderer } = this.props;
35 |
36 | if (option && option.create) {
37 | return (
38 |
39 |
40 | Add item {option.value}
41 |
42 |
43 | );
44 | }
45 |
46 | if (optionRenderer != undefined) {
47 | return optionRenderer(option);
48 | }
49 |
50 | return {option.label} ;
51 | };
52 |
53 | render() {
54 | return (
55 |
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/browser/components/util/Modal.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import * as ReactDOM from 'react-dom';
6 |
7 | type EventCreator = (
8 | actionName: string
9 | ) => (event: React.MouseEvent) => void;
10 |
11 | interface Props {
12 | title: string;
13 | dialogClass?: string;
14 | onDismiss: (actionName: string) => any;
15 | children: (buttonAction: EventCreator) => JSX.Element;
16 | }
17 |
18 | interface State {
19 | invokedAction: string;
20 | }
21 |
22 | export default class Modal extends React.Component {
23 | private self?: HTMLElement;
24 | private anchor = document.getElementById('modal-container');
25 |
26 | componentDidMount() {
27 | // for some reason (bug in BS4.b2?) clicking the backdrop instead of close
28 | // causes the modal to hang and not re-open. test this before removing
29 | // 'static' below.
30 | $(this.self as Record).modal({ backdrop: 'static' });
31 | }
32 |
33 | hideModal = () => {
34 | $(this.self as Record)
35 | .modal('hide')
36 | .on('hidden.bs.modal' as any, this.onDismiss);
37 | };
38 |
39 | onDismiss = () => {
40 | this.props.onDismiss(this.state.invokedAction);
41 | };
42 |
43 | buttonActionCreator = (actionName: string) => {
44 | return (event) => {
45 | this.setState({
46 | invokedAction: actionName,
47 | });
48 | this.hideModal();
49 | };
50 | };
51 |
52 | render() {
53 | const { title, children, dialogClass } = this.props;
54 |
55 | return ReactDOM.createPortal(
56 | {
61 | if (r) {
62 | this.self = r;
63 | }
64 | }}
65 | >
66 |
67 |
68 |
69 |
{title}
70 |
71 | {children(this.buttonActionCreator)}
72 |
73 |
74 |
,
75 | this.anchor!
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/browser/components/util/ToggleLink.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 | import { Component } from 'react';
6 |
7 | interface Props {
8 | state: boolean;
9 | onClick: any;
10 | }
11 |
12 | export default class ToggleLink extends Component {
13 | render() {
14 | const { state, onClick, children } = this.props;
15 |
16 | const icon = state ? 'fa fa-check-circle' : 'fa fa-times-circle-o';
17 |
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/browser/ext.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | type ReactSFC = (...args: any[]) => any;
5 | interface ExtensionRegistry {
6 | [k: string]: ReactSFC[];
7 | }
8 |
9 | const registry: ExtensionRegistry = {};
10 |
11 | export function register(extensionPoint: string, ext: ReactSFC) {
12 | const exts = registry[extensionPoint] || [];
13 | registry[extensionPoint] = exts.concat(ext);
14 | }
15 |
16 | export function getExtensions(extensionPoint: string): ReactSFC[] {
17 | return registry[extensionPoint] || [];
18 | }
19 |
20 | export function mapExtensions(extensionPoint: string, ...args: any[]): any[] {
21 | const exts = registry[extensionPoint] || [];
22 | return exts.map((e) => e(...args));
23 | }
24 |
--------------------------------------------------------------------------------
/browser/extensions/DemoFooter.ext.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 |
6 | import { register } from '../ext';
7 |
8 | register('footer', (props) => {
9 | return (
10 |
22 | );
23 | });
24 |
--------------------------------------------------------------------------------
/browser/extensions/README.md:
--------------------------------------------------------------------------------
1 | # Display Extensions
2 |
3 | Display extensions allow site administrators to change how the attribution builder looks and functions on the client side. You can think of them like plugins for a template engine. They can alter things like the site name/logo, footer, and other pieces as functionality is added.
4 |
5 | ## Format
6 |
7 | An extension must:
8 |
9 | * Be located in the `extensions` directory (this one).
10 | * Have an extension of `.ext.ts`, `.ext.tsx`, `.ext.js`, or `.ext.jsx`.
11 | * Import/require the `register` function from `../ext`.
12 | * Call `register` with two parameters:
13 | * `extensionPoint` _(string)_ - The name of the extension point (hook) where your extension will run.
14 | * `ext` _(function)_ - The function that will be called when this extension is triggered. Parameters and return values may vary for each extension point; see below.
15 |
16 | ### Example
17 |
18 | See `DemoFooter.ext.tsx` in this directory. This extension adds a link to this project's GitHub page in the footer.
19 |
20 | ## Extension Points
21 |
22 | The following are the current extension points shipped with oss-attribution-builder. If you think there should be another, feel free to submit an issue or pull request.
23 |
24 | ### `footer`
25 |
26 | Add a page footer with links to your site/company's support resources.
27 |
28 | A [React functional component].
29 |
30 | * Input:
31 | * `props` - React props. Empty.
32 | * Return: a rendered React component.
33 |
34 | ### `landing-after`
35 |
36 | Add additional information to the landing/home page, after the "jumbotron" description and buttons.
37 |
38 | A [React functional component].
39 |
40 | * Input:
41 | * `props` - React props. Empty.
42 | * Return: a rendered React component.
43 |
44 | ### `landing-description`
45 |
46 | Add to or replace the description on the landing page, above the project buttons.
47 |
48 | A [React functional component].
49 |
50 | * Input:
51 | * `props` - React props:
52 | * `children` - default description
53 | * Return: a rendered React component.
54 |
55 | ### `navbar-logo`
56 |
57 | Replace the "Attribution Builder" text with your own text or logo.
58 |
59 | A [React functional component].
60 |
61 | * Input:
62 | * `props` - React props:
63 | * `children` - the default header text
64 | * Return: a rendered React component.
65 |
66 | ### `navbar-end`
67 |
68 | Add additional items to the end of the navbar.
69 |
70 | A [React functional component].
71 |
72 | * Input:
73 | * `props` - React props. Empty.
74 | * Return: a rendered React component.
75 |
76 | ### `package-editor-end`
77 |
78 | Add additional instructions or logic after the package editor form (but before the save/add button).
79 |
80 | A [React functional component].
81 |
82 | * Input:
83 | * `props` - React props:
84 | * `project` - An object describing the current project. See [WebProject].
85 | * `pkg` - An object describing the package information. See [WebPackage].
86 | * `usage` - An object describing usage information. See [PackageUsage].
87 | * `license` - An object describing the selected license. See [WebLicense].
88 | * `questions` - A list of questions displayed on the form. Selections will be present in `usage`, not here. See [TagQuestion].
89 | * Return: a rendered React component.
90 |
91 | ### `page-end`
92 |
93 | Add content to the end of the page.
94 |
95 | A [React functional component].
96 |
97 | * Input:
98 | * `props` - React props. Empty.
99 | * Return: a rendered React component.
100 |
101 | ### `page-not-found`
102 |
103 | Customize the 404 page (for client-side routes).
104 |
105 | A [React functional component].
106 |
107 | * Input:
108 | * `props` - React props:
109 | * `children` - Default 404 error content, inside a Bootstrap card body.
110 | * `match` - A react-router match object; see its `path` property for the current location.
111 | * Return: a rendered React component.
112 |
113 | ### `page-start`
114 |
115 | Add content to the beginning of the page.
116 |
117 | A [React functional component].
118 |
119 | * Input:
120 | * `props` - React props. Empty.
121 | * Return: a rendered React component.
122 |
123 | ### `project-acl-editor-top`
124 |
125 | Add content above the ACL editor table. May be useful for help text with respect to the format that your organization uses for groups.
126 |
127 | A [React functional component].
128 |
129 | * Input:
130 | * `props` - React props. Empty.
131 | * Return: a rendered React component.
132 |
133 | ### `project-acl-editor-implicit-description`
134 |
135 | Change the text above the implicit permissions table, if enabled.
136 |
137 | A [React functional component].
138 |
139 | * Input:
140 | * `props` - React props. Empty.
141 | * Return: a rendered React component.
142 |
143 | [React functional component]: https://reactjs.org/docs/components-and-props.html#functional-and-class-components
144 | [WebProject]: ../../server/api/projects/interfaces.ts
145 | [WebPackage]: ../../server/api/packages/interfaces.ts
146 | [PackageUsage]: ../../server/api/projects/interfaces.ts
147 | [WebLicense]: ../../server/api/licenses/interfaces.ts
148 | [TagQuestion]: ../../server/licenses/interfaces.ts
--------------------------------------------------------------------------------
/browser/history.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | const createBrowserHistory = require("history").createBrowserHistory;
5 |
6 | // manually export our own history to make navigation outside react (i.e., in
7 | // redux actions) easier.
8 |
9 | export default createBrowserHistory();
10 |
--------------------------------------------------------------------------------
/browser/modules/common.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { fetchAuth } from '../util/index';
5 |
6 | export const SET_GENERAL_ERROR = 'app/common/set-general-error';
7 | export const RECEIVE_SITE_INFO = 'app/common/receive-site-info';
8 | export const SET_ADMIN_MODE = 'app/common/set-admin-mode';
9 |
10 | export const ADMIN_SESSION_KEY = 'admin-enabled';
11 |
12 | const initial = {
13 | generalError: undefined as { message: string } | undefined,
14 | info: {} as any,
15 | admin: sessionStorage.getItem(ADMIN_SESSION_KEY) === '1',
16 | };
17 |
18 | export default function reducer(state = initial, action: any = {}) {
19 | switch (action.type) {
20 | case SET_GENERAL_ERROR:
21 | return {
22 | ...state,
23 | generalError: action.message,
24 | };
25 |
26 | case RECEIVE_SITE_INFO:
27 | return {
28 | ...state,
29 | info: action.info,
30 | };
31 |
32 | case SET_ADMIN_MODE:
33 | action.enabled
34 | ? sessionStorage.setItem(ADMIN_SESSION_KEY, '1')
35 | : sessionStorage.removeItem(ADMIN_SESSION_KEY);
36 | return {
37 | ...state,
38 | admin: action.enabled,
39 | };
40 |
41 | default:
42 | return state;
43 | }
44 | }
45 |
46 | export function setGeneralError(error?: { message: string } | string) {
47 | if (typeof error === 'string') {
48 | error = { message: error };
49 | }
50 |
51 | return {
52 | type: SET_GENERAL_ERROR,
53 | message: error,
54 | };
55 | }
56 |
57 | export function receiveSiteInfo(info: any) {
58 | return {
59 | type: RECEIVE_SITE_INFO,
60 | info,
61 | };
62 | }
63 |
64 | export function fetchSiteInfo(query?: any) {
65 | return async (dispatch: any) => {
66 | const info = await fetchAuth('/api/v1/info');
67 | const data = await info.json();
68 | dispatch(receiveSiteInfo(data));
69 | };
70 | }
71 |
72 | export function setAdminMode(enabled: boolean) {
73 | return {
74 | type: SET_ADMIN_MODE,
75 | enabled,
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/browser/modules/licenses.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { WebLicense, WebTag } from '../../server/api/v1/licenses/interfaces';
5 | import { fetchAuth } from '../util';
6 |
7 | export const RECEIVE_LICENSES = 'app/licenses/receive-licenses';
8 |
9 | interface State {
10 | list: WebLicense[];
11 | map: Map;
12 | tags: { [key: string]: WebTag };
13 | }
14 |
15 | const initial: State = {
16 | list: [],
17 | map: new Map(),
18 | tags: {},
19 | };
20 |
21 | export default function reducer(state = initial, action: any = {}): State {
22 | switch (action.type) {
23 | case RECEIVE_LICENSES:
24 | return {
25 | ...state,
26 | list: action.licenses,
27 | map: action.licenses.reduce((map, curr) => {
28 | map.set(curr.name, curr);
29 | return map;
30 | }, state.map),
31 | tags: action.tags,
32 | };
33 |
34 | default:
35 | return state;
36 | }
37 | }
38 |
39 | /*** Action creators ***/
40 |
41 | export function receiveLicenses(data) {
42 | return {
43 | type: RECEIVE_LICENSES,
44 | licenses: data.licenses,
45 | tags: data.tags,
46 | };
47 | }
48 |
49 | /*** Bound action creators ***/
50 |
51 | export function fetchLicenses() {
52 | return (dispatch) => {
53 | return fetchAuth('/api/v1/licenses/')
54 | .then((response) => response.json())
55 | .then((json) => dispatch(receiveLicenses(json)));
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/browser/modules/packages.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import 'whatwg-fetch';
5 |
6 | import { WebPackage } from '../../server/api/v1/packages/interfaces';
7 | import { fetchAuth, reqJSON } from '../util';
8 |
9 | export const RECEIVE_PACKAGE = 'app/packages/receive-package';
10 | export const RECEIVE_PACKAGE_SEARCH_RESULTS =
11 | 'app/packages/reveive-package-search-results';
12 | export const RECEIVE_PACKAGE_VERIFICATION_QUEUE =
13 | 'app/packages/receive-package-verification-queue';
14 |
15 | export interface PackageSet {
16 | [key: number]: WebPackage;
17 | }
18 |
19 | interface State {
20 | set: PackageSet;
21 | completions: WebPackage[];
22 | verificationQueue: Array>;
23 | }
24 |
25 | const initial: State = {
26 | set: {},
27 | completions: [],
28 | verificationQueue: [],
29 | };
30 |
31 | export default function reducer(state = initial, action: any = {}): State {
32 | switch (action.type) {
33 | case RECEIVE_PACKAGE_SEARCH_RESULTS: {
34 | const newPackages = {};
35 | action.results.forEach((pkg) => {
36 | newPackages[pkg.packageId] = pkg;
37 | });
38 | return {
39 | ...state,
40 | set: {
41 | ...state.set,
42 | ...newPackages,
43 | },
44 | completions: action.results,
45 | };
46 | }
47 |
48 | case RECEIVE_PACKAGE:
49 | return {
50 | ...state,
51 | set: {
52 | ...state.set,
53 | [action.package.packageId]: action.package,
54 | },
55 | };
56 |
57 | case RECEIVE_PACKAGE_VERIFICATION_QUEUE:
58 | return {
59 | ...state,
60 | verificationQueue: action.queue,
61 | };
62 |
63 | default:
64 | return state;
65 | }
66 | }
67 |
68 | /*** Action creators ***/
69 |
70 | export function receivePackageSearchResults(results) {
71 | return {
72 | type: RECEIVE_PACKAGE_SEARCH_RESULTS,
73 | results: results.results,
74 | };
75 | }
76 |
77 | export function receivePackage(pkg) {
78 | return {
79 | type: RECEIVE_PACKAGE,
80 | package: pkg,
81 | };
82 | }
83 |
84 | export function receiveVerificationQueue(results) {
85 | return {
86 | type: RECEIVE_PACKAGE_VERIFICATION_QUEUE,
87 | queue: results.queue,
88 | };
89 | }
90 |
91 | /*** Bound action creators ***/
92 |
93 | /**
94 | * Search packages.
95 | */
96 | export function searchPackages(query) {
97 | return (dispatch) => {
98 | return reqJSON('/api/v1/packages/', { query }).then((json) =>
99 | dispatch(receivePackageSearchResults(json))
100 | );
101 | };
102 | }
103 |
104 | /**
105 | * Fetch a single package. Updates state & dispatches when complete.
106 | */
107 | export function fetchPackage(packageId: number, extended = false) {
108 | const q = extended ? '?extended=1' : '';
109 | return (dispatch) => {
110 | return fetchAuth(`/api/v1/packages/${packageId}${q}`)
111 | .then((response) => response.json())
112 | .then((json) => dispatch(receivePackage(json)));
113 | };
114 | }
115 |
116 | /**
117 | * Admin action: mark a package as "verified" (or incorrect), with comments.
118 | */
119 | export function verifyPackage(
120 | packageId: number,
121 | verified: boolean,
122 | comments: string
123 | ) {
124 | return (dispatch) => {
125 | return reqJSON(`/api/v1/packages/${packageId}/verify`, {
126 | verified,
127 | comments,
128 | }).then((json) => dispatch(fetchPackage(packageId)));
129 | };
130 | }
131 |
132 | /**
133 | * Admin action: get the queue of packages that need verification love.
134 | */
135 | export function fetchVerificationQueue() {
136 | return (dispatch) => {
137 | return reqJSON(
138 | '/api/v1/packages/verification',
139 | undefined,
140 | 'GET'
141 | ).then((json) => dispatch(receiveVerificationQueue(json)));
142 | };
143 | }
144 |
--------------------------------------------------------------------------------
/browser/reducers.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { combineReducers } from 'redux';
5 |
6 | import commonReducer from './modules/common';
7 | import licenseReducer from './modules/licenses';
8 | import packageReducer from './modules/packages';
9 | import projectReducer from './modules/projects';
10 |
11 | export default combineReducers({
12 | common: commonReducer,
13 | licenses: licenseReducer,
14 | packages: packageReducer,
15 | projects: projectReducer,
16 | });
17 |
--------------------------------------------------------------------------------
/browser/store.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { applyMiddleware, createStore } from 'redux';
5 |
6 | import { setGeneralError } from './modules/common';
7 | import app from './reducers';
8 |
9 | // capture and show errors on the UI for server actions
10 | const errorHandler = (inst) => (next) => (action) => {
11 | const result = next(action);
12 |
13 | // is this a thunk/promise action?
14 | if (typeof action === 'function') {
15 | // check the promise for an error, and dispatch that
16 | result.catch((error) => {
17 | inst.dispatch(setGeneralError(error));
18 | throw error;
19 | });
20 | }
21 | return result;
22 | };
23 |
24 | // enable thunk/promise actions
25 | const thunk = (inst) => (next) => (action) => {
26 | if (typeof action === 'function') {
27 | return action(inst.dispatch, inst.getState);
28 | }
29 | return next(action);
30 | };
31 |
32 | const createStoreWithMiddleware = applyMiddleware(
33 | errorHandler,
34 | thunk
35 | )(createStore);
36 |
37 | const store = createStoreWithMiddleware(app);
38 |
39 | export default store;
40 |
--------------------------------------------------------------------------------
/browser/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "jsx": "react"
6 | },
7 | "include": [
8 | "**/*.ts",
9 | "**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/browser/util/ExtensionPoint.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as React from 'react';
5 |
6 | import { getExtensions } from '../ext';
7 |
8 | interface Props {
9 | ext: string;
10 | children?: any;
11 | [p: string]: any;
12 | }
13 |
14 | interface State {
15 | crashed?: boolean;
16 | }
17 |
18 | export default class ExtensionPoint extends React.Component {
19 | constructor(props) {
20 | super(props);
21 | this.state = {
22 | crashed: false,
23 | };
24 | }
25 |
26 | componentDidCatch(error, info) {
27 | // tslint:disable:no-console
28 | console.error(`Extension at point ${this.props.ext} crashed.`);
29 | console.error('Component stack:', info.componentStack);
30 | this.setState({ crashed: true });
31 | }
32 |
33 | render() {
34 | if (this.state.crashed) {
35 | return (
36 |
37 | Bug: An extension that was supposed to render here
38 | crashed. Details may be available in the browser console.
39 |
40 | );
41 | }
42 |
43 | const exts = getExtensions(this.props.ext);
44 | if (exts.length === 0) {
45 | // tslint:disable-next-line:no-null-keyword
46 | return this.props.children || null;
47 | }
48 |
49 | return exts.map((Ext, i) => (
50 |
51 | ));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/browser/util/debounce.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | export default function debounce(fn: (...args: any[]) => void, time: number) {
5 | let timeout;
6 | return (...args) => {
7 | clearTimeout(timeout);
8 | timeout = setTimeout(() => {
9 | timeout = undefined;
10 | fn(...args);
11 | }, time);
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/browser/util/download.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /**
5 | * Force a download of a string as a text document.
6 | */
7 | export function downloadText(filename: string, text: string) {
8 | // make a fake link
9 | const encoded = encodeURIComponent(text);
10 | const ele = document.createElement('a');
11 | ele.setAttribute('href', `data:text/plain;charset=utf-8,${encoded}`);
12 | ele.setAttribute('download', filename);
13 | ele.style.display = 'none';
14 |
15 | // add it to the page and click it, then remove it
16 | document.body.appendChild(ele);
17 | ele.click();
18 | document.body.removeChild(ele);
19 | }
20 |
--------------------------------------------------------------------------------
/browser/util/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import 'whatwg-fetch';
5 |
6 | import { ADMIN_SESSION_KEY } from '../modules/common';
7 |
8 | /**
9 | * Convenience function for sending/receiving JSON for API calls.
10 | */
11 | export async function reqJSON(url: string, obj?: any, method: string = 'POST') {
12 | const body = obj != undefined ? JSON.stringify(obj) : undefined;
13 | const response = await fetchAuth(url, {
14 | method,
15 | headers: {
16 | Accept: 'application/json',
17 | 'Content-Type': 'application/json',
18 | },
19 | body,
20 | });
21 | return await response.json();
22 | }
23 |
24 | /**
25 | * A wrapper around fetch() to inject an authorization token.
26 | *
27 | * Will throw on non 2xx responses.
28 | */
29 | export async function fetchAuth(url: string, options?: any) {
30 | options = options || {};
31 | const headers = options.headers || {};
32 |
33 | // add admin header if enabled in store
34 | if (sessionStorage.getItem(ADMIN_SESSION_KEY) === '1') {
35 | headers['X-Admin'] = '1';
36 | }
37 |
38 | const response = await fetch(url, {
39 | ...options,
40 | headers,
41 | credentials: 'same-origin',
42 | });
43 |
44 | if (response.ok) {
45 | return response;
46 | } else {
47 | let error;
48 |
49 | // try to parse as json
50 | try {
51 | const json = await response.json();
52 | error = new Error(json.error);
53 | } catch (ex) {
54 | // otherwise just use the HTTP status code
55 | error = new Error(response.statusText);
56 | }
57 |
58 | error.code = response.status;
59 | error.response = response;
60 | throw error;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/browser/util/viewport.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import debounce from './debounce';
5 |
6 | export function inViewport(el: HTMLElement) {
7 | const r = el.getBoundingClientRect();
8 | return (
9 | r.bottom > 0 &&
10 | r.top < window.innerHeight &&
11 | r.right > 0 &&
12 | r.left < window.innerWidth
13 | );
14 | }
15 |
16 | /**
17 | * Fire a function when the given element is visible on the page.
18 | *
19 | * Detatches the event listener afterwards.
20 | */
21 | export function triggerOnVisible(el: HTMLElement, fn: () => void) {
22 | if (inViewport(el)) {
23 | fn();
24 | return;
25 | }
26 |
27 | const listener = debounce(() => {
28 | if (inViewport(el)) {
29 | window.removeEventListener('scroll', listener);
30 | fn();
31 | }
32 | }, 200);
33 | window.addEventListener('scroll', listener);
34 | }
35 |
--------------------------------------------------------------------------------
/config/default.js:
--------------------------------------------------------------------------------
1 | // static configuration
2 | const config = {};
3 |
4 | config.server = {
5 | hostname: '0.0.0.0',
6 | port: 8000,
7 | maxRequestSize: '100kb',
8 |
9 | /**
10 | * Set to a header value to use a content security policy, or set to
11 | * `false` to disable entirely.
12 | */
13 | contentSecurityPolicy: "script-src 'self'",
14 |
15 | /**
16 | * Enable/disable CORS support.
17 | *
18 | * false - no CORS support
19 | * true - equivalent to '*' -- use only in development
20 | * a string - set a static allowed origin
21 | */
22 | cors: false,
23 | };
24 |
25 | config.database = {
26 | host: 'database',
27 | port: 5432,
28 | database: 'postgres',
29 | user: 'postgres',
30 | password: () => 'oss-attribution-builder-postgres',
31 | ssl: undefined,
32 | };
33 |
34 | config.modules = {
35 | auth: 'nullauth',
36 | };
37 |
38 | config.admin = {
39 | groups: new Set(['self:admin']),
40 | verifiers: new Set(['self:verifier']),
41 | };
42 |
43 | config.globalACL = {
44 | 'self:viewer': 'viewer',
45 | };
46 |
47 | // load once asked for
48 | function load() {
49 | return Promise.resolve(config);
50 | }
51 |
52 | module.exports = {
53 | default: config,
54 | config: config,
55 | load: load,
56 | };
57 |
--------------------------------------------------------------------------------
/config/dev.js:
--------------------------------------------------------------------------------
1 | const base = require('./default');
2 |
3 | const config = base.config;
4 |
5 | config.server = {
6 | hostname: '0.0.0.0',
7 | port: 2424,
8 | contentSecurityPolicy: false,
9 | cors: true,
10 | };
11 |
12 | config.database = {
13 | host: 'localhost',
14 | port: 5432,
15 | database: 'postgres',
16 | user: 'postgres',
17 | password: () => 'oss-attribution-builder-postgres',
18 | ssl: null,
19 | };
20 |
21 | module.exports = {
22 | default: config,
23 | config: config,
24 | load: base.load,
25 | };
26 |
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | services:
4 | database:
5 | image: postgres
6 | ports:
7 | - 5432:5432
8 | volumes:
9 | - ./docs/schema.sql:/docker-entrypoint-initdb.d/init.sql
10 | environment:
11 | # don't do this in production -- this works for test/dev
12 | POSTGRES_PASSWORD: oss-attribution-builder-postgres
13 |
--------------------------------------------------------------------------------
/docker-compose.selenium.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | services:
4 | web:
5 | build: .
6 | ports:
7 | - 8000:8000
8 | links:
9 | - database
10 | environment:
11 | - USER
12 | database:
13 | image: postgres
14 | environment:
15 | # don't do this in production -- this works for test/dev
16 | POSTGRES_PASSWORD: oss-attribution-builder-postgres
17 | volumes:
18 | - ./docs/schema.sql:/docker-entrypoint-initdb.d/init.sql
19 | selenium:
20 | # use standalone-chrome-debug if you want to VNC in and watch
21 | image: selenium/standalone-chrome
22 | ports:
23 | - 4444:4444
24 | - 5900:5900
25 | links:
26 | - web
27 | environment:
28 | SCREEN_WIDTH: 1500
29 | SCREEN_HEIGHT: 2000
30 | shm_size: 2G
31 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | services:
4 | web:
5 | build: .
6 | ports:
7 | - 8000:8000
8 | links:
9 | - database
10 | environment:
11 | - USER
12 | database:
13 | image: postgres
14 | volumes:
15 | - ./docs/schema.sql:/docker-entrypoint-initdb.d/init.sql
16 | environment:
17 | # don't do this in production -- this works for test/dev
18 | POSTGRES_PASSWORD: oss-attribution-builder-postgres
19 |
--------------------------------------------------------------------------------
/docs/for-admins.md:
--------------------------------------------------------------------------------
1 | # For Staff and Administrators
2 |
3 | ## A Note about Packages
4 |
5 | Packages in the attribution builder are shared site-wide. This means that team A can enter in the details for (e.g.) WebKit 1.0, and team B can later on add WebKit to their document without needing to re-enter that information.
6 |
7 | However, team B could also opt to change the information about WebKit 1.0, perhaps because they noticed it was incorrect. Their changes will update the site-wide copy of WebKit 1.0, but they _will not_ affect projects that previously added the package. Each package & version is stored with a revision ID, so teams effectively cannot tamper with projects they do not own.
8 |
9 | ## Administration
10 |
11 | At the moment, there aren't any explicit administration actions. However, if you are an admin (i.e., your group was specified in the config file), then you will see an "Admin" link at the bottom right of every page. Clicking it will make a small checkbox appear, and at that point you will be able to browse through and edit projects while skipping access control lists.
12 |
13 | ## Verifying Packages
14 |
15 | You may designate an additional group in the configuration as "verifiers". Verifiers have access to a hidden interface located at `[domain.com]/packages/verify`. This screen will list the most popular packages entered into the system.
16 |
17 | Selecting a package will open a form with information about the package. A verifier can audit this information, and if everything looks OK they can check all of the boxes and save. If something looked wrong or needed corrections, they can be noted in the comments box and saved.
18 |
19 | 
20 |
21 | So why do this? Well, on a project page, anyone using that revision of a package (see above -- there may be multiple revisions of package version) will see a checkmark or a thumbs-down icon, indicating that the information about a package was correct or that it needs to be fixed.
22 |
23 | 
24 |
25 | Clicking on the icon will reveal who verified the information and any comments entered. Using this functionally you can effectively audit and curate the local information entered into your company's attribution documents.
--------------------------------------------------------------------------------
/docs/for-users.md:
--------------------------------------------------------------------------------
1 | # For Users
2 |
3 | The attribution builder revolves around projects. A project is just a version of software that you plan on distributing. For example, a mobile app would be a project. A project contains a list of open source packages used, along with information on how they were used and licensed. A project ultimately outputs a single attribution document
4 |
5 | From the front page of the website, you have the option of creating a new project or exploring existing projects that you have access to.
6 |
7 | ## Working with Projects
8 |
9 | A project includes information needed to generate your attribution document. You can create your project at any time and update its information whenever you need.
10 |
11 | To start, just click "New Project" from the homepage. You will be asked for some basic information about your project, such as:
12 |
13 | * What it is
14 | * Who your legal contact is
15 | * Your release date
16 | * Who manages the project
17 |
18 | Once you've filled those details out, hit Next and you'll be shown your project:
19 |
20 | 
21 |
22 | You can click anything underlined with dashes to edit the details.
23 |
24 | ### Adding Packages
25 |
26 | Now we get to the fun part: adding the list of open source packages you used. You will be asked for a lot of information here, but it's all used to speed up the process of building and reviewing your document. The website will save everything you input here in a shared database so that future projects you create will have some information already completed.
27 |
28 | To add a package, click the blue "Add Package" button. You'll see a form with a bunch fun buttons to press, but the most important thing is this box:
29 |
30 | 
31 |
32 | As it suggests, start typing in the name of an open source package used in your project. If the exact version already exists in our database, select it. If not, select the option to create a new package. You will need to fill out information about the package. If you were lucky, some of these fields will already be filled out.
33 |
34 | 
35 |
36 | When picking a license, you might not have to paste in the text of the license if we already have it stored. If you're unsure what the name of the license is, paste the text into the larger box below the dropdown instead.
37 |
38 | The copyright statement is common across most open source packages, and usually looks something like "Copyright © 2008-2012 Michael Bluth". For some packages, there may be a "NOTICE" file; you can paste that in here as well.
39 |
40 | Finally, we'll ask you if you modified this package, and if it was linked dynamically or statically. You can add notes about your usage of this package for others; these notes won't appear in your attribution document but may be useful for your team for review.
41 |
42 | If you've made a mistake and need to remove a package, just click the "X" on the top right corner of the package in your project.
43 |
44 | If you need to edit a package's details instead, remove it and re-add it -- the details of the package are saved and you won't need to re-enter everything.
45 |
46 | ## Attribution Documents
47 |
48 | This is the simplest step: once you've added your packages, click the big green Build button. You'll be taken to a screen with the text of your attribution document and any warnings/issues we have found. Clicking on a warning will highlight the relevant section in your document.
49 |
50 | When you've decided your document looks finished, scroll back up to the top and press "Save & Download". You'll get a copy of the document to distribute, and it will be stored in the attribution builder database in case it ever needs to be recovered.
--------------------------------------------------------------------------------
/docs/schema.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS pg_trgm;
2 |
3 | CREATE TABLE IF NOT EXISTS projects
4 | (
5 | project_id TEXT PRIMARY KEY NOT NULL,
6 | title TEXT NOT NULL,
7 | version TEXT NOT NULL,
8 | description TEXT,
9 | planned_release TIMESTAMP WITH TIME ZONE,
10 | created_on TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
11 | contacts JSONB,
12 | acl JSONB,
13 | packages_used JSONB NOT NULL,
14 | refs JSONB NOT NULL DEFAULT '{}',
15 | metadata JSONB
16 | );
17 | CREATE INDEX IF NOT EXISTS projects_packages_used_gin ON projects USING GIN (packages_used jsonb_path_ops);
18 | CREATE INDEX IF NOT EXISTS projects_refs_gin ON projects USING GIN (refs);
19 |
20 | CREATE TABLE IF NOT EXISTS attribution_documents
21 | (
22 | doc_id SERIAL PRIMARY KEY NOT NULL,
23 | project_id TEXT NOT NULL,
24 | project_version TEXT NOT NULL,
25 | content TEXT NOT NULL,
26 | created_on TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
27 | created_by TEXT NOT NULL,
28 | CONSTRAINT attribution_documents_projects_project_id_fk FOREIGN KEY (project_id) REFERENCES projects (project_id)
29 | );
30 |
31 | CREATE TABLE IF NOT EXISTS packages
32 | (
33 | package_id SERIAL PRIMARY KEY NOT NULL,
34 | name TEXT NOT NULL,
35 | version TEXT NOT NULL,
36 | website TEXT,
37 | license TEXT,
38 | copyright TEXT,
39 | license_text TEXT,
40 | created_by TEXT,
41 | verified BOOLEAN -- null: unverified, true: verified good, false: verified bad
42 | );
43 | CREATE INDEX IF NOT EXISTS packages_name_version_package_id_index ON packages USING BTREE (name, version, package_id);
44 | CREATE INDEX IF NOT EXISTS packages_name_version_tsv_index ON packages USING GIN (to_tsvector('english', name || ' ' || version));
45 |
46 | CREATE TABLE IF NOT EXISTS packages_verify
47 | (
48 | id SERIAL PRIMARY KEY NOT NULL,
49 | package_id INTEGER NOT NULL,
50 | verified_on TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
51 | verified_by TEXT NOT NULL,
52 | comments TEXT,
53 | CONSTRAINT packages_verify_packages_package_id_fk FOREIGN KEY (package_id) REFERENCES packages (package_id)
54 | );
55 | CREATE INDEX IF NOT EXISTS packages_verify_package_id_index ON packages_verify (package_id);
56 |
57 | CREATE TABLE IF NOT EXISTS projects_audit
58 | (
59 | id SERIAL PRIMARY KEY NOT NULL,
60 | project_id TEXT NOT NULL,
61 | who TEXT NOT NULL,
62 | changed_on TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
63 | changed_to JSONB NOT NULL,
64 | CONSTRAINT project_audit_projects_project_id_fk FOREIGN KEY (project_id) REFERENCES projects (project_id)
65 | );
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "amazon-oss-attribution-builder",
3 | "version": "0.9.0",
4 | "description": "A website to help you create attribution documents for product releases",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/amzn/oss-attribution-builder"
8 | },
9 | "author": "Jacob Peddicord ",
10 | "license": "Apache-2.0",
11 | "engines": {
12 | "node": "^12.0.0"
13 | },
14 | "scripts": {
15 | "step": "tsc && webpack",
16 | "build": "tsc && webpack && npm run static",
17 | "static": "mkdir -p build/server && cp -r config build && cp -r assets build/server && yaml2json docs/openapi.yaml > build/server/api/openapi.json",
18 | "dev": "export NODE_ENV=development && npm run build && concurrently -k \"docker-compose -f docker-compose.dev.yml up\" \"tsc -w\" \"nodemon -d 1 -w build build/server/localserver.js\" \"webpack-dev-server\"",
19 | "start": "export NODE_ENV=production && npm run build && node ./build/server/localserver.js",
20 | "test": "npm run build && jasmine 'build/server/**/*.spec.js'",
21 | "test-ui": "trap 'docker-compose -f docker-compose.selenium.yml stop' INT HUP EXIT; docker-compose -f docker-compose.selenium.yml up -d --build && tsc && jasmine 'build/spec/selenium/*.spec.js'",
22 | "lint": "prettier --write '**/*.{ts,tsx,js,css,scss}' '!build/**/*' && tslint -c tslint.json -p . --fix && tslint -c tslint.json -p browser --fix"
23 | },
24 | "files": [
25 | "build/**/*"
26 | ],
27 | "bin": {
28 | "oss-attribution-builder": "build/server/localserver.js"
29 | },
30 | "prettier": {
31 | "singleQuote": true
32 | },
33 | "devDependencies": {
34 | "@types/body-parser": "1.16.4",
35 | "@types/bootstrap": "^3.3.39",
36 | "@types/compression": "0.0.33",
37 | "@types/history": "^4.6.0",
38 | "@types/jasmine": "^2.8.7",
39 | "@types/jquery": "^3.5.5",
40 | "@types/mockery": "^1.4.29",
41 | "@types/mz": "0.0.31",
42 | "@types/node": "^14.14.14",
43 | "@types/node-fetch": "^1.6.9",
44 | "@types/passport": "^0.3.3",
45 | "@types/react": "^16.3.14",
46 | "@types/react-dom": "^16.0.5",
47 | "@types/react-redux": "^5.0.20",
48 | "@types/react-router-dom": "^4.2.6",
49 | "@types/react-select": "^1.2.6",
50 | "@types/selenium-webdriver": "^3.0.3",
51 | "@types/tapable": "^1.0.0",
52 | "@types/webpack": "^4.4.27",
53 | "bootstrap": "^4.3.1",
54 | "concurrently": "^3.5.0",
55 | "css-loader": "^5.2.0",
56 | "file-loader": "^3.0.1",
57 | "font-awesome": "^4.7.0",
58 | "history": "^4.6.3",
59 | "jasmine": "^2.99.0",
60 | "jasmine-spec-reporter": "^4.2.1",
61 | "jasmine-ts-console-reporter": "^3.1.1",
62 | "jquery": "^3.5.0",
63 | "mini-css-extract-plugin": "^1.4.0",
64 | "mockery": "^2.1.0",
65 | "node-sass": "^4.14.1",
66 | "nodemon": "^2.0.7",
67 | "pg-monitor": "^0.8.5",
68 | "popper.js": "^1.14.3",
69 | "prettier": "^2.0.5",
70 | "react": "^16.3.2",
71 | "react-dom": "^16.13.1",
72 | "react-redux": "^5.0.7",
73 | "react-router-dom": "^4.1.2",
74 | "react-select": "^1.2.1",
75 | "redux": "^3.7.2",
76 | "sass-loader": "^7.1.0",
77 | "selenium-webdriver": "^3.0.1",
78 | "ts-loader": "^5.3.3",
79 | "tslint": "^5.10.0",
80 | "tslint-config-prettier": "^1.12.0",
81 | "tslint-react": "^3.6.0",
82 | "typescript": "~4.1.3",
83 | "url-loader": "^1.1.2",
84 | "webpack": "^4.43.0",
85 | "webpack-cli": "^3.2.3",
86 | "webpack-dev-server": "^3.11.2",
87 | "yamljs": "^0.3.0"
88 | },
89 | "dependencies": {
90 | "compression": "^1.7.2",
91 | "core-js": "^2.5.6",
92 | "cors": "^2.8.5",
93 | "express": "^4.16.3",
94 | "immutable": "^3.7.6",
95 | "moment": "^2.22.1",
96 | "mz": "^2.4.0",
97 | "node-fetch": "^2.6.1",
98 | "passport": "^0.4.0",
99 | "pg-promise": "^6.3.8",
100 | "source-map-support": "^0.4.0",
101 | "spdx-license-list": "^4.1.0",
102 | "swagger-ui-express": "^4.0.2",
103 | "tiny-attribution-generator": "^0.1.1",
104 | "uuid": "^3.3.2",
105 | "whatwg-fetch": "^2.0.4",
106 | "winston": "^3.2.1"
107 | },
108 | "optionalDependencies": {
109 | "cookie-parser": "^1.4.3",
110 | "passport-cookie": "^1.0.4",
111 | "passport-http": "^0.3.0"
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/server/api/routes-v1.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as express from 'express';
5 |
6 | import { userInfo } from '../auth';
7 | import { asyncApi } from '../util/middleware';
8 | import licensesRouter from './v1/licenses';
9 | import packagesRouter from './v1/packages';
10 | import projectsRouter from './v1/projects';
11 |
12 | export let router = express.Router();
13 | export default router;
14 |
15 | // basic site/user info route
16 | router.get('/info', asyncApi(userInfo));
17 | router.use('/projects', projectsRouter);
18 | router.use('/packages', packagesRouter);
19 | router.use('/licenses', licensesRouter);
20 |
--------------------------------------------------------------------------------
/server/api/routes.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as express from 'express';
5 | import * as swaggerUi from 'swagger-ui-express';
6 | import * as winston from 'winston';
7 |
8 | import config from '../config';
9 | import v1Router from './routes-v1';
10 |
11 | export let router = express.Router();
12 | router.use(express.json({ limit: config.server.maxRequestSize }));
13 |
14 | // actual APIs are versioned
15 | router.use('/v1', v1Router);
16 |
17 | // api docs
18 | router.use(
19 | '/docs',
20 | swaggerUi.serve,
21 | // tslint:disable-next-line:no-var-requires
22 | swaggerUi.setup(require('./openapi.json'), {
23 | customCss: '.swagger-ui .topbar { display: none }',
24 | })
25 | );
26 |
27 | // unprefixed v1 routes
28 | router.use((req, res, next) => {
29 | winston.warn(`Deprecated API URL used: ${req.path} -- prefix with /v1/`);
30 | next();
31 | });
32 | router.use(v1Router);
33 |
34 | // error handling for all of the above
35 | router.use((err: any, req: any, res: any, next: any) => {
36 | if (
37 | err.name === 'UnauthorizedError' ||
38 | err.name === 'AccessError' ||
39 | err.name === 'RequestError'
40 | ) {
41 | res.status(err.status).send({ error: err.message });
42 | return;
43 | }
44 |
45 | winston.error(err.stack ? err.stack : err);
46 | res.status(500).send({ error: 'Internal error' });
47 | });
48 |
49 | // 404 handler (for API-specific routes)
50 | router.use((req, res, next) => {
51 | res.status(404).send({ error: 'Not a valid route' });
52 | });
53 |
--------------------------------------------------------------------------------
/server/api/v1/licenses/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as express from 'express';
5 |
6 | import { licenses, mapTag } from '../../../licenses';
7 | import { asyncApi } from '../../../util/middleware';
8 | import { WebLicense, WebTag } from './interfaces';
9 |
10 | export const router = express.Router();
11 | export default router;
12 |
13 | let cachedLicenses: WebLicense[] = [];
14 | const cachedTags: { [key: string]: WebTag } = {};
15 |
16 | /**
17 | * Retrieve all license and tag data.
18 | */
19 | router.get(
20 | '/',
21 | asyncApi(async (req, res) => {
22 | // this is _slightly_ expensive, so cache it
23 | if (cachedLicenses.length === 0) {
24 | cacheLicenseData();
25 | }
26 |
27 | return {
28 | licenses: cachedLicenses,
29 | tags: cachedTags,
30 | };
31 | })
32 | );
33 |
34 | function cacheLicenseData() {
35 | // a little silly, but keep two license lists; one for those that ask to be
36 | // displayed first and the other for those that don't care
37 | const first: any[] = [];
38 | const rest: any[] = [];
39 |
40 | // load each license...
41 | for (const [id, data] of (licenses as any).entries()) {
42 | const tags = data.get('tags');
43 |
44 | // fill the tag cache as we go
45 | let sortFirst = false;
46 | for (const tag of tags) {
47 | const mod = mapTag(tag);
48 | cachedTags[tag] = {
49 | presentation: mod.presentation,
50 | questions: mod.questions,
51 | };
52 |
53 | // check the sort option, since we handle that part server-side
54 | if (mod.presentation && mod.presentation.sortFirst) {
55 | sortFirst = true;
56 | }
57 | }
58 |
59 | const item = {
60 | name: id,
61 | tags,
62 | };
63 |
64 | // send it to either of the lists by presentation preference
65 | if (sortFirst) {
66 | first.push(item);
67 | } else {
68 | rest.push(item);
69 | }
70 | }
71 |
72 | cachedLicenses = first.concat(rest);
73 | }
74 |
--------------------------------------------------------------------------------
/server/api/v1/licenses/interfaces.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { TagModule } from '../../../licenses/interfaces';
5 |
6 | export interface WebLicense {
7 | name: string;
8 | tags: string[];
9 | }
10 |
11 | export type WebTag = Pick;
12 |
--------------------------------------------------------------------------------
/server/api/v1/packages/auth.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as winston from 'winston';
5 |
6 | import auth from '../../../auth';
7 | import { isAdmin, isUserInAnyGroup } from '../../../auth/util';
8 | import { config } from '../../../config';
9 | import { AccessError } from '../../../errors';
10 |
11 | export async function canValidate(req) {
12 | const user = auth.extractRequestUser(req);
13 | const groups = await auth.getGroups(user);
14 |
15 | if (isUserInAnyGroup(groups, config.admin.verifiers)) {
16 | return true;
17 | }
18 |
19 | if (isAdmin(req, groups)) {
20 | return true;
21 | }
22 |
23 | winston.warn(`User ${user} cannot validate package metadata`);
24 | return false;
25 | }
26 |
27 | export async function assertCanValidate(req) {
28 | if (!(await canValidate(req))) {
29 | throw new AccessError(
30 | 'You do not have access to validate package metadata.'
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/server/api/v1/packages/interfaces.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | export interface WebPackage {
5 | packageId: number;
6 | name: string;
7 | version: string;
8 | website?: string;
9 | license?: string;
10 | licenseText?: string;
11 | copyright?: string;
12 | createdBy?: string;
13 | verified?: boolean;
14 | extra?: {
15 | verification?: PackageVerification;
16 | stats?: PackageStats;
17 | latest?: number;
18 | };
19 | }
20 |
21 | export interface PackageVerification {
22 | verifiedOn: string;
23 | verifiedBy: string;
24 | comments: string;
25 | }
26 |
27 | export interface PackageStats {
28 | numProjects: number;
29 | }
30 |
--------------------------------------------------------------------------------
/server/api/v1/projects/auth.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as mockery from 'mockery';
5 |
6 | import { AccessLevel } from './interfaces';
7 |
8 | describe('projects auth', function () {
9 | let mock: any;
10 | let assertProjectAccess: any;
11 | let effectivePermission: any;
12 |
13 | function makeReq() {
14 | return {
15 | user: {
16 | user: 'someone',
17 | },
18 | get: () => undefined,
19 | } as any;
20 | }
21 |
22 | function makeProj() {
23 | return {
24 | project_id: 'foo',
25 | contacts: {
26 | legal: ['lawyer'],
27 | },
28 | acl: {
29 | wallet: 'owner' as AccessLevel,
30 | } as any,
31 | };
32 | }
33 |
34 | beforeEach(function () {
35 | mockery.enable({ useCleanCache: true, warnOnUnregistered: false });
36 |
37 | mock = {
38 | auth: {
39 | getGroups: jasmine
40 | .createSpy('getGroups')
41 | .and.returnValue(Promise.resolve(['a-nobody'])),
42 | extractRequestUser: (req) => req.user.user,
43 | },
44 | config: {
45 | admin: {
46 | groups: new Set(['admin-users']),
47 | },
48 | globalACL: {},
49 | },
50 | };
51 |
52 | mockery.registerMock('../../../auth', { default: mock.auth });
53 | mockery.registerMock('../../../config', { config: mock.config });
54 | mockery.registerMock('../config', { config: mock.config });
55 |
56 | mockery.registerAllowable('./auth');
57 | const auth = require('./auth');
58 | assertProjectAccess = auth.assertProjectAccess;
59 | effectivePermission = auth.effectivePermission;
60 | });
61 |
62 | afterEach(function () {
63 | mockery.deregisterAll();
64 | mockery.disable();
65 | });
66 |
67 | it('should allow owners to edit', async function (done) {
68 | const req = makeReq();
69 | const proj = makeProj();
70 | mock.auth.getGroups = jasmine
71 | .createSpy('getGroups')
72 | .and.returnValue(Promise.resolve(['wallet']));
73 | try {
74 | await assertProjectAccess(req, proj, 'owner');
75 | } catch (e) {
76 | fail(e);
77 | }
78 | done();
79 | });
80 |
81 | it('should allow legal contact to view', async function (done) {
82 | const req = makeReq();
83 | const proj = makeProj();
84 | req.user.user = 'lawyer';
85 | try {
86 | await assertProjectAccess(req, proj, 'viewer');
87 | } catch (e) {
88 | fail(e);
89 | }
90 | done();
91 | });
92 |
93 | it('should allow admins to edit', async function (done) {
94 | const req = makeReq();
95 | const proj = makeProj();
96 |
97 | // an admin account is not enough...
98 | mock.auth.getGroups = jasmine
99 | .createSpy('getGroups')
100 | .and.returnValue(Promise.resolve(['admin-users']));
101 | try {
102 | await assertProjectAccess(req, proj, 'owner');
103 | } catch (e) {
104 | expect(e.message).toMatch(/do not have access/);
105 | }
106 |
107 | // ...as you also need to set the header
108 | req.get = (h) => (h === 'X-Admin' ? '1' : undefined);
109 | try {
110 | await assertProjectAccess(req, proj, 'owner');
111 | } catch (e) {
112 | fail(e);
113 | }
114 |
115 | // ...as it checks the admin groups
116 | done();
117 | });
118 |
119 | it('should block anyone else', async function (done) {
120 | const req = makeReq();
121 | const proj = makeProj();
122 |
123 | try {
124 | await assertProjectAccess(req, proj, 'viewer');
125 | fail();
126 | } catch (err) {
127 | expect(err.message).toMatch(/do not have access/);
128 | }
129 | done();
130 | });
131 |
132 | describe('effectivePermission', function () {
133 | it('should return undefined for a single-entry ACL that does not match', async function (done) {
134 | const req = makeReq();
135 | const proj = makeProj();
136 | const level = await effectivePermission(req, proj);
137 | expect(level).toBeUndefined();
138 | done();
139 | });
140 |
141 | it('should return an access level for a single-entry ACL that does match', async function (done) {
142 | const req = makeReq();
143 | const proj = makeProj();
144 | mock.auth.getGroups = jasmine
145 | .createSpy('getGroups')
146 | .and.returnValue(Promise.resolve(['wallet']));
147 | const level = await effectivePermission(req, proj);
148 | expect(level).toEqual('owner');
149 | done();
150 | });
151 |
152 | it('should return a stronger level for multiple matching ACLs', async function (done) {
153 | const req = makeReq();
154 | const proj = makeProj();
155 | proj.acl.wallet = 'viewer';
156 | proj.acl.dog = 'editor';
157 | mock.auth.getGroups = jasmine
158 | .createSpy('getGroups')
159 | .and.returnValue(Promise.resolve(['wallet', 'dog']));
160 | const level = await effectivePermission(req, proj);
161 | expect(level).toEqual('editor');
162 | done();
163 | });
164 |
165 | it('should return a lower level when the higher does not match', async function (done) {
166 | const req = makeReq();
167 | const proj = makeProj();
168 | proj.acl.wallet = 'viewer';
169 | proj.acl.dog = 'editor';
170 | mock.auth.getGroups = jasmine
171 | .createSpy('getGroups')
172 | .and.returnValue(Promise.resolve(['wallet']));
173 | const level = await effectivePermission(req, proj);
174 | expect(level).toEqual('viewer');
175 | done();
176 | });
177 | });
178 | });
179 |
--------------------------------------------------------------------------------
/server/api/v1/projects/auth.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import auth from '../../../auth';
5 | import { isAdmin, isUserInGroup } from '../../../auth/util';
6 | import { config } from '../../../config';
7 | import * as db from '../../../db/projects';
8 | import { AccessError } from '../../../errors';
9 | import { AccessLevel, AccessLevelStrength } from './interfaces';
10 |
11 | /**
12 | * Check if the request's user is the project's contact list.
13 | */
14 | export function isInContacts(
15 | req: any,
16 | project: Pick
17 | ) {
18 | const user = auth.extractRequestUser(req);
19 | for (const type of Object.keys(project.contacts)) {
20 | const contactList = project.contacts[type];
21 | if (contactList.includes(user)) {
22 | return true;
23 | }
24 | }
25 | return false;
26 | }
27 |
28 | export type ProjectAccess = Pick;
29 |
30 | /**
31 | * Create middleware that asserts an access level on a project, and
32 | * attaches project data to res.locals.project.
33 | */
34 | export function requireProjectAccess(level: AccessLevel) {
35 | return async (req, res, next) => {
36 | try {
37 | const {
38 | params: { projectId },
39 | } = req;
40 |
41 | const project = await db.getProject(projectId);
42 | await assertProjectAccess(req, project, level);
43 | res.locals.project = project;
44 |
45 | next();
46 | } catch (err) {
47 | next(err);
48 | }
49 | };
50 | }
51 |
52 | /**
53 | * Throw an error if the request's user has no access.
54 | */
55 | export async function assertProjectAccess(
56 | req: any,
57 | project: ProjectAccess | undefined,
58 | level: AccessLevel
59 | ): Promise {
60 | if (project != undefined) {
61 | const effective = await effectivePermission(req, project);
62 | if (effective != undefined) {
63 | if (AccessLevelStrength[effective] >= AccessLevelStrength[level]) {
64 | return;
65 | }
66 | }
67 | }
68 |
69 | throw new AccessError(
70 | 'This project does not exist or you do not have access to it.'
71 | );
72 | }
73 |
74 | export async function effectivePermission(
75 | req: any,
76 | project: ProjectAccess
77 | ): Promise {
78 | const user = auth.extractRequestUser(req);
79 | const reqGroups = await auth.getGroups(user);
80 |
81 | // start by checking the admin list
82 | // (this can be replaced with the global ACL at some point)
83 | if (isAdmin(req, reqGroups)) {
84 | return 'owner';
85 | }
86 |
87 | // check the global ACL
88 | const globalLevel = getAclLevel(config.globalACL, reqGroups);
89 | const globalStrength = globalLevel ? AccessLevelStrength[globalLevel] : 0;
90 |
91 | // then check the project ACL
92 | const projectLevel = getAclLevel(project.acl, reqGroups);
93 | const projectStrength = projectLevel ? AccessLevelStrength[projectLevel] : 0;
94 |
95 | // pick the higher of the two
96 | const [effective, effectiveStrength] =
97 | projectStrength > globalStrength
98 | ? [projectLevel, projectStrength]
99 | : [globalLevel, globalStrength];
100 |
101 | // then check the contact list (defaults to view permissions)
102 | if (
103 | effectiveStrength < AccessLevelStrength.viewer &&
104 | isInContacts(req, project)
105 | ) {
106 | return 'viewer';
107 | }
108 |
109 | return effective;
110 | }
111 |
112 | /**
113 | * Given an ACL and a user's groups, return their access level.
114 | */
115 | function getAclLevel(
116 | acl: db.DbProject['acl'],
117 | groups: string[]
118 | ): AccessLevel | undefined {
119 | let effective: AccessLevel | undefined;
120 | let effectiveStrength = 0;
121 | for (const entity of Object.keys(acl)) {
122 | // skip groups that aren't relevant for the requester
123 | if (!isUserInGroup(entity, groups)) {
124 | continue;
125 | }
126 |
127 | // if we find a level with stronger access, use that
128 | const level = acl[entity];
129 | const strength = AccessLevelStrength[level];
130 | if (strength > effectiveStrength) {
131 | effective = level;
132 | effectiveStrength = strength;
133 | }
134 | }
135 |
136 | return effective;
137 | }
138 |
--------------------------------------------------------------------------------
/server/api/v1/projects/interfaces.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { AccessLevel, DbProjectRef } from '../../../db/projects';
5 |
6 | export interface WebProject {
7 | projectId: string;
8 | title: string;
9 | version: string;
10 | description: string;
11 | plannedRelease: any;
12 | createdOn: any;
13 | contacts: { [key: string]: string[] };
14 | acl: { [key: string]: AccessLevel };
15 | packagesUsed: PackageUsage[];
16 | refs: { [projectId: string]: DbProjectRef };
17 | metadata: { [key: string]: any };
18 | access: {
19 | level: AccessLevel;
20 | canEdit: boolean;
21 | };
22 | }
23 |
24 | export interface PackageUsage {
25 | packageId: number;
26 | notes?: string;
27 | // tag-added properties
28 | [key: string]: string | boolean | number | undefined;
29 | }
30 |
31 | export const AccessLevelStrength: { [key: string]: number } = {
32 | viewer: 1,
33 | editor: 2,
34 | owner: 3,
35 | };
36 |
37 | export { AccessLevel } from '../../../db/projects';
38 |
39 | export interface RefInfo {
40 | title: string;
41 | version: string;
42 | packageIds: number[];
43 | }
44 |
--------------------------------------------------------------------------------
/server/app.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import 'core-js/shim';
5 | import 'source-map-support/register';
6 |
7 | import * as path from 'path';
8 |
9 | import * as compression from 'compression';
10 | import * as cors from 'cors';
11 | import * as express from 'express';
12 | import * as passport from 'passport';
13 | import * as winston from 'winston';
14 |
15 | import { router as apiRoutes } from './api/routes';
16 | import auth from './auth';
17 | import { config, load } from './config';
18 | import { globalCustomMiddleware } from './custom';
19 | import { connect } from './db';
20 |
21 | // install a crash handler to log errors
22 | process.on('uncaughtException', (err: Record) => {
23 | winston.error('FATAL exception: ' + err);
24 | winston.error(err.stack);
25 | process.exit(99);
26 | });
27 |
28 | // let's get this runnin
29 | const app = express();
30 |
31 | // apply a security policy for general scripts.
32 | // webpack uses eval() for cheap source maps, so don't enable during development.
33 | // don't use it with selenium, either, since it needs eval() to do a bunch of things.
34 | if (config.server.contentSecurityPolicy) {
35 | app.use((req, res, next) => {
36 | res.set('Content-Security-Policy', config.server.contentSecurityPolicy);
37 | return next();
38 | });
39 | } else {
40 | winston.warn('Content-Security-Policy disabled');
41 | }
42 |
43 | // CORS
44 | if (config.server.cors) {
45 | if (config.server.cors === true) {
46 | winston.warn('Allowing CORS for any origin');
47 | app.use(cors());
48 | } else if (typeof config.server.cors === 'string') {
49 | app.use(cors({ origin: config.server.cors }));
50 | }
51 | }
52 |
53 | // auth
54 | app.use(passport.initialize());
55 | auth.initialize(app, passport);
56 |
57 | // optional custom middleware
58 | app.use(globalCustomMiddleware);
59 |
60 | // api/logic
61 | app.use('/api', apiRoutes);
62 |
63 | // static stuff
64 | app.use(compression());
65 | app.use('/assets', express.static(path.join(__dirname, 'assets')));
66 | app.use('/res', express.static(path.join(__dirname, '../res')));
67 |
68 | // catch-all for client-side routes
69 | app.use('/', (req, res) => {
70 | res.sendFile(__dirname + '/assets/template.html');
71 | });
72 |
73 | /**
74 | * Load app configuration, initialize, and listen.
75 | */
76 | export const start = async function (port, hostname) {
77 | winston.info('Starting up...');
78 |
79 | // wait for configuration to resolve
80 | try {
81 | await load();
82 | } catch (ex) {
83 | winston.error(ex);
84 | throw ex;
85 | }
86 |
87 | // connect to postgresql
88 | connect({
89 | host: config.database.host,
90 | port: config.database.port,
91 | database: config.database.database,
92 | user: config.database.user,
93 | password: config.database.password(),
94 | ssl: config.database.ssl,
95 | });
96 |
97 | winston.info('Configuration ready; launching HTTP server');
98 |
99 | // go!
100 | app.listen(port, hostname);
101 | };
102 |
103 | export default app;
104 |
--------------------------------------------------------------------------------
/server/auth/base.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { Express } from 'express';
5 | import { PassportStatic } from 'passport';
6 |
7 | /**
8 | * An interface for handling back-end user look-up.
9 | *
10 | * Implementations of this interface must be activated by specifying the
11 | * name of the module in your site configuration.
12 | *
13 | * For a sample implementation, see `nullauth`.
14 | */
15 | interface AuthBase {
16 | /**
17 | * Given an Express request object and a Passport instance, register any
18 | * needed routes & middleware to authenticate.
19 | *
20 | * Typically this means calling passport.use and app.use with some auth
21 | * strategy:
22 | *
23 | * passport.use(new MyStrategy((cookie, headers) => {
24 | * // some verification here
25 | * }));
26 | * app.use(passport.authenticate('my-strategy', {session: false}));
27 | *
28 | * Your strategy should return a user object that looks like AuthUser.
29 | * This usually means calling `done(null, {user: username})` in your
30 | * strategy function.
31 | * You may assume passport.initialize has already been called.
32 | *
33 | * See http://passportjs.org/ for a list of strategies.
34 | */
35 | initialize(app: Express, passport: PassportStatic): void;
36 |
37 | /**
38 | * Given an Express request object, return the username of the current user.
39 | *
40 | * If sitting behind a trusted proxy, you can often pull this out of
41 | * a header. If you have more custom authentication (say, a Passport hook),
42 | * you may need to store data in the request object via middleware and
43 | * then read it here.
44 | */
45 | extractRequestUser(request: any): string;
46 |
47 | /**
48 | * Given a username, look up the display name. Return null if the user
49 | * does not exist.
50 | *
51 | * For example, you could look up a user's full name in LDAP/AD.
52 | * Consider caching this method.
53 | */
54 | getDisplayName(user: string): Promise;
55 | // 'null' above remains for backwards compat, but elsewhere this should be undefined
56 |
57 | /**
58 | * Given a username, look up the list of groups the user is a member of.
59 | * Groups should, ideally, be prefixed with the group type, or at least
60 | * some token to distinguish them from user accounts. Project ACLs specify
61 | * users and groups in the same namespace, so it's on the auth backend to
62 | * separate these appropriately.
63 | *
64 | * For example, you could return `['ldap:group1', 'ldap:group2', 'custom:another-group']`.
65 | * Consider caching this method.
66 | */
67 | getGroups(user: string): Promise;
68 | }
69 |
70 | export default AuthBase;
71 |
72 | export interface AuthUser {
73 | user?: string;
74 | }
75 |
--------------------------------------------------------------------------------
/server/auth/impl/nullauth.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as cookieParser from 'cookie-parser';
5 | import { Express } from 'express';
6 | import { PassportStatic } from 'passport';
7 | import * as CookieStrategy from 'passport-cookie';
8 | import { BasicStrategy } from 'passport-http';
9 |
10 | import AuthBase, { AuthUser } from '../base';
11 |
12 | export default class NullAuth implements AuthBase {
13 | /**
14 | * We use both cookies and HTTP basic auth here:
15 | *
16 | * Basic auth is used for a user-visible dummy login page, which sets a cookie.
17 | * HTTP Basic/digest auth can't be used in AJAX requests, hence the cookie strategy.
18 | *
19 | * If you're lucky, your environment might involve an authenticating reverse proxy
20 | * and you won't need to do any of this -- just check a header.
21 | */
22 | initialize(app: Express, passport: PassportStatic) {
23 | // register cookies and http basic strategies
24 | passport.use(
25 | new CookieStrategy(
26 | { cookieName: 'nullauth-dummy-user' },
27 | (token, done) => {
28 | done(undefined, { user: token } as AuthUser);
29 | }
30 | )
31 | );
32 | passport.use(
33 | new BasicStrategy((user, pass, done) => {
34 | done(undefined, user);
35 | })
36 | );
37 |
38 | // configure dummy login page
39 | app.get(
40 | '/dummy-login',
41 | passport.authenticate('basic', { session: false }),
42 | (req, res) => {
43 | res.cookie('nullauth-dummy-user', req.user);
44 | res.redirect('/');
45 | }
46 | );
47 | // selenium needs a page with no authentication to set a cookie on
48 | app.get('/dummy-no-auth', (req, res) => {
49 | res.send('OK');
50 | });
51 | // and cookie auth for the rest
52 | app.use(cookieParser());
53 | app.use(
54 | // also allow basic auth for tinkering with swagger ui
55 | passport.authenticate(['basic', 'cookie'], {
56 | session: false,
57 | failureRedirect: '/dummy-login',
58 | })
59 | );
60 | }
61 |
62 | extractRequestUser(request: any): string {
63 | return (
64 | request.get('X-REMOTE-USER') ||
65 | request.get('X-FORWARDED-USER') ||
66 | request.user.user ||
67 | process.env.USER ||
68 | 'unknown'
69 | );
70 | }
71 |
72 | async getDisplayName(user: string): Promise {
73 | // special test case; this user will never "exist"
74 | if (user === 'nobody') {
75 | return;
76 | }
77 | return user;
78 | }
79 |
80 | async getGroups(user: string): Promise {
81 | return [`self:${user}`, 'everyone'];
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/server/auth/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import config from '../config';
5 | import AuthBase from './base';
6 | import { canHaveAdmin } from './util';
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const impl = require(`./impl/${config.modules.auth}`).default;
10 | const instance = new impl() as AuthBase;
11 |
12 | export default instance;
13 |
14 | export async function userInfo(req: any, res: any): Promise {
15 | const username = instance.extractRequestUser(req);
16 |
17 | const displayName = await instance.getDisplayName(username);
18 | const groups = await instance.getGroups(username);
19 | const canAdmin = canHaveAdmin(groups);
20 |
21 | return {
22 | displayName,
23 | groups,
24 | permissions: {
25 | admin: canAdmin,
26 | },
27 | globalACL: config.globalACL,
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/server/auth/util.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import config from '../config';
5 | import { canHaveAdmin, isAdmin, isUserInGroup } from './util';
6 |
7 | describe('auth util', function () {
8 | beforeAll(function () {
9 | config.admin = {
10 | groups: new Set(['admins']),
11 | };
12 | });
13 |
14 | it('should validate groups', function () {
15 | const req = {
16 | user: {
17 | groups: ['abc', 'xyz'],
18 | },
19 | };
20 | expect(isUserInGroup('xyz', req.user.groups)).toBe(true);
21 | expect(isUserInGroup('qwerty', req.user.groups)).toBe(false);
22 | });
23 |
24 | it('should validate user groups against configured admin set', function () {
25 | expect(canHaveAdmin(['employees', 'admins'])).toBe(true);
26 | expect(canHaveAdmin(['admins'])).toBe(true);
27 | expect(canHaveAdmin(['employees'])).toBe(false);
28 | expect(canHaveAdmin([])).toBe(false);
29 | });
30 |
31 | it('should only authorize admin actions when header is set', function () {
32 | const request = {
33 | get: (h) => undefined,
34 | } as any;
35 | expect(isAdmin(request, ['admins'])).toBe(false);
36 | request.get = (h) => {
37 | return h === 'X-Admin' ? '1' : undefined;
38 | };
39 | expect(isAdmin(request, ['admins'])).toBe(true);
40 | expect(isAdmin(request, ['blah'])).toBe(false);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/server/auth/util.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { Request } from 'express';
5 |
6 | import { config } from '../config';
7 |
8 | /**
9 | * Check if a request's user is in a given group.
10 | */
11 | export function isUserInGroup(group: string, groups: string[]) {
12 | return groups.indexOf(group) !== -1;
13 | }
14 |
15 | /**
16 | * Check if a request's user is in any groups in the given set.
17 | * This is basically just set intersection.
18 | */
19 | export function isUserInAnyGroup(supplied: string[], check: Set) {
20 | for (const g of supplied) {
21 | if (check.has(g)) {
22 | return true;
23 | }
24 | }
25 | return false;
26 | }
27 |
28 | /**
29 | * Check if a user has administrative access to projects.
30 | */
31 | export function isAdmin(req: Request, groups: string[]): boolean {
32 | return req.get('X-Admin') === '1' && canHaveAdmin(groups);
33 | }
34 |
35 | /**
36 | * Check if a user _could_ be an admin, if requested during auth.
37 | */
38 | export function canHaveAdmin(groups: string[]) {
39 | return isUserInAnyGroup(groups, config.admin.groups);
40 | }
41 |
--------------------------------------------------------------------------------
/server/config.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /**
5 | * A configuration loader that sources the actual configuration from the
6 | * environment, or the default configuration if not specified.
7 | */
8 |
9 | const name = process.env.CONFIG_NAME ? process.env.CONFIG_NAME : 'default';
10 | // tslint:disable-next-line:no-var-requires
11 | const actual = require(`../config/${name}.js`);
12 |
13 | export let config = actual.config;
14 | export let load = actual.load;
15 |
16 | export default config;
17 |
--------------------------------------------------------------------------------
/server/custom.ts:
--------------------------------------------------------------------------------
1 | import { NextFunction } from 'express';
2 |
3 | export function globalCustomMiddleware(
4 | req: Express.Request,
5 | res: Express.Response,
6 | next: NextFunction
7 | ) {
8 | // override here with anything you please; this will never have "real" logic in it.
9 | // this is a suitable location to add in request timing/logging, rate limiting, etc.
10 | next();
11 | }
12 |
--------------------------------------------------------------------------------
/server/db/attribution_documents.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import pg from './index';
5 |
6 | export interface AttributionDocument {
7 | doc_id: number;
8 | project_id: string;
9 | project_version: string;
10 | created_on: Date;
11 | created_by: string;
12 | content: string;
13 | }
14 |
15 | export function getAttributionDocument(
16 | projectId: string,
17 | docId: number
18 | ): Promise {
19 | return pg().oneOrNone(
20 | 'select * from attribution_documents where project_id = $1 and doc_id = $2',
21 | [projectId, docId]
22 | );
23 | }
24 |
25 | export function findDocumentsForProject(
26 | projectId: string
27 | ): Promise<
28 | Array<
29 | Pick<
30 | AttributionDocument,
31 | 'doc_id' | 'project_version' | 'created_on' | 'created_by'
32 | >
33 | >
34 | > {
35 | return pg().query(
36 | 'select doc_id, project_version, created_on, created_by from attribution_documents where project_id = $1 order by created_on desc',
37 | [projectId]
38 | );
39 | }
40 |
41 | export async function storeAttributionDocument(
42 | projectId: string,
43 | projectVersion: string,
44 | content: string,
45 | createdBy: string
46 | ): Promise {
47 | const doc = await pg().one(
48 | 'insert into attribution_documents(project_id, project_version, content, created_by) ' +
49 | 'values ($1, $2, $3, $4) returning doc_id',
50 | [projectId, projectVersion, content, createdBy]
51 | );
52 | return doc.doc_id;
53 | }
54 |
--------------------------------------------------------------------------------
/server/db/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as pgPromise from 'pg-promise';
5 |
6 | const options = {};
7 |
8 | // note: pg-monitor isn't installed in prod; this doesn't work there intentionally.
9 | if (process.env.DEBUG_SQL) {
10 | // tslint:disable-next-line:no-var-requires
11 | const monitor = require('pg-monitor');
12 | monitor.attach(options);
13 | }
14 |
15 | const pgp = pgPromise(options);
16 | let pg: pgPromise.IDatabase;
17 |
18 | export function connect(cn: any) {
19 | pg = pgp(cn);
20 | }
21 |
22 | export default function getDB() {
23 | return pg;
24 | }
25 |
--------------------------------------------------------------------------------
/server/db/packages.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import pg from './index';
5 |
6 | export interface Package {
7 | package_id: number;
8 | name: string;
9 | version: string;
10 | website?: string;
11 | license?: string;
12 | copyright?: string;
13 | license_text?: string;
14 | created_by?: string;
15 | verified?: boolean;
16 | }
17 |
18 | export interface PackageVerify {
19 | id: number;
20 | package_id: number;
21 | verified_on: any;
22 | verified_by: string;
23 | comments?: string;
24 | }
25 |
26 | export function searchPackages(
27 | search: string,
28 | limit: number
29 | ): Promise {
30 | // build up a sanitized postgresql fulltext tsquery
31 | // remove junk, split on space, add quotes + wildcard, then join with &
32 | const tsquery = search
33 | .trim()
34 | .replace(/[^\w\s]+/u, '')
35 | .split(/\s+/u)
36 | .filter((t) => t.length > 0)
37 | .map((t) => `'${t}':*`)
38 | .join(' & ');
39 |
40 | // two queries here:
41 | // the inner query is actually running the full-text search (to take
42 | // advantage of an index), and the outer performs a distinct query to
43 | // only fetch the latest revision of each package
44 | return pg().query(
45 | `select distinct on (name, version) * from (` +
46 | `select * from packages ` +
47 | `where to_tsvector('english', name || ' ' || version) @@ to_tsquery($1)` +
48 | `) as search order by name, version, package_id desc limit $2`,
49 | [tsquery, limit]
50 | );
51 | }
52 |
53 | export function getLatestPackageRevision(
54 | name: string,
55 | version: string
56 | ): Promise {
57 | return pg().oneOrNone(
58 | 'select * from packages where name = $1 and version = $2 order by package_id desc limit 1',
59 | [name, version]
60 | );
61 | }
62 |
63 | export function getPackage(packageId: number): Promise {
64 | return pg().oneOrNone(
65 | 'select * from packages where package_id = $1',
66 | packageId
67 | );
68 | }
69 |
70 | export function getPackages(packageIdList: number[]): Promise {
71 | return pg().query('select * from packages where package_id = any($1)', [
72 | packageIdList,
73 | ]);
74 | }
75 |
76 | export async function createPackageRevision(
77 | name: string,
78 | version: string,
79 | website: string,
80 | license: string,
81 | copyright: string,
82 | licenseText: string,
83 | createdBy: string
84 | ): Promise {
85 | // normalize some fields
86 | copyright = copyright.trim();
87 | if (licenseText != undefined) {
88 | licenseText = licenseText.replace(/^[\r\n]+|[\r\n]+$/g, '');
89 | }
90 |
91 | const result = await pg().one(
92 | 'insert into packages(name, version, website, license, copyright, license_text, created_by) ' +
93 | 'values ($1, $2, $3, $4, $5, $6, $7) returning package_id',
94 | [name, version, website, license, copyright, licenseText, createdBy]
95 | );
96 | return result.package_id;
97 | }
98 |
99 | export async function getUnverifiedPackages(limit: number = 25) {
100 | // take a deep breath
101 | return pg().any(
102 | // select all packages,
103 | 'select pkg.package_id, pkg.name, pkg.version, count(pkg.package_id) as count from packages pkg ' +
104 | // joining against projects using each package by inspecting the JSONB
105 | // packages_used field (@> is a postgres JSON search operator),
106 | 'join projects pj on pj.packages_used @> ' +
107 | "json_build_array(json_build_object('package_id', pkg.package_id))::jsonb " +
108 | // excluding unverified packages,
109 | 'where verified is null ' +
110 | // and grouped by package id for the count() in select, which is then sorted.
111 | 'group by pkg.package_id order by count desc, pkg.package_id desc',
112 | [limit]
113 | );
114 | // and yes there is an index for the above query (see sql/projects.sql)
115 | }
116 |
117 | export async function verifyPackage(
118 | packageId: number,
119 | verified?: boolean
120 | ): Promise {
121 | await pg().none('update packages set verified = $2 where package_id = $1', [
122 | packageId,
123 | verified,
124 | ]);
125 | }
126 |
127 | export async function getPackageVerifications(
128 | packageId: number
129 | ): Promise {
130 | return pg().any(
131 | 'select * from packages_verify where package_id = $1 order by verified_on desc',
132 | [packageId]
133 | );
134 | }
135 |
136 | export async function addVerification(
137 | packageId,
138 | verifiedBy,
139 | comments
140 | ): Promise {
141 | const result = await pg().one(
142 | 'insert into packages_verify(package_id, verified_by, comments) values ($1, $2, $3) returning id',
143 | [packageId, verifiedBy, comments]
144 | );
145 | return result.id;
146 | }
147 |
--------------------------------------------------------------------------------
/server/db/projects.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import generateID from '../util/idgen';
5 | import pg from './index';
6 | import { addAuditItem } from './projects_audit';
7 |
8 | export interface DbProject {
9 | project_id: string;
10 | title: string;
11 | version: string;
12 | description?: string;
13 | planned_release?: Date;
14 | created_on: Date;
15 | // contact type: [list of contacts]
16 | contacts: { [type: string]: string[] };
17 | // resource (user/group) name: access level
18 | acl: { [resource: string]: AccessLevel };
19 | packages_used: DbPackageUsage[];
20 | refs: { [projectId: string]: DbProjectRef };
21 | metadata?: { [key: string]: any };
22 | }
23 |
24 | export interface DbPackageUsage {
25 | package_id: number;
26 | [key: string]: any;
27 | }
28 |
29 | export interface DbProjectRef {
30 | type: 'cloned_from' | 'related' | 'includes';
31 | [key: string]: any;
32 | }
33 |
34 | export type AccessLevel = 'owner' | 'editor' | 'viewer';
35 |
36 | export function getProject(projectId: string): Promise {
37 | return pg().oneOrNone(
38 | 'select * from projects where project_id = $1',
39 | projectId
40 | );
41 | }
42 |
43 | export function searchProjects(): Promise {
44 | return pg().query('select * from projects order by created_on desc');
45 | }
46 |
47 | export function searchOwnProjects(groups: string[]): Promise {
48 | if (groups.length === 0) {
49 | return Promise.resolve([]);
50 | }
51 | return pg().query(
52 | 'select * from projects where acl ?| $1 order by created_on desc',
53 | [groups]
54 | );
55 | }
56 |
57 | type ProjectInput = Pick<
58 | DbProject,
59 | | 'title'
60 | | 'version'
61 | | 'description'
62 | | 'planned_release'
63 | | 'contacts'
64 | | 'acl'
65 | | 'refs'
66 | | 'metadata'
67 | >;
68 | export async function createProject(
69 | project: ProjectInput,
70 | who: string
71 | ): Promise {
72 | const projectId = generateID(10);
73 |
74 | // add the entry itself
75 | await pg().none(
76 | 'insert into projects(' +
77 | 'project_id, title, version, description, planned_release, contacts, acl, refs, metadata, packages_used' +
78 | ") values($1, $2, $3, $4, $5, $6, $7, $8, $9, '[]'::jsonb)",
79 | [
80 | projectId,
81 | project.title,
82 | project.version,
83 | project.description,
84 | project.planned_release,
85 | project.contacts,
86 | project.acl,
87 | project.refs,
88 | project.metadata,
89 | ]
90 | );
91 |
92 | // add auditing info
93 | await addAuditItem(projectId, who, {
94 | ...project,
95 | project_id: projectId,
96 | });
97 | return projectId;
98 | }
99 |
100 | export async function patchProject(
101 | projectId: string,
102 | changes: Partial,
103 | who: string
104 | ): Promise {
105 | // build a set up update statements
106 | const patches: string[] = [];
107 | for (const change of Object.keys(changes)) {
108 | // make a named parameter with the same name as the field
109 | patches.push(`${change} = $(${change})`);
110 | }
111 | const patchStr = patches.join(' ');
112 |
113 | // insert them all
114 | await pg().none(
115 | `update projects set ${patchStr} where project_id = $(projectId)`,
116 | Object.assign({ projectId }, changes)
117 | );
118 |
119 | // audit the change
120 | await addAuditItem(projectId, who, changes);
121 | }
122 |
123 | export async function updatePackagesUsed(
124 | projectId: string,
125 | packagesUsed: any,
126 | who: string
127 | ): Promise {
128 | await pg().none(
129 | 'update projects set packages_used = $1 where project_id = $2',
130 | [JSON.stringify(packagesUsed), projectId]
131 | );
132 |
133 | await addAuditItem(projectId, who, {
134 | packages_used: packagesUsed,
135 | });
136 | }
137 |
138 | export async function getProjectRefs(
139 | projectIds: string[]
140 | ): Promise<
141 | Array>
142 | > {
143 | return await pg().query(
144 | 'select project_id, title, version, packages_used from projects where project_id = any($1)',
145 | [projectIds]
146 | );
147 | }
148 |
149 | export async function getProjectsRefReverse(
150 | projectId: string
151 | ): Promise>> {
152 | return await pg().query(
153 | 'select project_id, title, version from projects where refs ? $1',
154 | [projectId]
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/server/db/projects_audit.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import pg from './index';
5 |
6 | export async function getProjectAuditLog(projectId: string) {
7 | return pg().query(
8 | 'select * from projects_audit where project_id = $1 order by changed_on desc',
9 | [projectId]
10 | );
11 | }
12 |
13 | export async function addAuditItem(
14 | projectId: string,
15 | who: string,
16 | changes: any
17 | ): Promise {
18 | await pg().none(
19 | 'insert into projects_audit(project_id, who, changed_to) values($1, $2, $3)',
20 | [projectId, who, changes]
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/server/errors/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /**
5 | * Authz error for valid tokens, but no access to a resource.
6 | */
7 | export class AccessError extends Error {
8 | status: number;
9 |
10 | constructor(message: string) {
11 | super();
12 | this.name = 'AccessError';
13 | this.message = message;
14 | this.status = 403;
15 | }
16 | }
17 |
18 | /**
19 | * Invalid request.
20 | */
21 | export class RequestError extends Error {
22 | status: number;
23 |
24 | constructor(message: string) {
25 | super();
26 | this.name = 'RequestError';
27 | this.message = message;
28 | this.status = 400;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/server/licenses/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as fs from 'fs';
5 | import * as path from 'path';
6 |
7 | import * as Immutable from 'immutable';
8 | import * as winston from 'winston';
9 | import { TagModule } from './interfaces';
10 |
11 | type LicenseMap = Immutable.Map>;
12 |
13 | const tagCache = new Map();
14 |
15 | export const licenses: LicenseMap = loadLicenses();
16 |
17 | export function mapTag(name): TagModule {
18 | let mod = tagCache.get(name);
19 | if (mod == undefined) {
20 | mod = require(`./tags/${name}`);
21 | tagCache.set(name, mod);
22 | }
23 |
24 | return mod;
25 | }
26 |
27 | function loadLicenses(): LicenseMap {
28 | const licenseMap = new Map>();
29 |
30 | // start with SPDX licenses
31 | const spdxData = require('spdx-license-list/full');
32 | for (const id of Object.keys(spdxData)) {
33 | licenseMap.set(
34 | id,
35 | Immutable.fromJS({
36 | tags: ['all', 'spdx', 'unknown'],
37 | text: cleanup(spdxData[id].licenseText),
38 | })
39 | );
40 | }
41 | winston.info(`Loaded ${licenseMap.size} SPDX licenses`);
42 |
43 | // then load known/custom license data
44 | // overwriting SPDX is OK
45 | // Sync function used here; this is only called during app startup
46 | const files = fs.readdirSync(path.join(__dirname, 'known'));
47 | for (const f of files) {
48 | if (f.endsWith('.js')) {
49 | const id = path.basename(f, '.js');
50 | licenseMap.set(id, processKnownLicense(id, spdxData));
51 | }
52 | }
53 | winston.info(`Loaded ${licenseMap.size} total licenses`);
54 |
55 | return Immutable.fromJS(licenseMap);
56 | }
57 |
58 | function processKnownLicense(id: string, spdxData: any) {
59 | const info = require(`./known/${id}`);
60 | let text = info.text;
61 | const tags = info.tags.concat(['all']);
62 |
63 | // overwriting an SPDX license?
64 | if (spdxData.hasOwnProperty(id)) {
65 | winston.info(`Overwriting SPDX license ${id}`);
66 | if (info.text === true) {
67 | winston.info(`Re-using ${id} license text`);
68 | text = spdxData[id].licenseText;
69 | tags.push('spdx'); // restore spdx tag if opting in to text
70 | }
71 | }
72 |
73 | if (typeof text !== 'string') {
74 | throw new Error(
75 | `License ${id} neither supplied license text, nor referenced SPDX text`
76 | );
77 | }
78 |
79 | text = cleanup(text);
80 |
81 | return Immutable.fromJS({ tags, text });
82 | }
83 |
84 | function cleanup(text: string): string {
85 | // get that crlf outta here
86 | text = text.replace(/\r?\n/g, '\n');
87 | // trim empty lines
88 | text = text.replace(/^\s*$/gm, '');
89 | // trim trailing whitespace
90 | text = text.replace(/^\S\s+$/gm, '');
91 | // trim excess newlines from start and end
92 | text = text.replace(/^\n+|\n+$/g, '');
93 | // trim excess interior newlines
94 | text = text.replace(/\n{3,}/g, '\n\n');
95 | return text;
96 | }
97 |
--------------------------------------------------------------------------------
/server/licenses/interfaces.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | export interface TagModule {
5 | validateSelf?: (
6 | name: string | undefined,
7 | text: string,
8 | tags: string[]
9 | ) => ValidationResult[] | undefined;
10 | validateUsage?: (pkg: any, usage: any) => ValidationResult[] | undefined;
11 | transformCopyright?: (original: string) => string;
12 | transformLicense?: (original: string, packages) => string;
13 | presentation?: TagPresentation;
14 | questions?: TagQuestions;
15 | }
16 |
17 | export interface ValidationResult {
18 | level: 0 | 1 | 2;
19 | message: string;
20 | }
21 |
22 | /**
23 | * Various flags a tag can set to influence the behavior of
24 | * the web UI. Used by modules; see LicensePresentation for what
25 | * actually gets sent to the browser.
26 | */
27 | export interface TagPresentation {
28 | // should this license be shown first in the list? (alphabetically if competing with
29 | // other licenses with this flag)
30 | sortFirst?: boolean;
31 | // additional text to display in the drop-down list -- keep it really short! maybe use an emoji?
32 | shortText?: string;
33 | // text to display under the license box once selected. keep this reasonably short too!
34 | longText?: string;
35 | // if true, removes the "paste license text here" box. useful for licenses that never change,
36 | // e.g. GPL, Apache
37 | fixedText?: boolean;
38 | }
39 |
40 | /**
41 | * A question to be asked when adding a package.
42 | *
43 | * Questions are added dynamically to the UI based on tags applied to a
44 | * selected license. For information on this behavior, see the README in the
45 | * tags directory.
46 | */
47 | export interface TagQuestion {
48 | /**
49 | * A presentational label for this tag.
50 | */
51 | label: string;
52 |
53 | /**
54 | * Whether this question must be answered for the form to be submitted.
55 | */
56 | required: boolean;
57 |
58 | /**
59 | * The type of data this tag stores. Data will be coerced from form values.
60 | */
61 | type: 'string' | 'boolean' | 'number';
62 |
63 | /**
64 | * The form widget to use when displaying this question.
65 | *
66 | * 'radio' and 'select' values should be accompanied by an 'options' list;
67 | * see that field for info.
68 | */
69 | widget: 'radio' | 'text' | 'select';
70 |
71 | /**
72 | * If supplied, represents a list of valid options a user may choose from.
73 | *
74 | * Each element should be of the form `[value, label]` where 'value' is the
75 | * actual stored value and 'label' is the UI-friendly label.
76 | */
77 | options?: Array<[string | boolean | number, string]>;
78 | }
79 |
80 | export interface TagQuestions {
81 | [key: string]: TagQuestion;
82 | }
83 |
--------------------------------------------------------------------------------
/server/licenses/known.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { List } from 'immutable';
5 | import { licenses } from './index';
6 |
7 | describe('known licenses', function () {
8 | it('declare required properties', function () {
9 | for (const [name, data] of (licenses as any).entries()) {
10 | const license = name;
11 | expect(license).toBeDefined();
12 |
13 | expect(List.isList(data.get('tags'))).toBe(
14 | true,
15 | `license ${name}'s tags property was not a list`
16 | );
17 | expect(typeof data.get('text')).toEqual(
18 | 'string',
19 | `license ${name}'s text was not a string`
20 | );
21 | }
22 | });
23 |
24 | it('used tags must exist', function () {
25 | for (const data of (licenses as any).values()) {
26 | for (const tag of data.get('tags')) {
27 | require(`./tags/${tag}`);
28 | }
29 | }
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/server/licenses/known/Apache-2.0.ts:
--------------------------------------------------------------------------------
1 | export const tags = ['notice', 'fixed-text', 'popular'];
2 | export const text = true; // use SPDX version
3 |
--------------------------------------------------------------------------------
/server/licenses/known/MIT.ts:
--------------------------------------------------------------------------------
1 | export const tags = ['popular'];
2 |
3 | // text could also be assigned to true to use the SPDX text, but in this example
4 | // we're just supplying it ourselves
5 | export const text = `
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | `;
24 |
--------------------------------------------------------------------------------
/server/licenses/known/MyCustomLicense.ts:
--------------------------------------------------------------------------------
1 | export const tags = ['popular', 'linkage', 'validation-demo'];
2 |
3 | export const text = `
4 | This is a sample custom license.
5 |
6 | It's not very special, and it's also not really a license at all.
7 | `;
8 |
--------------------------------------------------------------------------------
/server/licenses/known/README.md:
--------------------------------------------------------------------------------
1 | Each license in this directory must be a JavaScript (TypeScript) module with the following exports:
2 |
3 | * `tags` - a list of tags relevant to this license
4 | * `text` - the full text of the license, used if license text was not supplied by the user
5 |
6 | For a description of tags, see the README in the tags directory next to this one.
7 |
8 | The license text will be automatically stripped of leading and trailing newlines; other kinds of whitespace will be preserved.
9 |
10 | The name of the license is inferred from the filename, and _should_ be an SPDX ID, however nothing will break if it isn't.
--------------------------------------------------------------------------------
/server/licenses/tags.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as fs from 'fs';
5 | import * as path from 'path';
6 |
7 | import { TagModule } from './interfaces';
8 |
9 | const tagsDir = path.join(__dirname, 'tags');
10 |
11 | describe('license tags', function () {
12 | const modules: TagModule[] = [];
13 |
14 | beforeAll(function (done) {
15 | fs.readdir(tagsDir, (err, files) => {
16 | if (err) {
17 | throw err;
18 | }
19 |
20 | for (const f of files) {
21 | if (f.endsWith('.js')) {
22 | modules.push(require(path.join(tagsDir, f)));
23 | }
24 | }
25 |
26 | done();
27 | });
28 | });
29 |
30 | it('may export a questions object', function () {
31 | for (const mod of modules) {
32 | if (mod.questions == undefined) {
33 | continue;
34 | }
35 |
36 | // runtime type checking, basically
37 | for (const key of Object.keys(mod.questions)) {
38 | const q = mod.questions[key];
39 | expect(q.label).toEqual(
40 | jasmine.any(String),
41 | 'label key must be a string'
42 | );
43 | expect(q.required).toEqual(
44 | jasmine.any(Boolean),
45 | 'required key must be a boolean'
46 | );
47 | expect(q.type).toMatch(/string|boolean|number/, 'type key mismatch');
48 | expect(q.widget).toMatch(/radio|text|select/, 'widget key mismatch');
49 |
50 | if (q.options) {
51 | expect(q.options).toEqual(
52 | jasmine.any(Array),
53 | 'options must be an array if present'
54 | );
55 | for (const opt of q.options) {
56 | expect(opt.length).toEqual(
57 | 2,
58 | 'options array items must be 2-tuple arrays'
59 | );
60 | expect(opt[0]).toBeDefined();
61 | expect(opt[1]).toEqual(jasmine.any(String));
62 | }
63 | }
64 | }
65 | }
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/server/licenses/tags/README.md:
--------------------------------------------------------------------------------
1 | Tags for validation purposes should be placed here.
2 |
3 | Validation levels:
4 | * 0 - Error
5 | * 1 - Warning
6 | * 2 - Note
7 |
8 | ## Exports
9 |
10 | A tag module may have the following exports (all optional):
11 |
12 | * `validateSelf(name, text, tags)`
13 |
14 | Validate the existence of a license on a given project. The license name, text, and any other tags will be provided. This function should return a list of messages in the format:
15 |
16 | ```
17 | {level: 0, message: ''}
18 | ```
19 |
20 | where `level` is a validation level described above, and `message` is any message you want displayed in the output. Messages are displayed in the "Warnings and Notes" section before the document preview.
21 |
22 | The license text may be the internal/known/SPDX version, or it may be the user-supplied text. User-supplied text gets priority; to know when this occurs you can check for the `user-supplied` tag.
23 |
24 | `tags` includes all tags applied to the current license. This may be useful in the `all` tag if you want to check for the existence or absence of any other tags.
25 |
26 | * `validateUsage(pkg, usage)`
27 |
28 | Validate the package against how it was used in this project.
29 | Usage information, in the same format as stored in the database, will be supplied.
30 |
31 | `pkg` is a `Package` object from `tiny-attribution-generator`.
32 |
33 | This function should return a list of messages in the same format as `validateSelf`.
34 |
35 | * `transformCopyright(original)`
36 |
37 | A tag may implement this to change how a copyright statement is rendered in the output document. For example, the `notice` tag clears this out in favor of showing statements in a different section of the document.
38 |
39 | * `transformLicense(original, packages)`
40 |
41 | A tag may implement this to change how a license itself is rendered in the output document. It receives the original license text and a list of packages. For example, the `notice` tag uses this to re-locate copyright statements to a "NOTICE" section following the license. The `spdx` tag uses this to word-wrap SPDX-provided licenses to 80 characters.
42 |
43 | `packages` is a list of `Package` objects from `tiny-attribution-generator`.
44 |
45 | * `presentation = {}`
46 |
47 | For website/UI purposes, a tag may suggest presentation options. These options are keys of an object named `presentation` (not a function). All are optional.
48 |
49 | Available options:
50 |
51 | * `sortFirst` (boolean, default false) - specifies whether this license should appear above others in a listing.
52 | * `shortText` (string) - can add text to license listings, for example on the license dropdown. keep it short; picking an emoji might be a good idea!
53 | * `longText` (string) - displayed below a license when selected.
54 | * `fixedText` (boolean, default false) - if set, removes the textarea for pasting a custom license when a license with this tag is selected. primarily used by the `fixed-text` tag.
55 |
56 | Example:
57 |
58 | ```
59 | export const presentation = { showFirst: true; }
60 | ```
61 |
62 | For more usage examples, see tags `fixed-text` and `popular`.
63 |
64 | * `questions = {}` _(optional)_
65 |
66 | Tags attached to a license may ask for more information from users. For example, a tag could ask a question asking if a package is modified. Tags may export any number of questions in this object.
67 |
68 | Questions are specified by adding a new key to the object with a unique name. If this name is shared by another tag, it will be overwritten. This name is also used as a key in usage metadata; e.g. if your question key is `foo` then you'll start seeing `{"foo": "bar"}` entries in your database.
69 |
70 | The schema is detailed in the [TagQuestion interface](../interfaces.ts). Have a look at that for valid options.
71 |
72 | For examples, see the `linkage` and `modified` tags. `unknown` also imports these questions into its own set.
73 |
74 | ## Meta tags
75 |
76 | The following tags are more deeply integrated into the generator and should not be deleted:
77 |
78 | * `all` - applied to all packages in a document
79 | * `unknown` - applied to packages which use a license not "known" by the generator; meaning it wasn't present in the 'known' directory. will apply to SPDX licenses unless configured otherwise.
80 | * `fixed-text` - used by the UI to hide the license text box; useful for license texts that should never be changed
81 | * `spdx` - applied to SPDX-supplied licenses (so, most that ship with the attribution builder)
82 | * `user-supplied` - applied to any licenses where the user supplied the text, unknown license or not
83 |
84 | Note the key differences in `user-supplied` and `unknown`; `unknown` refers to the license by name being unknown, where user-supplied is applied if any license is pasted in. So, both `unknown` and `user-supplied` will apply to a license where only the text was pasted in, but only `user-supplied` would apply if they selected MIT *and* pasted in a license.
85 |
--------------------------------------------------------------------------------
/server/licenses/tags/all.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { ValidationResult } from '../interfaces';
5 |
6 | /**
7 | * The 'all' tag is applied to all packages.
8 | *
9 | * Validation that checks for the presence/abscence of other tags can be set up here.
10 | */
11 |
12 | export function validateSelf(name, text, tags) {
13 | const warnings: ValidationResult[] = [];
14 | const nicename = name ? name : 'The provided license';
15 |
16 | // look for excessively long lines, but ignore SPDX-tagged licenses.
17 | // those are word wrapped after this validation happens
18 | // (keen eyes will note that this will never match SPDX-supplied texts since
19 | // the `text` parameter is user-supplied, but it _does_ match e.g. picking BSD-3-Clause
20 | // and pasting the license text in.)
21 | if (text.match(/.{100,}/) && !tags.includes('spdx')) {
22 | warnings.push({
23 | level: 1,
24 | message: `${nicename} contains long lines. Consider word-wrapping the text.`,
25 | });
26 | }
27 |
28 | // look for template markers
29 | if (text.match(/</i)) {
38 | // for some reason, BSD SPDX licenses include these, so they'll get removed in
39 | // that tag's transformLicense function. add a note that we're doing that.
40 | if (tags.includes('spdx')) {
41 | warnings.push({
42 | level: 1,
43 | message: `${nicename} had a stub copyright line (with "", or "" markers) removed.`,
44 | });
45 | } else {
46 | warnings.push({
47 | level: 2,
48 | message: `${nicename} has a stub copyright line (with "", or "" markers).`,
49 | });
50 | }
51 | }
52 |
53 | return warnings;
54 | }
55 |
--------------------------------------------------------------------------------
/server/licenses/tags/fixed-text.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | export function validateSelf(name, text, tags) {
5 | if (tags.includes('user-supplied')) {
6 | return [
7 | {
8 | level: 0,
9 | message:
10 | `The ${name} license's text is standardized -- ` +
11 | 'if the text is actually different, you may have a different license.',
12 | },
13 | ];
14 | }
15 | }
16 |
17 | export const presentation = {
18 | fixedText: true,
19 | longText: "This license text is standardized; you don't need to paste it in.",
20 | };
21 |
--------------------------------------------------------------------------------
/server/licenses/tags/linkage.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // NOTE: this tag is referenced from the UI
5 |
6 | export const questions = {
7 | link: {
8 | label: 'Linkage',
9 | options: [
10 | ['dynamic', 'Dynamically linked'],
11 | ['static', 'Statically linked'],
12 | ],
13 | type: 'string',
14 | widget: 'radio',
15 | required: true,
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/licenses/tags/modified.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | export const questions = {
5 | modified: {
6 | label: 'Modified',
7 | options: [
8 | [false, 'Unmodified'],
9 | [true, 'Changes made'],
10 | ],
11 | type: 'boolean',
12 | widget: 'radio',
13 | required: true,
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/server/licenses/tags/notice.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // indicates that software using this license generally has a NOTICE file
5 | // these notices will be rendered under the license text.
6 | // largely Apache-2.0 specific.
7 |
8 | // we strip out the original copyright text up top
9 | export function transformCopyright(original) {
10 | return '';
11 | }
12 |
13 | // ...and place it at the bottom
14 | export function transformLicense(original, packages) {
15 | const notices: string[] = [];
16 | for (const pkg of packages) {
17 | if (pkg.copyrights.length === 0) {
18 | continue;
19 | }
20 |
21 | // oss-a-b doesn't yet support multiple copyrights
22 | const copyright = pkg.copyrights[0];
23 |
24 | const indented = copyright.replace(/^|\n/g, '\n ');
25 | const notice = `* For ${pkg.name} see also this required NOTICE:${indented}`;
26 | notices.push(notice);
27 | }
28 |
29 | const noticeText = notices.join('\n');
30 | return `${original}\n\n${noticeText}`;
31 | }
32 |
--------------------------------------------------------------------------------
/server/licenses/tags/popular.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // tslint:disable:no-empty
5 |
6 | // A demonstration of annotating licenses as "popular" so that they show
7 | // up more prominently in the license list. Can be used to mark licenses
8 | // that your company prefers, for example.
9 |
10 | export const presentation = {
11 | sortFirst: true,
12 | shortText: '⭐️',
13 | };
14 |
--------------------------------------------------------------------------------
/server/licenses/tags/spdx.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // This tag notes that the license was pulled from SPDX sources.
5 |
6 | export function transformLicense(original, packages) {
7 | let text = original;
8 |
9 | // SPDX BSD texts include a stub copyright line for some reason. remove those.
10 | // see the `all` tag for a general warning applied to these.
11 | text = text.replace(/^\s*Copyright.*<(year|owner)>.*[\r\n]*/i, '');
12 |
13 | return text;
14 | }
15 |
--------------------------------------------------------------------------------
/server/licenses/tags/unknown.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // Meta-tag: this is applied to any license that doesn't match a "known"
5 | // license (see the 'known' directory right next to this one)
6 |
7 | // tslint:disable:no-var-requires
8 |
9 | export function validateSelf(name, text, tags) {
10 | // less "confused" message for SPDX licenses
11 | if (tags.includes('spdx')) {
12 | return [
13 | {
14 | level: 1,
15 | message:
16 | `The attribution builder has the text of ${name}, ` +
17 | "but it doesn't have any additional instructions available. " +
18 | 'Review this license carefully to ensure you comply with its terms.',
19 | },
20 | ];
21 | }
22 |
23 | const proper = name ? `${name}` : 'This license';
24 | return [
25 | {
26 | level: 1,
27 | message:
28 | `${proper} is not known by the attribution builder. ` +
29 | 'Review it carefully to ensure you comply with its terms.',
30 | },
31 | ];
32 | }
33 |
34 | export const questions = {
35 | ...require('./linkage').questions,
36 | ...require('./modified').questions,
37 | };
38 |
--------------------------------------------------------------------------------
/server/licenses/tags/user-supplied.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // Meta-tag; see "all"
5 |
--------------------------------------------------------------------------------
/server/licenses/tags/validation-demo.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | export const questions = {
5 | causeWarning: {
6 | label: 'Cause a warning for this package?',
7 | options: [
8 | [false, 'No'],
9 | [true, 'Yes'],
10 | ],
11 | type: 'boolean',
12 | widget: 'select',
13 | required: true,
14 | },
15 | };
16 |
17 | export function validateUsage(pkg, usage) {
18 | if (usage.causeWarning) {
19 | return [
20 | {
21 | level: 1,
22 | message: `This is a demo validation warning because you selected "Yes" to causing warnings on a package ("${pkg.name}") with MyCustomLicense (or another license with the "validation-demo" tag. Click it to highlight the package below!`,
23 | },
24 | ];
25 | }
26 | return [];
27 | }
28 |
--------------------------------------------------------------------------------
/server/localserver.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | // tslint:disable:no-var-requires no-console
6 |
7 | import winston = require('winston');
8 | winston.configure({
9 | transports: [new winston.transports.Console()],
10 | format: winston.format.combine(
11 | winston.format.timestamp(),
12 | winston.format.errors({ stack: true }),
13 | winston.format.colorize(),
14 | winston.format.printf(
15 | (info) => `[${info.timestamp} ${info.level}] ${info.message}`
16 | )
17 | ),
18 | });
19 |
20 | if (process.env.NODE_ENV === 'development') {
21 | winston.level = 'debug';
22 | winston.warn('Starting in development mode');
23 | }
24 |
25 | // use require() instead of import to set env beforehand
26 | const config = require('./config').default;
27 | const app = require('./app');
28 |
29 | app
30 | .start(config.server.port, config.server.hostname)
31 | .then(() => {
32 | winston.info(
33 | `Server running on port ${config.server.port} [${process.env.NODE_ENV}]`
34 | );
35 | })
36 | .catch((err) => console.error(err));
37 |
--------------------------------------------------------------------------------
/server/util/credentials.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { selfDestruct } from './credentials';
5 |
6 | describe('credentials', function () {
7 | describe('selfDestruct', function () {
8 | it('should return the called function value', function () {
9 | const x = () => 123;
10 | const modified = selfDestruct(x);
11 | expect(modified()).toEqual(123);
12 | });
13 |
14 | it('should throw after first call', function () {
15 | const x = () => 123;
16 | const modified = selfDestruct(x);
17 | modified();
18 | expect(modified).toThrowError(/destroyed/);
19 | });
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/server/util/credentials.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /**
5 | * Calls the given function, storing the result (secret).
6 | * Returns a new function that can only be called once, providing the secret.
7 | */
8 | export function selfDestruct(func) {
9 | let secret = func();
10 |
11 | return () => {
12 | if (secret == undefined) {
13 | throw new Error('Secret has been destroyed');
14 | }
15 |
16 | const copy = secret;
17 | secret = undefined;
18 | return copy;
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/server/util/idgen.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { randomBytes } from 'crypto';
5 |
6 | const alpha = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ';
7 | const size = alpha.length;
8 |
9 | /**
10 | * Generate an ID of an arbitrary size.
11 | */
12 | export default function generateID(length) {
13 | length = length || 1;
14 | let str = '';
15 | while (str.length < length) {
16 | str += generateShortID();
17 | }
18 | return str.slice(0, length);
19 | }
20 |
21 | /**
22 | * Generate a Flickr/Imgur/base58-style ID to discourage iteration of projects.
23 | */
24 | function generateShortID() {
25 | // play it safe: generate a number significantly smaller than Number.MAX_SAFE_INTEGER
26 | let id = parseInt(randomBytes(4).toString('hex'), 16);
27 | let str = '';
28 | while (id > size) {
29 | const index = id % size;
30 | str += alpha[index];
31 | id = (id - index) / size;
32 | }
33 | return str;
34 | }
35 |
--------------------------------------------------------------------------------
/server/util/middleware.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { Request, Response } from 'express';
5 |
6 | type Handler = (req: Request, res: Response) => Promise;
7 |
8 | export function asyncApi(handler: Handler) {
9 | return (req, res, next) => {
10 | handler(req, res)
11 | .then((obj) => {
12 | if (obj == undefined) {
13 | res.status(404).send({ error: 'Object not found' });
14 | } else {
15 | res.send(obj);
16 | }
17 | })
18 | .catch(next);
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/spec/helpers/output.js:
--------------------------------------------------------------------------------
1 | const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
2 |
3 | jasmine.getEnv().clearReporters();
4 | jasmine.getEnv().addReporter(new SpecReporter());
5 |
--------------------------------------------------------------------------------
/spec/selenium/driver.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // tslint:disable:no-console
5 |
6 | import webdriver = require('selenium-webdriver');
7 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 30 * 1000;
8 |
9 | export type CustomDriver = webdriver.WebDriver & {
10 | getRelative: (path: string) => webdriver.promise.Promise;
11 | setUser: (user: string) => webdriver.promise.Promise;
12 | };
13 |
14 | export default async function (): Promise {
15 | const driver: any = await new webdriver.Builder()
16 | .usingServer('http://localhost:4444/wd/hub')
17 | .forBrowser('chrome')
18 | .build();
19 | driver.getRelative = function (path: string) {
20 | return driver.get(`http://web:8000${path}`);
21 | };
22 | driver.setUser = async function (user: string = 'selenium') {
23 | driver.getRelative('/dummy-no-auth');
24 | await driver
25 | .manage()
26 | .addCookie({ name: 'nullauth-dummy-user', value: user });
27 | };
28 | await driver.setUser();
29 | return driver;
30 | }
31 |
--------------------------------------------------------------------------------
/spec/selenium/landing.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /* tslint:disable:no-floating-promises */
5 |
6 | import { By, until } from 'selenium-webdriver';
7 | import build, { CustomDriver } from './driver';
8 |
9 | describe('landing page', function () {
10 | let driver: CustomDriver;
11 | beforeAll(async function () {
12 | driver = await build();
13 | });
14 | afterAll(async function () {
15 | await driver.quit();
16 | });
17 |
18 | it('loads correctly', async function () {
19 | driver.getRelative('/');
20 | const title = await driver.getTitle();
21 | expect(title).toContain('Attribution Builder');
22 | await driver.wait(until.elementLocated(By.className('jumbotron')));
23 | });
24 |
25 | it('can navigate and render the new project form', async function () {
26 | driver.getRelative('/');
27 | driver.findElement(By.css('a[href="/projects/new"]')).click();
28 | await driver.wait(until.elementLocated(By.id('description')), 1000);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/spec/selenium/projects-auth.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /* tslint:disable:no-floating-promises */
5 |
6 | import { By, until } from 'selenium-webdriver';
7 | import build, { CustomDriver } from './driver';
8 |
9 | const projectName = 'ACL Test ' + new Date().getTime();
10 |
11 | describe('project authentication', function () {
12 | let driver: CustomDriver;
13 | beforeAll(async function () {
14 | driver = await build();
15 | await driver.manage().timeouts().implicitlyWait(1000);
16 | });
17 | afterAll(async function () {
18 | await driver.quit();
19 | });
20 |
21 | it('bootstraps a project', async function () {
22 | driver.getRelative('/projects/new');
23 | driver.wait(until.elementLocated(By.id('onboarding-form')));
24 |
25 | driver.findElement(By.id('title')).sendKeys(projectName);
26 | driver.findElement(By.id('version')).sendKeys('x');
27 | driver.findElement(By.id('description')).sendKeys('x');
28 | driver
29 | .findElement(By.css('input[name="openSourcing"][value="false"]'))
30 | .click();
31 | driver.findElement(By.id('legalContact')).sendKeys('a-real-person');
32 | driver.findElement(By.id('plannedRelease')).sendKeys('1111-11-11');
33 | driver
34 | .findElement(By.css('#ownerGroup-container .Select-placeholder'))
35 | .click();
36 | driver
37 | .findElement(
38 | By.xpath(
39 | '//*[@id="ownerGroup-container"]//div[@class="Select-menu"]//div[text()="everyone"]'
40 | )
41 | )
42 | .click();
43 |
44 | driver
45 | .findElement(By.css('form#onboarding-form button[type="submit"]'))
46 | .click();
47 |
48 | const headerText = await driver
49 | .findElement(By.id('project-heading'))
50 | .getText();
51 | expect(headerText).toContain(projectName);
52 | });
53 |
54 | it('can load the acl editor', async function () {
55 | driver.findElement(By.id('tools-dropdown-toggle')).click();
56 | driver.findElement(By.css('a[href$="/acl"]')).click();
57 | await driver.wait(until.elementLocated(By.id('project-acl-editor')));
58 | });
59 |
60 | it('wont let a person disown a project', async function () {
61 | const input = driver.findElement(
62 | By.xpath('//form//tbody/tr[1]//input[@type="text"]')
63 | );
64 | input.clear();
65 | input.sendKeys('blah');
66 | driver.findElement(By.css('button[type="submit"]')).click();
67 |
68 | driver.sleep(1000); // modal animation
69 | const errorText = await driver
70 | .findElement(By.css('#error-modal .modal-body'))
71 | .getText();
72 | expect(errorText).toContain('cannot remove yourself');
73 | driver.findElement(By.css('#error-modal button.btn-primary')).click(); // close button
74 | driver.wait(
75 | until.elementIsNotVisible(driver.findElement(By.css('#error-modal')))
76 | );
77 | driver.sleep(1000); // modal animation
78 |
79 | input.clear();
80 | await input.sendKeys('self:selenium');
81 | });
82 |
83 | it('can add some friends to the list', async function () {
84 | driver.findElement(By.id('acl-add')).click();
85 | const select2 = driver.findElement(By.xpath('//form//tbody/tr[2]//select'));
86 | const input2 = driver.findElement(
87 | By.xpath('//form//tbody/tr[2]//input[@type="text"]')
88 | );
89 | select2.click();
90 | const select2editor = driver.findElement(
91 | By.xpath('//form//tbody/tr[2]//select/option[@value="editor"]')
92 | );
93 | select2editor.click();
94 | input2.sendKeys('self:fake-editor');
95 |
96 | driver.findElement(By.id('acl-add')).click();
97 | const select3 = driver.findElement(By.xpath('//form//tbody/tr[3]//select'));
98 | const input3 = driver.findElement(
99 | By.xpath('//form//tbody/tr[3]//input[@type="text"]')
100 | );
101 | select3.click();
102 | const select3viewer = driver.findElement(
103 | By.xpath('//form//tbody/tr[3]//select/option[@value="viewer"]')
104 | );
105 | select3viewer.click();
106 | input3.sendKeys('self:fake-viewer');
107 |
108 | await driver.findElement(By.css('button[type="submit"]')).click();
109 | });
110 |
111 | it('cannot edit acl as editor', async function () {
112 | // switch users and return
113 | driver.wait(until.elementLocated(By.id('project-heading')));
114 | const projectUrl = await driver.getCurrentUrl();
115 | driver.setUser('fake-editor');
116 | await driver.get(projectUrl);
117 |
118 | // ensure the acl link isn't clickable
119 | const info = driver.findElement(By.id('acl-owner-info'));
120 | const classes = await info.getAttribute('class');
121 | const text = await info.getText();
122 | expect(classes).not.toContain('EditableText');
123 | expect(text).toContain('owned by');
124 |
125 | // but that main project details still are
126 | await driver.findElement(By.css('#project-heading .EditableText'));
127 | });
128 |
129 | it('cannot edit project as viewer', async function () {
130 | const projectUrl = await driver.getCurrentUrl();
131 | driver.setUser('fake-viewer');
132 | await driver.get(projectUrl);
133 |
134 | // nothing should be editable
135 | const numEditable = await driver.findElements(By.className('EditableText'));
136 | expect(numEditable.length).toEqual(0);
137 | });
138 | });
139 |
--------------------------------------------------------------------------------
/spec/support/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "spec",
3 | "helpers": [
4 | "helpers/**/*.js"
5 | ],
6 | "random": false
7 | }
--------------------------------------------------------------------------------
/styles/bootstrap-overrides.scss:
--------------------------------------------------------------------------------
1 | $white: #fff;
2 | $gray-100: #f8f9fa;
3 | $gray-200: #e9ecef;
4 | $gray-300: #dee2e6;
5 | $gray-400: #ced4da;
6 | $gray-500: #adb5bd;
7 | $gray-600: #868e96;
8 | $gray-700: #495057;
9 | $gray-800: #343a40;
10 | $gray-900: #212529;
11 | $black: #000;
12 |
13 | $theme-colors: (
14 | primary: #088f95,
15 | secondary: $gray-400,
16 | success: #395aab,
17 | info: #4aadb1,
18 | warning: #ffc141,
19 | danger: #f7790c,
20 | light: $gray-200,
21 | dark: #00585c,
22 | );
23 |
24 | $body-bg: $gray-100;
25 |
26 | $border-radius: 0.15rem;
27 | $border-radius-lg: 0.18rem;
28 | $border-radius-sm: 0.12rem;
29 |
30 | $font-size-base: 0.9rem;
31 |
32 | $h1-font-size: 2.2rem;
33 | $h2-font-size: 1.9rem;
34 | $h3-font-size: 1.6rem;
35 | $h4-font-size: 1.3rem;
36 | $h5-font-size: 1.1rem;
37 | $h6-font-size: 1rem;
38 |
--------------------------------------------------------------------------------
/styles/site.scss:
--------------------------------------------------------------------------------
1 | // Place any site-specific customizations here.
2 | // This file will never have any upstream changes in it; you can
3 | // replace it entirely.
4 |
--------------------------------------------------------------------------------
/styles/style.scss:
--------------------------------------------------------------------------------
1 | @import './bootstrap-overrides.scss';
2 | @import '~bootstrap/scss/bootstrap.scss';
3 |
4 | @import '~font-awesome/css/font-awesome.css';
5 | @import '~react-select/scss/default.scss';
6 |
7 | /*** Project styles ***/
8 |
9 | .EditableText {
10 | border-bottom: 1px dashed #bebebe;
11 |
12 | &:hover {
13 | background-color: #e3e3e3;
14 | cursor: pointer;
15 | }
16 |
17 | a:hover {
18 | text-decoration: none;
19 | }
20 | }
21 |
22 | .line-highlight {
23 | background-color: theme-color('warning');
24 | }
25 |
26 | dd.fixed-text {
27 | margin-left: 20px;
28 | max-height: 100px;
29 | overflow-y: auto;
30 | font-family: monospace;
31 | white-space: pre;
32 |
33 | > div {
34 | border: 1px dashed rgb(142, 175, 204);
35 | padding: 3px 5px;
36 | }
37 | }
38 |
39 | .document-warning {
40 | cursor: pointer;
41 |
42 | &:hover {
43 | border: 1px solid #555;
44 | }
45 | }
46 |
47 | @import './site.scss';
48 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "moduleResolution": "node",
5 | "target": "es2015",
6 | "lib": [
7 | "dom",
8 | "es2017"
9 | ],
10 | "sourceMap": true,
11 | "rootDir": "./",
12 | "outDir": "./build/",
13 | "pretty": true,
14 | "strict": true,
15 | "noImplicitAny": false,
16 | "noUnusedLocals": true
17 | },
18 | "include": [
19 | "server/**/*.ts",
20 | "spec/**/*.ts"
21 | ],
22 | "exclude": [
23 | "node_modules"
24 | ]
25 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended",
5 | "tslint-react",
6 | "tslint-config-prettier"
7 | ],
8 | "rules": {
9 | "interface-name": [false],
10 | "jsx-no-lambda": [false],
11 | "max-classes-per-file": [false],
12 | "member-access": [false],
13 | "member-ordering": [false],
14 | "no-floating-promises": [true],
15 | "no-null-keyword": [true],
16 | "no-unused-expression": [true],
17 | "no-unused-variable": [true],
18 | "object-literal-sort-keys": [false],
19 | "only-arrow-functions": [false],
20 | "triple-equals": [true, "allow-undefined-check"]
21 | }
22 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | 'use strict';
5 |
6 | const path = require('path');
7 | const webpack = require('webpack');
8 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
9 |
10 | const prod = process.env.NODE_ENV === 'production';
11 |
12 | let plugins = [
13 | new webpack.ProvidePlugin({
14 | $: 'jquery',
15 | jQuery: 'jquery',
16 | Popper: ['popper.js', 'default'],
17 | }),
18 | new MiniCssExtractPlugin({
19 | filename: '[name].css',
20 | }),
21 | ];
22 |
23 | module.exports = {
24 | mode: prod ? 'production' : 'development',
25 |
26 | resolve: {
27 | extensions: ['.ts', '.tsx', '.js', '.scss'],
28 | },
29 |
30 | module: {
31 | rules: [
32 | {
33 | test: /\.tsx?$/,
34 | loader: 'ts-loader',
35 | options: {
36 | configFile: 'browser/tsconfig.json',
37 | },
38 | },
39 | {
40 | test: /\.scss$/,
41 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
42 | },
43 | {
44 | test: /\.(woff2?|svg)(\?v=[\d\.]+)?$/,
45 | loader: 'url-loader?limit=10000',
46 | },
47 | {
48 | test: /\.(png|ttf|eot)(\?v=[\d\.]+)?$/,
49 | loader: 'file-loader',
50 | },
51 | ],
52 | },
53 |
54 | entry: {
55 | app: './browser/app.tsx',
56 | style: './styles/style.scss',
57 | },
58 |
59 | output: {
60 | path: path.join(__dirname, '/build/res'),
61 | filename: '[name].js',
62 | publicPath: '/res/',
63 | },
64 |
65 | devtool: prod ? 'source-map' : 'cheap-module-source-map',
66 |
67 | plugins,
68 |
69 | devServer: {
70 | port: 2425,
71 | publicPath: '/res/',
72 | proxy: {
73 | '*': 'http://localhost:2424',
74 | },
75 | stats: 'minimal',
76 | },
77 | };
78 |
--------------------------------------------------------------------------------