Compare commits

..

38 Commits
0.3.0 ... 0.4.6

Author SHA1 Message Date
Disk2019
019b838925 Updated Some More Tweaks (#13) 2020-06-29 16:06:22 +01:00
Disk2019
0fc18459cd Updated Some Tweaks (#11)
ResetBase is by Default Disabled in Win10 Dism.exe /online /Cleanup-Image /StartComponentCleanup . Hence added this tweak to enable it in script on line 271 .
Updated HKU Tweaks to Correct HKCU Values & removed last experimental tweak as its not needed anymore & does not do anything exccept loading default user hive & then unloading it.
2020-06-29 15:51:40 +01:00
undergroundwires
583c5660d6 removed failing continuous deployment #14 2020-06-29 14:51:52 +01:00
Disk2019
52d5713a99 Fixed Some More Issues (#12)
Fixed typo issues :
Force enable data execution prevention (DEP)
disable cortana
Disable diagnostics telemetry
Empty trash bin
2020-06-19 20:08:16 +00:00
undergroundwires-bot
b34a66f270 ⬆️ bumped to 0.4.5 2020-06-13 00:59:26 +00:00
dependabot[bot]
eed996f608 Bump websocket-extensions from 0.1.3 to 0.1.4 (#9)
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-06-13 00:59:03 +00:00
undergroundwires-bot
b96c5d0557 ⬆️ bumped to 0.4.4 2020-05-24 19:25:47 +01:00
undergroundwires
aab8f21a8d clicking outside of a card closes it 2020-05-24 19:25:47 +01:00
undergroundwires
c668a97950 fix "group by" overflows on smaller screens 2020-05-24 19:25:47 +01:00
undergroundwires
bb98d20637 one command to lint everything "npm run lint" 2020-05-24 19:25:47 +01:00
undergroundwires
e2ab124fb7 new footer with privacy policy 2020-05-24 19:25:47 +01:00
undergroundwires
0d2efe5b05 fixed close card button not being visible & cleanup 2020-05-24 19:25:47 +01:00
undergroundwires-bot
156a6554ef ⬆️ bumped to 0.4.3 2020-05-24 19:25:47 +01:00
undergroundwires
4a91e8ccd8 automated using bump-everywhere + more quality checks (#8)
- new workflows
- linting commands & linted stuff
- security checks & fixed audited vulnerabilities
- updated documentation
2020-05-24 19:25:43 +01:00
undergroundwires
997be7113f using deployment operations from aws-static-site-with-cd 2020-05-23 22:59:29 +01:00
undergroundwires
3e3bc07576 automatically increases patch number #5 2020-04-26 15:47:20 +00:00
undergroundwires
691f989682 reading version from package.json instead of version file #5 2020-04-26 15:47:20 +00:00
undergroundwires
226074c534 simplified heading 2020-04-26 15:47:20 +00:00
undergroundwires
97b7e03233 fixed broke link 2020-04-26 15:47:20 +00:00
undergroundwires
749a140eb8 removed redundant documentation 2020-04-26 15:47:20 +00:00
undergroundwires
4739a4ac40 Merge pull request #4 from undergroundwires/dependabot/npm_and_yarn/acorn-6.4.1
Bump acorn from 6.4.0 to 6.4.1
2020-04-05 16:58:59 +00:00
dependabot[bot]
4800340b9b Bump acorn from 6.4.0 to 6.4.1
Bumps [acorn](https://github.com/acornjs/acorn) from 6.4.0 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.4.0...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-04-05 16:20:27 +00:00
undergroundwires
074734242b 🚀 0.4.2 release 2020-02-29 18:51:55 +01:00
undergroundwires
802b36bdd8 shortened all HKEY paths 2020-01-12 10:10:37 +01:00
undergroundwires
0c39a06be5 set font on input 2020-01-11 10:55:43 +01:00
undergroundwires
e63ac4ae67 added missing semicolon for masking 2020-01-11 09:45:37 +01:00
undergroundwires
edd076fade 🚀 0.4.1 release 2020-01-11 09:27:19 +01:00
undergroundwires
0ce354ea09 using right 🔍 input type 2020-01-11 09:14:45 +01:00
undergroundwires
19813b6917 more efficient queries with single lowercase 2020-01-11 08:57:24 +01:00
undergroundwires
97a7747933 👀🔍 showing search queries 2020-01-11 08:57:06 +01:00
undergroundwires
92f1a36bcb hide grouping while searching 2020-01-11 07:20:44 +01:00
undergroundwires
31364bdfec fixed search bug 2020-01-11 07:18:02 +01:00
undergroundwires
5b743a67a4 Merge pull request #3 from undergroundwires/develop
Develop
2020-01-11 05:57:01 +01:00
undergroundwires
16a7327750 🚀 v0.4.0 2020-01-11 05:50:58 +01:00
undergroundwires
5ea46ecbf5 more margin for the scripts 2020-01-11 05:13:18 +01:00
undergroundwires
e3f82e069e refactorings 2020-01-11 05:13:03 +01:00
undergroundwires
95baf3175b more scripts & better organized 2020-01-11 05:12:36 +01:00
undergroundwires
89862b2775 🔍 support for search 2020-01-10 01:35:09 +01:00
59 changed files with 4686 additions and 2925 deletions

View File

@@ -1,91 +0,0 @@
name: Build & deploy
on:
push:
branches:
- master
jobs:
build-and-deploy:
runs-on: ubuntu-18.04
steps:
-
name: "Prepare: Checkout"
uses: actions/checkout@v1
-
name: "Prepare: Create AWS user profile"
run: >-
bash "aws/scripts/configure/create-user-profile.sh" \
--profile user \
--access-key-id ${{secrets.AWS_DEPLOYMENT_USER_ACCESS_KEY_ID}} \
--secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \
--region us-east-1
-
name: "Infrastructure: Deploy IAM stack"
run: >-
bash "aws/scripts/deploy/deploy-stack.sh" \
--template-file aws/iam-stack.yaml \
--stack-name privacysexy-iam-stack \
--capabilities CAPABILITY_IAM \
--region us-east-1 --role-arn ${{secrets.AWS_IAM_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}}
-
name: "Infrastructure: Deploy certificate stack"
run: >-
bash "aws/scripts/deploy/deploy-stack.sh" \
--template-file aws/certificate-stack.yaml \
--stack-name privacysexy-certificate-stack \
--region us-east-1 \
--role-arn ${{secrets.AWS_CERTIFICATE_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}}
-
name: "Infrastructure: Deploy DNS stack"
run: >-
bash "aws/scripts/deploy/deploy-stack.sh" \
--template-file aws/dns-stack.yaml \
--stack-name privacysexy-dns-stack \
--region us-east-1 \
--role-arn ${{secrets.AWS_DNS_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}}
-
name: "Infrastructure: Deploy web stack"
run: >-
bash "aws/scripts/deploy/deploy-stack.sh" \
--template-file aws/web-stack.yaml \
--stack-name privacysexy-web-stack \
--region us-east-1 \
--role-arn ${{secrets.AWS_WEB_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}}
-
name: "App: Setup node"
uses: actions/setup-node@v1
with:
node-version: '11.x'
-
name: "App: Install dependencies"
run: npm install
-
name: "App: Run tests"
run: npm run test:unit
-
name: "App: Build"
run: npm run build
-
name: "App: Deploy to S3"
run: >-
bash "aws/scripts/deploy/deploy-to-s3.sh" \
--folder dist \
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
--storage-class ONEZONE_IA \
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
--region us-east-1 \
--profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}}
-
name: "App: Invalidate CloudFront cache"
run: >-
bash "aws/scripts/deploy/invalidate-cloudfront-cache.sh" \
--paths "/*" \
--web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \
--role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \
--region us-east-1 \
--profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}}

18
.github/workflows/bump-and-release.yaml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Bump & release
on:
push: # Ensure a new release is created for each new tag
tags:
- '[0-9]+.[0-9]+.[0-9]+'
jobs:
bump-version-and-release:
if: github.event.base_ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
-
uses: undergroundwires/bump-everywhere@master
with:
user: undergroundwires-bot
release-token: ${{secrets.BUMP_GITHUB_PAT}} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559
# GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo.

117
.github/workflows/deploy.yaml vendored Normal file
View File

@@ -0,0 +1,117 @@
name: Build & deploy
on:
release:
types: [created] # will be triggered when a NON-draft release is created and published.
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
-
name: "Infrastructure: Checkout"
uses: actions/checkout@v2
with:
path: aws
repository: undergroundwires/aws-static-site-with-cd
-
name: "Infrastructure: Create AWS user profile & session name"
run: >-
bash "scripts/configure/create-user-profile.sh" \
--profile user \
--access-key-id ${{secrets.AWS_DEPLOYMENT_USER_ACCESS_KEY_ID}} \
--secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \
--region us-east-1 \
&& \
echo "::set-env name=SESSION_NAME::${{github.actor}}-${{github.event_name}}-$(echo ${{github.sha}} | cut -c1-8)"
working-directory: aws
-
name: "Infrastructure: Deploy IAM stack"
run: >-
bash "scripts/deploy/deploy-stack.sh" \
--template-file stacks/iam-stack.yaml \
--stack-name privacysexy-iam-stack \
--capabilities CAPABILITY_IAM \
--parameter-overrides "WebStackName=privacysexy-web-stack DnsStackName=privacysexy-dns-stack \
CertificateStackName=privacysexy-cert-stack RootDomainName=privacy.sexy" \
--region us-east-1 --role-arn ${{secrets.AWS_IAM_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{ env.SESSION_NAME }}
working-directory: aws
-
name: "Infrastructure: Deploy DNS stack"
run: >-
bash "scripts/deploy/deploy-stack.sh" \
--template-file stacks/dns-stack.yaml \
--stack-name privacysexy-dns-stack \
--parameter-overrides "RootDomainName=privacy.sexy" \
--region us-east-1 \
--role-arn ${{secrets.AWS_DNS_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{ env.SESSION_NAME }}
working-directory: aws
-
name: "Infrastructure: Deploy certificate stack"
run: >-
bash "scripts/deploy/deploy-stack.sh" \
--template-file stacks/certificate-stack.yaml \
--stack-name privacysexy-cert-stack \
--capabilities CAPABILITY_IAM \
--parameter-overrides "IamStackName=privacysexy-iam-stack RootDomainName=privacy.sexy DnsStackName=privacysexy-dns-stack" \
--region us-east-1 \
--role-arn ${{secrets.AWS_CERTIFICATE_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{ env.SESSION_NAME }}
working-directory: aws
-
name: "Infrastructure: Deploy web stack"
run: >-
bash "scripts/deploy/deploy-stack.sh" \
--template-file stacks/web-stack.yaml \
--stack-name privacysexy-web-stack \
--parameter-overrides "CertificateStackName=privacysexy-cert-stack DnsStackName=privacysexy-dns-stack \
RootDomainName=privacy.sexy UseDeepLinks=true" \
--capabilities CAPABILITY_IAM \
--region us-east-1 \
--role-arn ${{secrets.AWS_WEB_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{ env.SESSION_NAME }}
working-directory: aws
-
name: "App: Checkout"
uses: actions/checkout@v2
with:
path: site
ref: master # otherwise we don't get version bump commit
-
name: "App: Setup node"
uses: actions/setup-node@v1
with:
node-version: '14.x'
-
name: "App: Install dependencies"
run: npm install
working-directory: site
-
name: "App: Run tests"
run: npm run test:unit
working-directory: site
-
name: "App: Build"
run: npm run build
working-directory: site
-
name: "App: Deploy to S3"
run: >-
bash "aws/scripts/deploy/deploy-to-s3.sh" \
--folder site/dist \
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
--storage-class ONEZONE_IA \
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
--region us-east-1 \
--profile user --session ${{ env.SESSION_NAME }}
-
name: "App: Invalidate CloudFront cache"
run: >-
bash "aws/scripts/deploy/invalidate-cloudfront-cache.sh" \
--paths "/*" \
--web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \
--role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \
--region us-east-1 \
--profile user --session ${{ env.SESSION_NAME }}

37
.github/workflows/quality-checks.yaml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Quality checks
on:
pull_request:
branches:
- master
jobs:
lint:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Setup node
uses: actions/setup-node@v1
with:
node-version: 14.x
-
name: Install dependencies
run: npm ci
-
name: Lint vue
run: npm run lint:vue
-
name: Lint yaml
run: npm run lint:yaml
-
name: 'Validate md: Relative URLs'
run: npm run lint:md:relative-urls
-
name: 'Validate md: Enforce standards'
run: npm run lint:md
-
name: 'Validate md: Ensure consistency'
run: npm run lint:md:consistency

24
.github/workflows/security-checks.yaml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Security checks
on:
pull_request:
branches:
- master
schedule:
- cron: '0 0 * * 0'
jobs:
npm-audit:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Setup node
uses: actions/setup-node@v1
with:
node-version: 14.x
-
name: NPM audit
run: npm audit

View File

@@ -1,26 +1,25 @@
name: Run tests
name: Test
on:
push:
pull_request:
branches:
- '*'
- '!master'
- master
jobs:
build-and-deploy:
run-tests:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2
-
name: Setup node
uses: actions/setup-node@v1
with:
node-version: '11.x'
node-version: '14.x'
-
name: Install dependencies
run: npm install
run: npm ci
-
name: Run tests
run: npm run test:unit

4
.markdownlint.json Normal file
View File

@@ -0,0 +1,4 @@
{
"default": true,
"MD013": false
}

View File

@@ -1,33 +1,114 @@
# Changelog
- All notable changes to this project will be documented in this file.
## 0.4.5 (2020-06-13)
## [Unreleased]
* automated using bump-everywhere + more quality checks (#8) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a27ca65b9eb29f009553e047bfb52f4200c0bd17)
* fixed close card button not being visible & cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/49249399fe89575ac26b26a2b063c50b29b2f554)
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ff334704466a1d8e04660ac61da777681ebb93e5)
* one command to lint everything "npm run lint" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/690f34686f4d35f09e78e5870447b09d7dfaa91b)
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/fb40bc571becf4e97d782af7d0de311cab37cec2)
* fix "group by" overflows on smaller screens | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e07609779b605ce7c80f75f186e4bacff60e8861)
* clicking outside of a card closes it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7b033f8d5b0e086c6076c1db3391144ae14ad296)
* automated using bump-everywhere + more quality checks (#8) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4a91e8ccd8a707bc6bea34ee28cff7fa4f66ee2f)
* fixed close card button not being visible & cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0d2efe5b05aa965458b78b8fa43754ce2f4fe11b)
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e2ab124fb799f56ada3570fdc911361cb803e889)
* one command to lint everything "npm run lint" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/bb98d20637cbf1d524ebb2973e308773006e3153)
* fix "group by" overflows on smaller screens | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c668a97950a1cb7c8bf2a7fd8a72d1101e65e8ce)
* clicking outside of a card closes it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/aab8f21a8d8dbed54798af581e6e1ad9e86a4be1)
## [0.3.0] - 2020-01-09
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.4...0.4.5)
- Added support for grouping
- Added some meta tags
- Added "Disable NVIDIA telemetry" script
- Added legacy browser compatibility for fonts & changed main font
## 0.4.4 (2020-05-24)
## [0.2.0] - 2020-01-06
* fixed close card button not being visible & cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/49249399fe89575ac26b26a2b063c50b29b2f554)
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ff334704466a1d8e04660ac61da777681ebb93e5)
* one command to lint everything "npm run lint" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/690f34686f4d35f09e78e5870447b09d7dfaa91b)
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/fb40bc571becf4e97d782af7d0de311cab37cec2)
* fix "group by" overflows on smaller screens | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e07609779b605ce7c80f75f186e4bacff60e8861)
* clicking outside of a card closes it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7b033f8d5b0e086c6076c1db3391144ae14ad296)
- Fixed typo in generated code.
- Better URL validation for documentation links in `application.yaml`.
- Slightly faster parsing of `application.yaml`
- Styled no JS error that's shown when JavaScript is disabled.
- The default selection is now *None* & instruction text is shown in code box when nothing is selected.
- Added hyphen lines when rendering of long function names
- Changed subtitle: added version as footer instead.
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.3...0.4.4)
## [0.1.0] - 2019-12-31
## 0.4.3 (2020-05-23)
- Initial release
* removed redundant documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/749a140eb8dba09cb67fec2f8dec937e66e3cff5)
* fixed broke link | [commit](https://github.com/undergroundwires/privacy.sexy/commit/97b7e03233d9718a8df30cb01ce06ca9489a0295)
* simplified heading | [commit](https://github.com/undergroundwires/privacy.sexy/commit/226074c5342f7463c06fcff1457d352ca30295a3)
* reading version from package.json instead of version file #5 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/691f989682179016ddcbf55a05cded29155288c9)
* automatically increases patch number #5 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3e3bc07576f7c7e74e3e11fc7d197cbb9a9fb8c0)
* using deployment operations from aws-static-site-with-cd | [commit](https://github.com/undergroundwires/privacy.sexy/commit/997be7113f676888892ffa35566d9ebb58a3e9ea)
* automated using bump-everywhere + more quality checks (#8) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a27ca65b9eb29f009553e047bfb52f4200c0bd17)
## All releases
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.2...0.4.3)
- [Unreleased] : https://github.com/undergroundwires/privacy.sexy/compare/v0.3.0...HEAD
- [v0.3.0] : https://github.com/undergroundwires/privacy.sexy/compare/v0.2.0...v0.3.0
- [v0.2.0] : https://github.com/undergroundwires/privacy.sexy/compare/v0.1.0...v0.2.0
- [v0.1.0] : https://github.com/undergroundwires/privacy.sexy/releases/tag/v0.1.0
## 0.4.2 (2020-02-29)
* added missing semicolon for masking | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e63ac4ae67da68243a525af149ff30e5d485b641)
* set font on input | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0c39a06be5e4b0a2031ad5e9f5220dd669afee53)
* shortened all HKEY paths | [commit](https://github.com/undergroundwires/privacy.sexy/commit/802b36bdd8dcc1f0a2853fe7da2ea2fccd69a88c)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.1...0.4.2)
## 0.4.1 (2020-01-11)
* fixed search bug | [commit](https://github.com/undergroundwires/privacy.sexy/commit/31364bdfec503af09ffbb58044a17dfb833fc8d9)
* hide grouping while searching | [commit](https://github.com/undergroundwires/privacy.sexy/commit/92f1a36bcb1e1fe7c90efe8ccd3ede55991e9d9c)
* 👀🔍 showing search queries | [commit](https://github.com/undergroundwires/privacy.sexy/commit/97a7747933d2b515cc03ab8243e6a8ae702ef16a)
* more efficient queries with single lowercase | [commit](https://github.com/undergroundwires/privacy.sexy/commit/19813b691746d98670823025c460480400e34b6e)
* using right 🔍 input type | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0ce354ea0956391ad3f37b252daac1127bfc601a)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.0...0.4.1)
## 0.4.0 (2020-01-11)
* 🔍 support for search | [commit](https://github.com/undergroundwires/privacy.sexy/commit/89862b2775703257b9dc2e19fbebde2c0d0fbda0)
* more scripts & better organized | [commit](https://github.com/undergroundwires/privacy.sexy/commit/95baf3175b0d2c7df516f7893a96346b94ac8eca)
* refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e3f82e069e305f6d94eab335470c8e7b44295dd6)
* more margin for the scripts | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5ea46ecbf52236953d19f09a8eade08b83e6cd34)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.3.0...0.4.0)
## 0.3.0 (2020-01-09)
* added description & more descriptive title | [commit](https://github.com/undergroundwires/privacy.sexy/commit/99576340b648550149871e2c0fe0b0d8c2dd0d7c)
* allow robots | [commit](https://github.com/undergroundwires/privacy.sexy/commit/eee0e785ec2c5e6bed53d21b4126a57773e35dba)
* removed unused references | [commit](https://github.com/undergroundwires/privacy.sexy/commit/cfd888f3afc5c260a0a4a73f2843b86b9f1df2cd)
* 🚫 disable NVIDIA telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ab28f4ed8538d51e1777c86302a63a0cd9c3cb2a)
* backwards compatibility for fonts | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4bc13e11926a6df77079646499e799742153b4ab)
* added back meta needed for responsiveness | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ed872ef3d9f6c92afc0ce0d06998c60463a8b4e8)
* fancy-font is renamed to main and now used | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6825001c61426194dc363b96b57a321241f3ba57)
* added support for grouping | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ec6b3c54072a77bb4305da1c234db6c649218b88)
* less hyphens as it looks better on mobile | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e0b080af69157f46ba12e2c25e794f5384671b51)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.2.0...0.3.0)
## 0.2.0 (2020-01-06)
* added GitHub Actions badge for build & deploy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a229aca68a92bbcd8e8176ac1dd25ce03509e074)
* more badges 📛🏆📜 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/090e8319091044e53484ba8338510f6fb7c3cb80)
* typo fixes + whitespace refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e99f210c9dcf61a21e445e2a331384b6066f2c98)
* switched content information to "why" section | [commit](https://github.com/undergroundwires/privacy.sexy/commit/beb3c8339f83a224ca66ad8a911a9265ffe7c9c0)
* fixed contribution URL | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7b4277d7706ccf6ba7e4b7b01aa46f8e3852cfc6)
* fixed wrong relation + lighter style | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8d05b03c9f3c9fc015be6615da8c283809712065)
* better URL validation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/aff463dd64fecff92a786fcba88621dff6b1cf73)
* refactoring to new function | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c646c102730481c3f4648eb714dc0a84ce35b13c)
* optimized find queries & refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d38f6cd6a8b33e11df854c7abea05974dc04d4ce)
* 🎨 styled no JS error | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c359f1d89c6874b3cc94154b993e33f58bd32268)
* simplified finding duplicates | [commit](https://github.com/undergroundwires/privacy.sexy/commit/57037aaefcc0e80f0f4719cea89568490a731028)
* fixed maintainability badge URL | [commit](https://github.com/undergroundwires/privacy.sexy/commit/aaea47e7d15fe41dea26968db0107a0c53d108f3)
* fixed wrong line dumps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5ccc7c59528885ae7729197df3dfa00f924a2b3f)
* refactorings in parsing | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2aa3742e30646bf1d1f3779419d161c3fb6c4808)
* using free function | [commit](https://github.com/undergroundwires/privacy.sexy/commit/20020af7c1d8de13948d8761fd4e7f0affb2badb)
* default selection is now none | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3140cc663b86394d543de90228aa53e6a304d8d9)
* added hyphen lines for longer names | [commit](https://github.com/undergroundwires/privacy.sexy/commit/cced601d686d550f4225018e5311b7433efbb5ae)
* more descriptive subtitle | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2cf9214b14d9720f747a71b3864ba7a28acf0ff4)
* added footer with version | [commit](https://github.com/undergroundwires/privacy.sexy/commit/10a34fae2f1a219ec52db0c74edb39b46ebd8abc)
* using font variables | [commit](https://github.com/undergroundwires/privacy.sexy/commit/60e6348dc8d53f1e81ebdb2ec0e1962aac1e9842)
* code-gen refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/246e753ddc9dc8bf630e538663584bf3423cc749)
* added text when nothing is chosen | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a7da75d4428090423b692ce45423f5bd300d8442)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.1.0...0.2.0)
## 0.1.0 (2019-12-31)
Initial release | [commits](https://github.com/undergroundwires/privacy.sexy/commit/4e7f244190c6ffbf7b20443e3e69cf2402c4268a)

View File

@@ -1,17 +1,20 @@
# privacy.sexy
![Build & deploy status](https://github.com/undergroundwires/privacy.sexy/workflows/Build%20&%20deploy/badge.svg)
![Vulnerabilities](https://snyk.io/test/github/undergroundwires/privacy.sexy/badge.svg)
> Web tool to enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/undergroundwires/privacy.sexy/issues)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
[![Maintainability](https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability)](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it.
> because privacy is sexy 🍑🍆
[![Tests status](https://github.com/undergroundwires/privacy.sexy/workflows/Test/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Quality checks status](https://github.com/undergroundwires/privacy.sexy/workflows/Quality%20checks/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Security checks status](https://github.com/undergroundwires/privacy.sexy/workflows/Security%20checks/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Bump & release status](https://github.com/undergroundwires/privacy.sexy/workflows/Bump%20&%20release/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Deploy status](https://github.com/undergroundwires/privacy.sexy/workflows/Build%20&%20deploy/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Auto-versioned by bump-everywhere](https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true)](https://github.com/undergroundwires/bump-everywhere)
[https://privacy.sexy](https://privacy.sexy)
## Why privacy.sexy
## Why
- You don't need to run any compiled software on your system, just run the generated scripts.
- It's open source, both application & infrastructure is 100% transparent
@@ -21,7 +24,7 @@ Web tool to generate scripts for enforcing privacy & security best-practices suc
## Extend scripts
Fork it & add more scripts in `src/application/application.yml` and send a pull request 👌
Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
## Commands
@@ -50,77 +53,22 @@ Fork it & add more scripts in `src/application/application.yml` and send a pull
- **Application Layer**
- Keeps the application state
- The [state](src/application/State/ApplicationState.ts) is a mutable singleton & event producer.
- The application is defined & controlled in a [single YAML file](`\application\application.yaml`) (see [Data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming))
- The application is defined & controlled in a [single YAML file](src/application/application.yaml) (see [Data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming))
![DDD + vue.js](docs/app-ddd.png)
### AWS Infrastructure
- The application runs in AWS 100% serverless and automatically provisioned using [CloudFormation files](/aws) and GitHub Actions.
- Maximum security & automation and minimum AWS costs were the highest priorities of the design.
[![AWS solution](docs/aws-solution.png)](https://github.com/undergroundwires/aws-static-site-with-cd)
![AWS solution](docs/aws-solution.png)
- It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd)
- Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/).
- Maximum security & automation and minimum AWS costs are the highest priorities of the design.
#### GitOps: CI/CD to AWS
- CI/CD is fully automated for this repo using different GIT events & GitHub actions.
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
- Everything that's merged in the master goes directly to production.
- Deploy infrastructure ► Deploy web application ► Invalidate CloudFront Cache
- See more at [build-and-deploy.yaml](.GitHub/workflows/build-and-deploy.yaml)
![CI/CD to AWS with GitHub Actions](docs/ci-cd.png)
##### CloudFormation
![CloudFormation design](docs/aws-cloudformation.png)
- AWS infrastructure is defined as code with following files:
- `iam-stack`: Creates & updates the deployment user.
- Everything in IAM layer is fine-grained using least privileges principle.
- Each deployment step has its own temporary credentials with own permissions.
- `certificate-stack.yaml`
- It'll generate SSL certification for the root domain and www subdomain.
- ❗ It [must](https://aws.amazon.com/premiumsupport/knowledge-center/cloudfront-invalid-viewer-certificate/) be deployed in `us-east-1` to be able to be used by CloudFront by `web-stack`.
- It uses CustomResource and a lambda instead of native `AWS::CertificateManager::Certificate` because:
- Problem:
- AWS variant waits until a certificate is validated.
- There's no way to automate validation without workaround.
- Solution:
- Deploy a lambda that deploys the certificate (so we don't wait until certificate is validated)
- Get DNS records to be used in validation & export it to be used later.
- `web-stack.yaml`: It'll deploy S3 bucket and CloudFront in front of it.
- `dns-stack.yaml`: It'll deploy Route53 hosted zone
- Each time Route53 hosted zone is re-created it's required to update the DNS records in the domain registrar. See *Configure your domain registrar*.
- I use cross stacks instead of single stack or nested stacks because:
- Easier to test & maintain & smaller files and different lifecycles for different areas.
- It allows to deploy web bucket in different region than others as other stacks are global (`us-east-1`) resources.
##### Initial deployment
- ❗ Prerequisite: A registered domain name for website.
1. **Configure build agent (GitHub actions)**
- Deploy manually `iam-stack.yaml` with stack name `privacysexy-iam-stack` (to follow the convention)
- It'll give you deploy user. Go to console & generate secret id + key (Security credentials => Create access key) for the user [IAM users](https://console.aws.amazon.com/iam/home#/users).
- 🚶 Deploy secrets:
- Add secret id & key in GitHub Secrets.
- `AWS_DEPLOYMENT_USER_ACCESS_KEY_ID`, `AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY`
- Add more secrets given from Outputs section of the CloudFormation stack.
- Run GitHub actions to deploy rest of the application.
- It'll run `certificate-stack.yaml` and then `iam-stack.yaml`.
2. **Configure your domain registrar**
-**Web stack will fail** after DNS stack because you need to validate your domain.
- 🚶 Go to your domain registrar and change name servers to NS values
- `dns-stack.yaml` outputs those in CloudFormation stack.
- You can alternatively find those in [Route53](https://console.aws.amazon.com/route53/home#hosted-zones)
- When nameservers of your domain updated, the certification will get validated automatically, you can then delete the failed stack in CloudFormation & re-run the GitHub actions.
## Thank you for the awesome projects 🍺
- [Vue.js](https://vuejs.org/) the only big JavaScript framework that's not backed by companies that make money off your data.
- [liquor-tree](https://GitHub.com/amsik/liquor-tree) for the awesome & super extensible tree component.
- [Ace](https://ace.c9.io/) for code box.
- [FileSaver.js](https://GitHub.com/eligrey/FileSaver.js) for save file dialog.
- [chai](https://GitHub.com/chaijs/chai) & [mocha](https://GitHub.com/mochajs/mocha) for making testing fun.
- [js-yaml-loader](https://GitHub.com/wwilsman/js-yaml-loader) for ahead of time loading `application.yml`
- [v-tooltip](https://GitHub.com/Akryum/v-tooltip) takes seconds to have a tooltip, exactly what I needed.
[![CI/CD to AWS with GitHub Actions](docs/gitops.png)](.github/workflows/)

View File

@@ -1 +0,0 @@
<mxfile host="www.draw.io" modified="2019-12-27T14:40:11.720Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" etag="6t_Q0ZRAKXZ_lLm1WdcF" version="12.4.3" type="device" pages="1"><diagram id="pFg2tUHn5hOZkmQyf_J4" name="Page-1">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</diagram></mxfile>

View File

@@ -1,211 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: Creates certificate for the root + www subdomain. !! It must be deployed in us-east-1 to be able to be used by CloudFront.
Parameters:
RootDomainName:
Type: String
Default: privacy.sexy
Description: The root DNS name of the website e.g. privacy.sexy
AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?<!-)
ConstraintDescription: Must be a valid root domain name
IamStackName:
Type: String
Default: privacysexy-iam-stack
Description: Name of the IAM stack.
Resources:
# The lambda workaround exists to be able to automate certificate deployment.
# Problem:
# Normally AWS AWS::CertificateManager::Certificate waits until a certificate is validated
# And there's no way to get validation DNS records from it to validate it.
# Solution:
# Deploy a lambda that deploys the certificate (so we don't wait until certificate is validated)
# Get DNS records to be used in validation & export it to be used later.
AcmCertificateForHostedZone:
Type: Custom::VerifiableCertificate #A Can use AWS::CloudFormation::CustomResource or Custom::String
Properties:
ServiceToken: !GetAtt ResolveCertificateLambda.Arn
# Lambda gets the following data:
RootDomainName: !Ref RootDomainName # Lambda will create both for root and www.root
Tags:
-
Key: Name
Value: !Ref RootDomainName
-
Key: Application
Value: privacy.sexy
ResolveCertificateLambda:
Type: AWS::Lambda::Function
Properties:
Description: Deploys certificate for root domain name + www and returns immediately arn + verification records.
Role:
Fn::ImportValue: !Join [':', [!Ref IamStackName, ResolveCertificateLambdaRoleArn]]
FunctionName: !Sub ${AWS::StackName}-cert-resolver-lambda # StackName- required for role to function
Handler: index.handler
Runtime: nodejs12.x
Timeout: 30
Tags:
-
Key: Application
Value: privacy.sexy
Code:
# Inline script is not the best way. Some variables are named shortly to not exceed the limit 4096 but it's the cheapest way (no s3 file)
ZipFile: >
'use strict';
const aws = require('aws-sdk');
const acm = new aws.ACM();
const log = (t) => console.log(t);
exports.handler = async (event, context) => {
log(`Request recieved:\n${JSON.stringify(event)}`);
const userData = event.ResourceProperties;
const rootDomain = userData.RootDomainName;
let data = null;
try {
switch(event.RequestType) {
case 'Create':
data = await handleCreateAsync(rootDomain, userData.Tags);
break;
case 'Update':
data = await handleUpdateAsync();
break;
case 'Delete':
data = await handleDeleteAsync(rootDomain);
break;
}
await sendResponseAsync(event, context, 'SUCCESS', data);
} catch(error) {
await sendResponseAsync(event, context, 'ERROR', {
title: `Failed to ${event.RequestType}, see error`,
error: error
});
}
}
async function handleCreateAsync(rootDomain, tags) {
const { CertificateArn } = await acm.requestCertificate({
DomainName: rootDomain,
SubjectAlternativeNames: [`www.${rootDomain}`],
Tags: tags,
ValidationMethod: 'DNS',
}).promise();
log(`Cert requested:${CertificateArn}`);
const waitAsync = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const maxAttempts = 10;
let options = undefined;
for (let attempt = 0; attempt < maxAttempts && !options; attempt++) {
await waitAsync(2000);
const { Certificate } = await acm.describeCertificate({ CertificateArn }).promise();
if(Certificate.DomainValidationOptions.filter((o) => o.ResourceRecord).length === 2) {
options = Certificate.DomainValidationOptions;
}
}
if(!options) {
throw new Error(`No records after ${maxAttempts} attempts.`);
}
return getResponseData(options, CertificateArn, rootDomain);
}
async function handleDeleteAsync(rootDomain) {
const certs = await acm.listCertificates({}).promise();
const cert = certs.CertificateSummaryList.find((cert) => cert.DomainName === rootDomain);
if (cert) {
await acm.deleteCertificate({ CertificateArn: cert.CertificateArn }).promise();
log(`Deleted ${cert.CertificateArn}`);
} else {
log('Cannot find'); // Do not fail, delete can be called when e.g. CF fails before creating cert
}
return null;
}
async function handleUpdateAsync() {
throw new Error(`Not yet implemented update`);
}
function getResponseData(options, arn, rootDomain) {
const findRecord = (url) => options.find(option => option.DomainName === url).ResourceRecord;
const root = findRecord(rootDomain);
const www = findRecord(`www.${rootDomain}`);
const data = {
CertificateArn: arn,
RootVerificationRecordName: root.Name,
RootVerificationRecordValue: root.Value,
WwwVerificationRecordName: www.Name,
WwwVerificationRecordValue: www.Value,
};
return data;
}
/* cfn-response can't async / await :( */
async function sendResponseAsync(event, context, responseStatus, responseData, physicalResourceId) {
return new Promise((s, f) => {
var b = JSON.stringify({
Status: responseStatus,
Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`,
PhysicalResourceId: physicalResourceId || context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData
});
log(`Response body:\n${b}`);
var u = require("url").parse(event.ResponseURL);
var r = require("https").request(
{
hostname: u.hostname,
port: 443,
path: u.path,
method: "PUT",
headers: {
"content-type": "",
"content-length": b.length
}
}, (p) => {
log(`Status code: ${p.statusCode}`);
log(`Status message: ${p.statusMessage}`);
s(context.done());
});
r.on("error", (e) => {
log(`request failed: ${e}`);
f(context.done(e));
});
r.write(b);
r.end();
});
}
Outputs:
CertificateArn:
Description: The Amazon Resource Name (ARN) of an AWS Certificate Manager (ACM) certificate.
Value: !GetAtt AcmCertificateForHostedZone.CertificateArn
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', CertificateArn ]]
RootVerificationRecordName:
Description: Name for root domain CNAME verification record
Value: !GetAtt AcmCertificateForHostedZone.RootVerificationRecordName
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', RootVerificationRecordName ]]
RootVerificationRecordValue:
Description: Value for root domain name CNAME verification record
Value: !GetAtt AcmCertificateForHostedZone.RootVerificationRecordValue
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', RootVerificationRecordValue ]]
WwwVerificationRecordName:
Description: Name for www domain name CNAME verification record
Value: !GetAtt AcmCertificateForHostedZone.WwwVerificationRecordName
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', WwwVerificationRecordName ]]
WwwVerificationRecordValue:
Description: Value for www domain name CNAME verification record
Value: !GetAtt AcmCertificateForHostedZone.WwwVerificationRecordValue
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', WwwVerificationRecordValue ]]

View File

@@ -1,61 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: Creates hosted zone & sets up records for the CloudFront URL.
Parameters:
RootDomainName:
Type: String
Default: privacy.sexy
Description: The root DNS name of the website e.g. privacy.sexy
AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?<!-)
ConstraintDescription: Must be a valid root domain name
CertificateStackName:
Type: String
Default: privacysexy-certificate-stack
Description: Name of the certificate stack.
Resources:
DNSHostedZone:
Type: AWS::Route53::HostedZone
Properties:
Name: !Ref RootDomainName
HostedZoneConfig:
Comment: !Join ['', ['Hosted zone for ', !Ref RootDomainName]]
HostedZoneTags:
-
Key: Application
Value: privacy.sexy
CertificateValidationDNSRecords:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: !Ref DNSHostedZone
RecordSets:
-
Name:
Fn::ImportValue: !Join [':', [!Ref CertificateStackName, RootVerificationRecordName]]
Type: 'CNAME'
TTL: '60'
ResourceRecords:
- Fn::ImportValue: !Join [':', [!Ref CertificateStackName, RootVerificationRecordValue]]
-
Name:
Fn::ImportValue: !Join [':', [!Ref CertificateStackName, WwwVerificationRecordName]]
Type: 'CNAME'
TTL: '60'
ResourceRecords:
- Fn::ImportValue: !Join [':', [!Ref CertificateStackName, WwwVerificationRecordValue]]
Outputs:
DNSHostedZoneNameServers:
Description: Name servers to update in domain registrar.
Value: !Join [' ', !GetAtt DNSHostedZone.NameServers]
DNSHostedZoneId:
Description: The ID of the hosted zone that you want to create the record in.
Value: !Ref DNSHostedZone
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', DNSHostedZoneId ]]

View File

@@ -1,496 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: |-
> Deploys the identity management for the deployment
# Granulatiy cheatsheet: https://iam.cloudonaut.io/
Parameters:
WebStackName:
Type: String
Default: privacysexy-web-stack
Description: Name of the web stack.
DnsStackName:
Type: String
Default: privacysexy-dns-stack
Description: Name of the DNS stack.
CertificateStackName:
Type: String
Default: privacysexy-certificate-stack
Description: Name of the IAM stack.
Resources:
# -----------------------------
# ------ User & Group ---------
# -----------------------------
DeploymentGroup:
Type: AWS::IAM::Group
Properties:
# GroupName: No hardcoded naming because of easier CloudFormation management
ManagedPolicyArns:
- !Ref AllowValidateTemplatePolicy
DeploymentUser:
Type: AWS::IAM::User
Properties:
# # UserName: No hardcoded naming because of easier CloudFormation management
# # Policies: Assing policies on group level
Tags:
-
Key: Application
Value: privacy.sexy
AddDeploymentUserToDeploymentGroup:
Type: AWS::IAM::UserToGroupAddition
Properties:
GroupName: !Ref DeploymentGroup
Users:
- !Ref DeploymentUser
# -----------------------------
# ----------- Roles -----------
# -----------------------------
IamStackDeployRole:
Type: AWS::IAM::Role
Properties:
Description: Allows to deploy IAM stack
AssumeRolePolicyDocument:
Statement:
-
Effect: Allow
Principal:
AWS: !GetAtt DeploymentUser.Arn
Action: sts:AssumeRole
Tags:
-
Key: Application
Value: privacy.sexy
ManagedPolicyArns:
- !Ref CloudFormationDeployPolicy
- !Ref PolicyDeployPolicy
- !Ref IamStackDeployPolicy
CertificateStackDeployRole:
Type: AWS::IAM::Role
Properties:
Description: Allows to deploy certificate stack
AssumeRolePolicyDocument:
Statement:
-
Effect: Allow
Principal:
AWS: !GetAtt DeploymentUser.Arn
Action: sts:AssumeRole
Tags:
-
Key: Application
Value: privacy.sexy
ManagedPolicyArns:
- !Ref CloudFormationDeployPolicy
- !Ref LambdaBackedCustomResourceDeployPolicy
DnsStackDeployRole:
Type: AWS::IAM::Role
Properties:
Description: Allows to deploy DNS stack
AssumeRolePolicyDocument:
Statement:
-
Effect: Allow
Principal:
AWS: !GetAtt DeploymentUser.Arn
Action: sts:AssumeRole
Tags:
-
Key: Application
Value: privacy.sexy
ManagedPolicyArns:
- !Ref CloudFormationDeployPolicy
- !Ref DnsStackDeployPolicy
WebStackDeployRole:
Type: AWS::IAM::Role
Properties:
Description: Allows to deploy web stack
AssumeRolePolicyDocument:
Statement:
-
Effect: Allow
Principal:
AWS: !GetAtt DeploymentUser.Arn
Action: sts:AssumeRole
Tags:
-
Key: Application
Value: privacy.sexy
ManagedPolicyArns:
- !Ref CloudFormationDeployPolicy
- !Ref WebStackDeployPolicy
S3SiteDeployRole:
Type: 'AWS::IAM::Role'
Properties:
Description: "Allows to deploy website to S3"
AssumeRolePolicyDocument:
Statement:
-
Effect: Allow
Principal:
AWS: !GetAtt DeploymentUser.Arn
Action: sts:AssumeRole
Tags:
-
Key: Application
Value: privacy.sexy
ManagedPolicyArns:
- !Ref S3SiteDeployPolicy
- !Ref StackExportReaderPolicy
CloudFrontSiteDeployRole:
Type: 'AWS::IAM::Role'
Properties:
Description: "Allows to informs to CloudFront to renew its cache from S3"
AssumeRolePolicyDocument:
Statement:
-
Effect: Allow
Principal:
AWS: !GetAtt DeploymentUser.Arn
Action: sts:AssumeRole
Tags:
-
Key: Application
Value: privacy.sexy
ManagedPolicyArns:
- !Ref CloudFrontInvalidationPolicy
- !Ref StackExportReaderPolicy
ResolveCertificateLambdaRole: # See certificate stack
Type: AWS::IAM::Role
Properties:
Description: Allow deployment of certificates
AssumeRolePolicyDocument:
Statement:
-
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- !Ref CertificateDeployPolicy
# --------------------------------
# ----------- Policies -----------
# --------------------------------
AllowValidateTemplatePolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: "No read & writes to resources, reveals just basic CloudFormation API to be used for validating templates"
# ManagedPolicyName: No hardcoded naming because of easier CloudFormation management
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowCloudFormationTemplateValidation
Effect: Allow
Action:
- cloudformation:ValidateTemplate
Resource: '*'
CloudFormationDeployPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: "Allows deploying CloudFormation using CLI command 'aws cloudformation deploy' (with change sets)"
# ManagedPolicyName: No hardcoded naming because of easier CloudFormation management
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowCloudFormationStackOperations
Effect: Allow
Action:
- cloudformation:GetTemplateSummary
- cloudformation:DescribeStacks
- cloudformation:CreateChangeSet
- cloudformation:ExecuteChangeSet
- cloudformation:DescribeChangeSet
Resource:
- !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/${WebStackName}/*
- !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/${DnsStackName}/*
- !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/${AWS::StackName}/*
- !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/${CertificateStackName}/*
IamStackDeployPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows deploying IAM CloudFormation stack.
# ManagedPolicyName: No hardcoded naming because of easier CloudFormation management
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowUserArnExport
Effect: Allow
Action:
- iam:GetUser
Resource:
- !GetAtt DeploymentUser.Arn
-
Sid: AllowTagging
Effect: Allow
Action:
- iam:TagResource
Resource:
- !Sub arn:aws:cloudformation::${AWS::AccountId}:stack/${AWS::StackName}/*
- !GetAtt DeploymentUser.Arn
-
Sid: AllowRoleDeployment
Effect: Allow
Action:
- iam:CreateRole
Resource:
- !Sub arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}-*
LambdaBackedCustomResourceDeployPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows deploying a lambda-backed custom resource.
# ManagedPolicyName: # ManagedPolicyName: No hardcoded naming because of easier CloudFormation management
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowLambdaDeployment
Effect: Allow
Action:
- lambda:GetFunction
- lambda:DeleteFunction
- lambda:CreateFunction
- lambda:GetFunctionConfiguration
- lambda:InvokeFunction
Resource:
- !Sub arn:aws:lambda:*:${AWS::AccountId}:function:${CertificateStackName}*
-
Sid: AllowPassingLambdaRole
Effect: Allow
Action:
- iam:PassRole
Resource:
- !GetAtt ResolveCertificateLambdaRole.Arn
CertificateDeployPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows deploying certifications stack.
# ManagedPolicyName: # ManagedPolicyName: No hardcoded naming because of easier CloudFormation management
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowCertificateDeployment
Effect: Allow
Action:
- acm:RequestCertificate
- acm:DescribeCertificate
- acm:DeleteCertificate
- acm:AddTagsToCertificate
- acm:ListCertificates
Resource: '*' # Certificate Manager does not support resource level IAM
PolicyDeployPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows deployment of policies
# ManagedPolicyName: Commented out because CloudFormation requires to rename when replacing custom-named resource
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowPolicyUpdates
Effect: Allow
Action:
- iam:ListPolicyVersions
- iam:CreatePolicyVersion
- iam:DeletePolicyVersion
- iam:CreatePolicy
- iam:DeletePolicy
- iam:GetPolicy
Resource:
- !Sub arn:aws:iam::${AWS::AccountId}:policy/${AWS::StackName}-* # when ManagedPolicyName is not given policies get name like StackName-*
-
Sid: AllowPoliciesOnRoles
Effect: Allow
Action:
- iam:AttachRolePolicy
- iam:DetachRolePolicy
- iam:GetRole
Resource:
- !Sub arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}-*
-
Sid: AllowPolicyAssigmentToGroup
Effect: Allow
Action:
- iam:AttachGroupPolicy
- iam:DetachGroupPolicy
Resource:
- !GetAtt DeploymentGroup.Arn
-
Sid: AllowGettingGroupInformation
Effect: Allow
Action:
- iam:GetGroup
Resource: !Sub arn:aws:iam::${AWS::AccountId}:group/${DeploymentGroup}
DnsStackDeployPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows deployment of DNS stack
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowHostedZoneDeployment
Effect: Allow
Action:
- route53:CreateHostedZone
- route53:ListQueryLoggingConfigs
- route53:DeleteHostedZone
- route53:GetChange
- route53:ChangeTagsForResource
- route53:GetHostedZone
- route53:ChangeResourceRecordSets
Resource: '*' # Does not support resource-level permissions https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/access-control-overview.html#access-control-manage-access-intro-resource-policies
WebStackDeployPolicy:
# We need a role to run s3:PutBucketPolicy, IAM users cannot run it. See https://stackoverflow.com/a/48551383
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows deployment of web stack
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowCloudFrontOAIDeployment
Effect: Allow
Action:
- cloudfront:GetCloudFrontOriginAccessIdentity
- cloudfront:CreateCloudFrontOriginAccessIdentity
- cloudfront:GetCloudFrontOriginAccessIdentityConfig
- cloudfront:DeleteCloudFrontOriginAccessIdentity
Resource: '*' # Does not support resource-level permissions https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cf-api-permissions-ref.html
-
Sid: AllowCloudFrontDistributionDeployment
Effect: Allow
Action:
- cloudfront:CreateDistribution
- cloudfront:DeleteDistribution
- cloudfront:UpdateDistribution
- cloudfront:GetDistribution
- cloudfront:TagResource
- cloudfront:UpdateCloudFrontOriginAccessIdentity
Resource: !Sub arn:aws:cloudfront::${AWS::AccountId}:*
-
Sid: AllowS3BucketPolicyAccess
Effect: Allow
Action:
- s3:CreateBucket
- s3:DeleteBucket
- s3:PutBucketWebsite
- s3:DeleteBucketPolicy
- s3:PutBucketPolicy
- s3:GetBucketPolicy
Resource: !Sub arn:aws:s3:::${WebStackName}*
-
Sid: AllowRecordDeploymentToRoute53
Effect: Allow
Action:
- route53:GetHostedZone
- route53:ChangeResourceRecordSets
- route53:GetChange
- route53:ListResourceRecordSets
Resource: '*' # Does not support resource-level permissions https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/access-control-overview.html#access-control-manage-access-intro-resource-policies
S3SiteDeployPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows listing buckets to be able to list objects in a bucket
# ManagedPolicyName: Commented out because CloudFormation requires to rename when replacing custom-named resources
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowListingObjects
Effect: Allow
Action:
- s3:ListBucket # To allow ListObjectsV2
Resource: !Sub arn:aws:s3:::${WebStackName}*
-
Sid: AllowUpdatingObjects
Effect: Allow
Action:
- s3:PutObject
- s3:DeleteObject
Resource: !Sub arn:aws:s3:::${WebStackName}*/*
CloudFrontInvalidationPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows creating invalidations on CloudFront
# ManagedPolicyName: Commented out because CloudFormation requires to rename when replacing custom-named resource
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowCloudFrontInvalidations
Effect: Allow
Action:
- cloudfront:CreateInvalidation
Resource: "*" # Does not support resource-level permissions https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cf-api-permissions-ref.html
StackExportReaderPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows creating invalidations on CloudFront
# ManagedPolicyName: Commented out because CloudFormation requires to rename when replacing custom-named resource
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: AllowGettingBucketName
Effect: Allow
Action:
- cloudformation:DescribeStacks
Resource: !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/${WebStackName}/*
Outputs:
ResolveCertificateLambdaRoleArn:
Description: The Amazon Resource Name (ARN) of the lambda for deploying certificates.
Value: !GetAtt ResolveCertificateLambdaRole.Arn
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', ResolveCertificateLambdaRoleArn ] ]
CertificateStackDeployRoleArn:
Description: "GitHub secret: AWS_CERTIFICATE_STACK_DEPLOYMENT_ROLE_ARN"
Value: !GetAtt CertificateStackDeployRole.Arn
DnsStackDeployRoleArn:
Description: "GitHub secret: AWS_DNS_STACK_DEPLOYMENT_ROLE_ARN"
Value: !GetAtt DnsStackDeployRole.Arn
IamStackDeployRoleArn:
Description: "GitHub secret: AWS_IAM_STACK_DEPLOYMENT_ROLE_ARN"
Value: !GetAtt IamStackDeployRole.Arn
WebStackDeployRoleArn:
Description: "GitHub secret: AWS_WEB_STACK_DEPLOYMENT_ROLE_ARN"
Value: !GetAtt WebStackDeployRole.Arn
S3SiteDeployRoleArn:
Description: "GitHub secret: AWS_S3_SITE_DEPLOYMENT_ROLE_ARN"
Value: !GetAtt S3SiteDeployRole.Arn
CloudFrontSiteDeployRoleArn:
Description: "GitHub secret: AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN"
Value: !GetAtt CloudFrontSiteDeployRole.Arn

View File

@@ -1,36 +0,0 @@
#!/bin/bash
# Parse parameters
while [[ "$#" -gt 0 ]]; do case $1 in
--user-profile) USER_PROFILE="$2"; shift;;
--role-profile) ROLE_PROFILE="$2"; shift;;
--role-arn) ROLE_ARN="$2"; shift;;
--session) SESSION="$2";shift;;
--region) REGION="$2";shift;;
*) echo "Unknown parameter passed: $1"; exit 1;;
esac; shift; done
# Verify parameters
if [ -z "$USER_PROFILE" ]; then echo "User profile name is not set."; exit 1; fi;
if [ -z "$ROLE_PROFILE" ]; then echo "Role profile name is not set."; exit 1; fi;
if [ -z "$ROLE_ARN" ]; then echo "Role ARN is not set"; exit 1; fi;
if [ -z "$SESSION" ]; then echo "Session name is not set."; exit 1; fi;
if [ -z "$REGION" ]; then echo "Region is not set."; exit 1; fi;
creds=$(aws sts assume-role --role-arn $ROLE_ARN --role-session-name $SESSION --profile $USER_PROFILE)
aws_access_key_id=$(echo $creds | jq -r '.Credentials.AccessKeyId')
echo ::add-mask::$aws_access_key_id
aws_secret_access_key=$(echo $creds | jq -r '.Credentials.SecretAccessKey')
echo ::add-mask::$aws_secret_access_key
aws_session_token=$(echo $creds | jq -r '.Credentials.SessionToken')
echo ::add-mask::$aws_session_token
aws configure --profile $ROLE_PROFILE set aws_access_key_id $aws_access_key_id
aws configure --profile $ROLE_PROFILE set aws_secret_access_key $aws_secret_access_key
aws configure --profile $ROLE_PROFILE set aws_session_token $aws_session_token
aws configure --profile $ROLE_PROFILE set region $REGION
echo Profile $ROLE_PROFILE is created
bash "${BASH_SOURCE%/*}/mask-identity.sh" --profile $ROLE_PROFILE

View File

@@ -1,25 +0,0 @@
#!/bin/bash
# Parse parameters
while [[ "$#" -gt 0 ]]; do case $1 in
--profile) PROFILE="$2"; shift;;
--access-key-id) ACCESS_KEY_ID="$2"; shift;;
--secret-access-key) SECRET_ACCESS_KEY="$2"; shift;;
--region) REGION="$2";shift;;
*) echo "Unknown parameter passed: $1"; exit 1;;
esac; shift; done
# Verify parameters
if [ -z "$PROFILE" ]; then echo "Profile name is not set."; exit 1; fi;
echo $PROFILE
if [ -z "$ACCESS_KEY_ID" ]; then echo "Access key ID is not set"; exit 1; fi;
if [ -z "$SECRET_ACCESS_KEY" ]; then echo "Secret access key is not set."; exit 1; fi;
if [ -z "$REGION" ]; then echo "Region is not set."; exit 1; fi;
aws configure --profile $PROFILE set aws_access_key_id $ACCESS_KEY_ID
aws configure --profile $PROFILE set aws_secret_access_key $SECRET_ACCESS_KEY
aws configure --profile $PROFILE set region $REGION
echo Profile $PROFILE is created
bash "${BASH_SOURCE%/*}/mask-identity.sh" --profile $PROFILE

View File

@@ -1,17 +0,0 @@
#!/bin/bash
# Parse parameters
while [[ "$#" -gt 0 ]]; do case $1 in
--profile) PROFILE="$2";shift;;
*) echo "Unknown parameter passed: $1"; exit 1;;
esac; shift; done
# Verify parameters
if [ -z "$PROFILE" ]; then echo "Profile name is not set."; exit 1; fi;
aws_identity=$(aws sts get-caller-identity --profile $PROFILE)
echo ::add-mask::$(echo $aws_identity | jq -r '.Account')
echo ::add-mask::$(echo $aws_identity | jq -r '.UserId')
echo ::add-mask::$(echo $aws_identity | jq -r '.Arn')
echo Credentials are masked

View File

@@ -1,43 +0,0 @@
#!/bin/bash
# Parse parameters
while [[ "$#" -gt 0 ]]; do case $1 in
--template-file) TEMPLATE_FILE="$2"; shift;;
--stack-name) STACK_NAME="$2"; shift;;
--profile) PROFILE="$2"; shift;;
--capabilities) CAPABILITY_IAM="$2"; shift;;
--role-arn) ROLE_ARN="$2";shift;;
--session) SESSION="$2";shift;;
--region) REGION="$2";shift;;
*) echo "Unknown parameter passed: $1"; exit 1;;
esac; shift; done
# Verify parameters
if [ -z "$TEMPLATE_FILE" ]; then echo "Template file is not set."; exit 1; fi;
if [ -z "$STACK_NAME" ]; then echo "Template file is not set."; exit 1; fi;
if [ -z "$PROFILE" ]; then echo "Profile is not set."; exit 1; fi;
if [ -z "$ROLE_ARN" ]; then echo "Role ARN is not set."; exit 1; fi;
if [ -z "$SESSION" ]; then echo "Role session is not set."; exit 1; fi;
echo Validating stack "$STACK_NAME"
aws cloudformation validate-template \
--template-body file://$TEMPLATE_FILE \
--profile $PROFILE
ROLE_PROFILE=$STACK_NAME
echo Assuming role
bash "${BASH_SOURCE%/*}/../configure/create-role-profile.sh" \
--role-profile $ROLE_PROFILE --user-profile $PROFILE \
--role-arn $ROLE_ARN \
--session $SESSION \
--region $REGION
echo Deploying stack "$TEMPLATE_FILE"
aws cloudformation deploy \
--template-file $TEMPLATE_FILE \
--stack-name $STACK_NAME \
${CAPABILITY_IAM:+ --capabilities $CAPABILITY_IAM} \
--no-fail-on-empty-changeset \
--profile $ROLE_PROFILE

View File

@@ -1,47 +0,0 @@
#!/bin/bash
# Parse parameters
while [[ "$#" -gt 0 ]]; do case $1 in
--folder) FOLDER="$2"; shift;;
--web-stack-name) WEB_STACK_NAME="$2"; shift;;
--web-stack-s3-name-output-name) WEB_STACK_S3_NAME_OUTPUT_NAME="$2"; shift;;
--storage-class) STORAGE_CLASS="$2"; shift;;
--profile) PROFILE="$2"; shift;;
--role-arn) ROLE_ARN="$2";shift;;
--session) SESSION="$2";shift;;
--region) REGION="$2";shift;;
*) echo "Unknown parameter passed: $1"; exit 1;;
esac; shift; done
# Verify parameters
if [ -z "$FOLDER" ]; then echo "Folder is not set."; exit 1; fi;
if [ -z "$PROFILE" ]; then echo "Profile is not set."; exit 1; fi;
if [ -z "$ROLE_ARN" ]; then echo "Role ARN is not set."; exit 1; fi;
if [ -z "$SESSION" ]; then echo "Role session is not set."; exit 1; fi;
if [ -z "$WEB_STACK_NAME" ]; then echo "Web stack name is not set."; exit 1; fi;
if [ -z "$WEB_STACK_S3_NAME_OUTPUT_NAME" ]; then echo "S3 name output name is not set."; exit 1; fi;
if [ -z "$STORAGE_CLASS" ]; then echo "S3 object storage class is not set."; exit 1; fi;
echo Assuming role
ROLE_PROFILE=deploy-s3
bash "${BASH_SOURCE%/*}/../configure/create-role-profile.sh" \
--role-profile $ROLE_PROFILE --user-profile $PROFILE \
--role-arn $ROLE_ARN \
--session $SESSION \
--region $REGION
echo Getting S3 bucket name from stack "$WEB_STACK_NAME" with output "$WEB_STACK_S3_NAME_OUTPUT_NAME"
S3_BUCKET_NAME=$(aws cloudformation describe-stacks \
--stack-name $WEB_STACK_NAME \
--query "Stacks[0].Outputs[?OutputKey=='$WEB_STACK_S3_NAME_OUTPUT_NAME'].OutputValue" \
--output text \
--profile $ROLE_PROFILE)
if [ -z "$S3_BUCKET_NAME" ]; then echo "Could not read S3 bucket name"; exit 1; fi;
echo ::add-mask::$S3_BUCKET_NAME # Just being extra cautious
echo Syncing folder to S3
aws s3 sync $FOLDER s3://$S3_BUCKET_NAME \
--storage-class $STORAGE_CLASS \
--no-progress --follow-symlinks --delete \
--profile $ROLE_PROFILE

View File

@@ -1,45 +0,0 @@
#!/bin/bash
# Parse parameters
while [[ "$#" -gt 0 ]]; do case $1 in
--paths) PATHS="$2"; shift;;
--web-stack-name) WEB_STACK_NAME="$2"; shift;;
--web-stack-cloudfront-arn-output-name) WEB_STACK_CLOUDFRONT_ARN_OUTPUT_NAME="$2"; shift;;
--profile) PROFILE="$2"; shift;;
--role-arn) ROLE_ARN="$2";shift;;
--session) SESSION="$2";shift;;
--region) REGION="$2";shift;;
*) echo "Unknown parameter passed: $1"; exit 1;;
esac; shift; done
# Verify parameters
if [ -z "$PATHS" ]; then echo "Paths is not set."; exit 1; fi;
if [ -z "$PROFILE" ]; then echo "Profile is not set."; exit 1; fi;
if [ -z "$ROLE_ARN" ]; then echo "Role ARN is not set."; exit 1; fi;
if [ -z "$SESSION" ]; then echo "Role session is not set."; exit 1; fi;
if [ -z "$WEB_STACK_NAME" ]; then echo "Web stack name is not set."; exit 1; fi;
if [ -z "$WEB_STACK_CLOUDFRONT_ARN_OUTPUT_NAME" ]; then echo "CloudFront ARN output name is not set."; exit 1; fi;
echo Assuming role
ROLE_PROFILE=invalidate-cloudfront
bash "${BASH_SOURCE%/*}/../configure/create-role-profile.sh" \
--role-profile $ROLE_PROFILE --user-profile $PROFILE \
--role-arn $ROLE_ARN \
--session $SESSION \
--region $REGION
echo Getting CloudFront ARN from stack "$WEB_STACK_NAME" with output "$WEB_STACK_CLOUDFRONT_ARN_OUTPUT_NAME"
CLOUDFRONT_ARN=$(aws cloudformation describe-stacks \
--stack-name $WEB_STACK_NAME \
--query "Stacks[0].Outputs[?OutputKey=='$WEB_STACK_CLOUDFRONT_ARN_OUTPUT_NAME'].OutputValue" \
--output text \
--profile $ROLE_PROFILE)
if [ -z "$CLOUDFRONT_ARN" ]; then echo "Could not read CloudFront ARN"; exit 1; fi;
echo :add-mask::$CLOUDFRONT_ARN
echo Syncing folder to S3
aws cloudfront create-invalidation \
--paths $PATHS \
--distribution-id $CLOUDFRONT_ARN \
--profile $ROLE_PROFILE

View File

@@ -1,138 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: |-
> Creates an S3 bucket configured for hosting a static webpage.
> Creates CloudFront distribution that has access to read the S3 bucket.
Parameters:
RootDomainName:
Type: String
Default: privacy.sexy
Description: The root DNS name of the website e.g. privacy.sexy
AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?<!-)
ConstraintDescription: Must be a valid root domain name
CertificateStackName:
Type: String
Default: privacysexy-certificate-stack
Description: Name of the certificate stack.
DnsStackName:
Type: String
Default: privacysexy-dns-stack
Description: Name of the certificate stack.
PriceClass:
Type: String
Description: The CloudFront distribution price class
Default: 'PriceClass_100'
AllowedValues:
- 'PriceClass_100'
- 'PriceClass_200'
- 'PriceClass_All'
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${AWS::StackName}-${RootDomainName} # Must have stack name for IAM to allow
WebsiteConfiguration:
IndexDocument: index.html
Tags:
-
Key: Application
Value: privacy.sexy
S3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument: # Only used for CloudFront as it's the only way, otherwise use IAM roles in IAM stack.
Statement:
-
Sid: AllowCloudFrontRead
Action: s3:GetObject
Effect: Allow
Principal:
CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
Resource: !Join ['', ['arn:aws:s3:::', !Ref S3Bucket, /*]]
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Sub 'CloudFront OAI for ${S3Bucket}'
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Comment: Cloudfront Distribution pointing to S3 bucket
Origins:
-
DomainName: !GetAtt S3Bucket.DomainName
Id: S3Origin
S3OriginConfig:
OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
Enabled: true
HttpVersion: 'http2'
DefaultRootObject: index.html
Aliases:
- !Ref RootDomainName
- !Sub 'www.${RootDomainName}'
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
Compress: true
TargetOriginId: S3Origin
ForwardedValues:
QueryString: true
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
PriceClass: !Ref PriceClass
ViewerCertificate:
AcmCertificateArn:
# Certificate must be validated before it can be used here
Fn::ImportValue: !Join [':', [!Ref CertificateStackName, CertificateArn]]
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1.1_2016
Tags:
-
Key: Application
Value: privacy.sexy
CloudFrontDNSRecords:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId:
Fn::ImportValue: !Join [':', [!Ref DnsStackName, DNSHostedZoneId]]
RecordSets:
-
Name: !Ref RootDomainName
Type: A
AliasTarget:
DNSName: !GetAtt CloudFrontDistribution.DomainName
EvaluateTargetHealth: false
HostedZoneId: Z2FDTNDATAQYW2 # Static CloudFront distribution zone https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html#cfn-route53-aliastarget-hostedzoneid
-
Name: !Join ['', ['www.', !Ref RootDomainName]]
Type: A
AliasTarget:
DNSName: !GetAtt CloudFrontDistribution.DomainName
EvaluateTargetHealth: false
HostedZoneId: Z2FDTNDATAQYW2 # Static CloudFront distribution zone https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html#cfn-route53-aliastarget-hostedzoneid
Outputs:
CloudFrontDistributionArn: # Used by deployment script to be able to deploy to right S3 bucket
Description: Tthe Amazon Resource Name (ARN) of the CloudFront distribution.
Value: !Ref CloudFrontDistribution
S3BucketName: # Used by deployment script to be able to deploy to right S3 bucket
Description: Name of the S3 bucket.
Value: !Ref S3Bucket

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

View File

@@ -1 +0,0 @@
<mxfile host="www.draw.io" modified="2019-12-30T13:07:22.931Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" etag="vfXOAJJrIONaUEloGBPR" version="12.4.7" type="device"><diagram id="ymF_tBZ9P2_Wfw9L8arg" name="Page-1">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</diagram></mxfile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

1
docs/gitops.drawio Normal file

File diff suppressed because one or more lines are too long

BIN
docs/gitops.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

4074
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,17 @@
{
"name": "privacy.sexy",
"version": "0.1.0",
"version": "0.4.5",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit"
"test:unit": "vue-cli-service test:unit",
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
"lint:vue": "vue-cli-service lint --no-fix",
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
"lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.26",
@@ -21,22 +26,29 @@
"v-tooltip": "^2.0.2",
"vue": "^2.6.11",
"vue-class-component": "^7.1.0",
"vue-js-modal": "^2.0.0-rc.3",
"vue-property-decorator": "^8.3.0"
},
"devDependencies": {
"@types/chai": "^4.2.7",
"@types/mocha": "^5.2.7",
"@types/ace": "0.0.42",
"@types/chai": "^4.2.7",
"@types/file-saver": "^2.0.1",
"@vue/cli-plugin-typescript": "^4.1.1",
"@types/mocha": "^5.2.7",
"@vue/cli-plugin-typescript": "^4.4.0",
"@vue/cli-plugin-unit-mocha": "^4.1.1",
"@vue/cli-service": "^4.1.1",
"@vue/test-utils": "1.0.0-beta.30",
"chai": "^4.2.0",
"js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.23.1",
"remark-cli": "^8.0.0",
"remark-lint-no-dead-urls": "^1.0.2",
"remark-preset-lint-consistent": "^3.0.0",
"remark-validate-links": "^10.0.0",
"sass": "^1.24.0",
"sass-loader": "^8.0.0",
"js-yaml-loader": "^1.2.2",
"typescript": "^3.7.4",
"vue-template-compiler": "^2.6.11"
"vue-template-compiler": "^2.6.11",
"yaml-lint": "^1.2.4"
}
}

View File

@@ -1,12 +1,11 @@
<template>
<div id="app">
<div class="wrapper">
<TheHeader class="row"
github-url="https://github.com/undergroundwires/privacy.sexy" />
<!-- <TheSearchBar> </TheSearchBar> -->
<TheHeader class="row" />
<TheSearchBar class="row" />
<TheScripts class="row"/>
<TheCodeArea class="row" theme="xcode" />
<TheCodeButtons class="row" />
<TheCodeButtons class="row code-buttons" />
<TheFooter />
</div>
</div>
@@ -15,20 +14,20 @@
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
import { ApplicationState, IApplicationState } from '@/application/State/ApplicationState';
import TheHeader from './presentation/TheHeader.vue';
import TheFooter from './presentation/TheFooter.vue';
import TheCodeArea from './presentation/TheCodeArea.vue';
import TheCodeButtons from './presentation/TheCodeButtons.vue';
import TheSearchBar from './presentation/TheSearchBar.vue';
import TheScripts from './presentation/Scripts/TheScripts.vue';
import TheHeader from '@/presentation/TheHeader.vue';
import TheFooter from '@/presentation/TheFooter.vue';
import TheCodeArea from '@/presentation/TheCodeArea.vue';
import TheCodeButtons from '@/presentation/TheCodeButtons.vue';
import TheSearchBar from '@/presentation/TheSearchBar.vue';
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
@Component({
components: {
TheHeader,
TheCodeArea,
TheCodeButtons,
TheSearchBar,
TheScripts,
TheSearchBar,
TheFooter,
},
})
@@ -68,6 +67,10 @@ body {
.row {
margin-bottom: 10px;
}
.code-buttons {
padding-bottom: 10px;
}
}
}

View File

@@ -4,8 +4,6 @@ import applicationFile from 'js-yaml-loader!./../application.yaml';
import { parseCategory } from './CategoryParser';
export function parseApplication(): Application {
const name = applicationFile.name as string;
const version = applicationFile.version as number;
const categories = new Array<Category>();
if (!applicationFile.actions || applicationFile.actions.length <= 0) {
throw new Error('Application does not define any action');
@@ -14,6 +12,10 @@ export function parseApplication(): Application {
const category = parseCategory(action);
categories.push(category);
}
const app = new Application(name, version, categories);
const app = new Application(
applicationFile.name,
applicationFile.repositoryUrl,
process.env.VUE_APP_VERSION,
categories);
return app;
}

View File

@@ -37,7 +37,7 @@ export class ApplicationState implements IApplicationState {
/** Initially selected scripts */
public readonly defaultScripts: Script[]) {
this.selection = new UserSelection(app, defaultScripts);
this.code = new ApplicationCode(this.selection, app.version.toString());
this.code = new ApplicationCode(this.selection, app.version);
this.filter = new UserFilter(app);
}
}

View File

@@ -0,0 +1,18 @@
import { IFilterResult } from './IFilterResult';
import { IScript } from '@/domain/Script';
import { ICategory } from '@/domain/ICategory';
export class FilterResult implements IFilterResult {
constructor(
public readonly scriptMatches: ReadonlyArray<IScript>,
public readonly categoryMatches: ReadonlyArray<ICategory>,
public readonly query: string) {
if (!query) { throw new Error('Query is empty or undefined'); }
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
}
public hasAnyMatches(): boolean {
return this.scriptMatches.length > 0
|| this.categoryMatches.length > 0;
}
}

View File

@@ -1,7 +1,8 @@
import { IScript, ICategory } from '@/domain/ICategory';
export interface IFilterMatches {
readonly scriptMatches: ReadonlyArray<IScript>;
export interface IFilterResult {
readonly categoryMatches: ReadonlyArray<ICategory>;
readonly scriptMatches: ReadonlyArray<IScript>;
readonly query: string;
hasAnyMatches(): boolean;
}

View File

@@ -1,8 +1,8 @@
import { IFilterMatches } from './IFilterMatches';
import { IFilterResult } from './IFilterResult';
import { ISignal } from '@/infrastructure/Events/Signal';
export interface IUserFilter {
readonly filtered: ISignal<IFilterMatches>;
readonly filtered: ISignal<IFilterResult>;
readonly filterRemoved: ISignal<void>;
setFilter(filter: string): void;
removeFilter(): void;

View File

@@ -1,10 +1,11 @@
import { IFilterMatches } from './IFilterMatches';
import { FilterResult } from './FilterResult';
import { IFilterResult } from './IFilterResult';
import { Application } from '../../../domain/Application';
import { IUserFilter } from './IUserFilter';
import { Signal } from '@/infrastructure/Events/Signal';
export class UserFilter implements IUserFilter {
public readonly filtered = new Signal<IFilterMatches>();
public readonly filtered = new Signal<IFilterResult>();
public readonly filterRemoved = new Signal<void>();
constructor(private application: Application) {
@@ -15,15 +16,19 @@ export class UserFilter implements IUserFilter {
if (!filter) {
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
}
const filterLowercase = filter.toLocaleLowerCase();
const filteredScripts = this.application.getAllScripts().filter(
(script) => script.name.toLowerCase().includes(filter.toLowerCase()) ||
script.code.toLowerCase().includes(filter.toLowerCase()));
(script) =>
script.name.toLowerCase().includes(filterLowercase) ||
script.code.toLowerCase().includes(filterLowercase));
const filteredCategories = this.application.getAllCategories().filter(
(script) => script.name.toLowerCase().includes(filterLowercase));
const matches: IFilterMatches = {
scriptMatches: filteredScripts,
categoryMatches: null,
query: filter,
};
const matches = new FilterResult(
filteredScripts,
filteredCategories,
filter,
);
this.filtered.notify(matches);
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ declare module 'js-yaml-loader!*' {
interface ApplicationYaml {
name: string;
version: number;
repositoryUrl: string;
actions: ReadonlyArray<YamlCategory>;
}

View File

@@ -11,14 +11,12 @@ export class Application implements IApplication {
constructor(
public readonly name: string,
public readonly version: number,
public readonly repositoryUrl: string,
public readonly version: string,
public readonly categories: ReadonlyArray<ICategory>) {
if (!name) {
throw Error('Application has no name');
}
if (!version) {
throw Error('Version cannot be zero');
}
if (!name) { throw Error('Application has no name'); }
if (!repositoryUrl) { throw Error('Application has no repository url'); }
if (!version) { throw Error('Version cannot be empty'); }
this.flattened = flatten(categories);
if (this.flattened.allCategories.length === 0) {
throw new Error('Application must consist of at least one category');
@@ -48,6 +46,10 @@ export class Application implements IApplication {
public getAllScripts(): IScript[] {
return this.flattened.allScripts;
}
public getAllCategories(): ICategory[] {
return this.flattened.allCategories;
}
}
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {

View File

@@ -3,7 +3,8 @@ import { ICategory } from '@/domain/ICategory';
export interface IApplication {
readonly name: string;
readonly version: number;
readonly repositoryUrl: string;
readonly version: string;
readonly categories: ReadonlyArray<ICategory>;
readonly totalScripts: number;
readonly totalCategories: number;
@@ -12,6 +13,7 @@ export interface IApplication {
findCategory(categoryId: number): ICategory | undefined;
findScript(scriptId: string): IScript | undefined;
getAllScripts(): ReadonlyArray<IScript>;
getAllCategories(): ReadonlyArray<ICategory>;
}
export { IScript } from '@/domain/IScript';

14
src/global.d.ts vendored
View File

@@ -38,6 +38,18 @@ declare module 'liquor-tree' {
data: ICustomLiquorTreeData;
}
// https://amsik.github.io/liquor-tree/#Component-Options
export interface ILiquorTreeOptions {
multiple: boolean;
checkbox: boolean;
checkOnSelect: boolean;
autoCheckChildren: boolean;
parentSelect: boolean;
keyboardNavigation: boolean;
deletion: (node: ILiquorTreeExistingNode) => void;
filter: ILiquorTreeFilter;
}
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
interface ILiquorTreeNodeState {
checked: boolean;
@@ -58,7 +70,7 @@ declare module 'liquor-tree' {
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
interface ILiquorTreeFilter {
emptyText: string;
matcher(query: string, node: ILiquorTreeNewNode): boolean;
matcher(query: string, node: ILiquorTreeExistingNode): boolean;
}
const LiquorTree: PluginObject<any> & VueClass<any>;

View File

@@ -1,3 +1,4 @@
import { VModalBootstrapper } from './Modules/VModalBootstrapper';
import { TreeBootstrapper } from './Modules/TreeBootstrapper';
import { IconBootstrapper } from './Modules/IconBootstrapper';
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
@@ -19,6 +20,7 @@ export class ApplicationBootstrapper implements IVueBootstrapper {
new TreeBootstrapper(),
new VueBootstrapper(),
new TooltipBootstrapper(),
new VModalBootstrapper(),
];
}
}

View File

@@ -0,0 +1,8 @@
import VModal from 'vue-js-modal';
import { VueConstructor, IVueBootstrapper } from './../IVueBootstrapper';
export class VModalBootstrapper implements IVueBootstrapper {
public bootstrap(vue: VueConstructor): void {
vue.use(VModal, { dynamic: true, injectModalsContainer: true });
}
}

View File

@@ -4,6 +4,7 @@
<CardListItem
class="card"
v-for="categoryId of categoryIds"
:data-category="categoryId"
v-bind:key="categoryId"
:categoryId="categoryId"
:activeCategoryId="activeCategoryId"
@@ -32,6 +33,9 @@ export default class CardList extends StatefulVue {
public async mounted() {
const state = await this.getCurrentStateAsync();
this.setCategories(state.app.categories);
this.onOutsideOfActiveCardClicked(() => {
this.activeCategoryId = null;
});
}
public onSelected(categoryId: number, isExpanded: boolean) {
@@ -41,7 +45,21 @@ export default class CardList extends StatefulVue {
private setCategories(categories: ReadonlyArray<ICategory>): void {
this.categoryIds = categories.map((category) => category.id);
}
private onOutsideOfActiveCardClicked(callback) {
const outsideClickListener = (event) => {
if (!this.activeCategoryId) {
return;
}
const element = document.querySelector(`[data-category="${this.activeCategoryId}"]`);
if (!element.contains(event.target)) {
callback();
}
};
document.addEventListener('click', outsideClickListener);
}
}
</script>
<style scoped lang="scss">

View File

@@ -8,11 +8,15 @@
<div class="card__inner">
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span>
<span v-else>Oh no 😢</span>
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="expand-button" />
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" />
</div>
<div class="card__expander" v-on:click.stop>
<font-awesome-icon :icon="['fas', 'times']" class="close-button" v-on:click="onSelected(false)"/>
<div class="card__expander__content">
<ScriptsTree :categoryId="categoryId"></ScriptsTree>
</div>
<div class="card__expander__close-button">
<font-awesome-icon :icon="['fas', 'times']" v-on:click="onSelected(false)"/>
</div>
</div>
</div>
</template>
@@ -52,7 +56,6 @@ export default class CardListItem extends StatefulVue {
this.cardTitle = value ? await this.getCardTitleAsync(value) : undefined;
}
private async getCardTitleAsync(categoryId: number): Promise<string | undefined> {
const state = await this.getCurrentStateAsync();
const category = state.app.findCategory(this.categoryId);
@@ -64,73 +67,66 @@ export default class CardListItem extends StatefulVue {
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
$big-screen-width: 991px;
$medium-screen-width: 767px;
$small-screen-width: 380px;
.card {
margin: 15px;
width: calc((100% / 3) - 30px);
transition: all 0.2s ease-in-out;
//media queries for stacking cards
@media screen and (max-width: 991px) {
width: calc((100% / 2) - 30px);
}
// Media queries for stacking cards
@media screen and (max-width: $big-screen-width) { width: calc((100% / 2) - 30px); }
@media screen and (max-width: $medium-screen-width) { width: 100%; }
@media screen and (max-width: $small-screen-width) { width: 90%; }
@media screen and (max-width: 767px) {
width: 100%;
}
@media screen and (max-width: 380px) {
width: 90%;
}
&:hover {
.card__inner {
background-color: $accent;
transform: scale(1.05);
}
}
&__inner {
width: 100%;
&__inner {
padding: 30px;
position: relative;
cursor: pointer;
background-color: $gray;
color: $light-gray;
font-size: 1.5em;
text-transform: uppercase;
text-align: center;
transition: all 0.2s ease-in-out;
&:hover {
background-color: $accent;
transform: scale(1.05);
}
&:after {
transition: all 0.3s ease-in-out;
}
.expand-button {
&__expand-icon {
width: 100%;
margin-top: .25em;
}
}
//Expander
&__expander {
transition: all 0.2s ease-in-out;
background-color: $slate;
width: 100%;
position: relative;
background-color: $slate;
color: $light-gray;
display: flex;
justify-content: center;
align-items: center;
&__content {
width: 100%;
display: flex;
justify-content: center;
word-break: break-word;
}
.close-button {
font-size: 0.75em;
position: absolute;
top: 10px;
right: 10px;
&__close-button {
width: auto;
font-size: 1.5em;
align-self: flex-start;
margin-right:0.25em;
cursor: pointer;
&:hover {
opacity: 0.9;
}
@@ -138,33 +134,27 @@ export default class CardListItem extends StatefulVue {
}
&.is-collapsed {
.card__inner {
&:after {
content: "";
opacity: 0;
}
}
.card__expander {
max-height: 0;
min-height: 0;
overflow: hidden;
margin-top: 0;
opacity: 0;
}
}
&.is-expanded {
.card__inner {
background-color: $accent;
&:after{
content: "";
opacity: 1;
display: block;
height: 0;
width: 0;
position: absolute;
bottom: -30px;
left: calc(50% - 15px);
@@ -172,14 +162,12 @@ export default class CardListItem extends StatefulVue {
border-right: 15px solid transparent;
border-bottom: 15px solid #333a45;
}
}
.card__expander {
min-height: 200px;
// max-height: 1000px;
// overflow-y: auto;
margin-top: 30px;
opacity: 1;
}
@@ -206,12 +194,7 @@ export default class CardListItem extends StatefulVue {
}
}
//Expander Widths
//when 3 cards in a row
@media screen and (min-width: 992px) {
@media screen and (min-width: $big-screen-width) { // when 3 cards in a row
.card:nth-of-type(3n+2) .card__expander {
margin-left: calc(-100% - 30px);
}
@@ -224,12 +207,9 @@ export default class CardListItem extends StatefulVue {
.card__expander {
width: calc(300% + 60px);
}
}
//when 2 cards in a row
@media screen and (min-width: 768px) and (max-width: 991px) {
@media screen and (min-width: $medium-screen-width) and (max-width: $big-screen-width) { // when 2 cards in a row
.card:nth-of-type(2n+2) .card__expander {
margin-left: calc(-100% - 30px);
}
@@ -239,6 +219,5 @@ export default class CardListItem extends StatefulVue {
.card__expander {
width: calc(200% + 30px);
}
}
</style>

View File

@@ -4,12 +4,12 @@
<span class="part">
<span
class="part"
v-bind:class="{ 'disabled': isGrouped, 'enabled': !isGrouped}"
@click="!isGrouped ? toggleGrouping() : undefined">Cards</span>
v-bind:class="{ 'disabled': cardsSelected, 'enabled': !cardsSelected}"
@click="groupByCard()">Cards</span>
<span class="part">|</span>
<span class="part"
v-bind:class="{ 'disabled': !isGrouped, 'enabled': isGrouped}"
@click="isGrouped ? toggleGrouping() : undefined">None</span>
v-bind:class="{ 'disabled': noneSelected, 'enabled': !noneSelected}"
@click="groupByNone()">None</span>
</span>
</div>
</template>
@@ -20,14 +20,35 @@ import { StatefulVue } from '@/presentation/StatefulVue';
import { IApplicationState } from '@/application/State/IApplicationState';
import { Grouping } from './Grouping';
const DefaultGrouping = Grouping.Cards;
@Component
export default class TheGrouper extends StatefulVue {
public currentGrouping: Grouping;
public isGrouped = true;
public toggleGrouping() {
this.currentGrouping = this.currentGrouping === Grouping.None ? Grouping.Cards : Grouping.None;
this.isGrouped = this.currentGrouping === Grouping.Cards;
public cardsSelected = false;
public noneSelected = false;
private currentGrouping: Grouping;
public mounted() {
this.changeGrouping(DefaultGrouping);
}
public groupByCard() {
this.changeGrouping(Grouping.Cards);
}
public groupByNone() {
this.changeGrouping(Grouping.None);
}
private changeGrouping(newGrouping: Grouping) {
if (this.currentGrouping === newGrouping) {
return;
}
this.currentGrouping = newGrouping;
this.cardsSelected = newGrouping === Grouping.Cards;
this.noneSelected = newGrouping === Grouping.None;
this.$emit('groupingChanged', this.currentGrouping);
}
}

View File

@@ -1,41 +1,46 @@
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
import { IApplication } from './../../../domain/IApplication';
import { ICategory, IScript } from '@/domain/ICategory';
import { INode } from './SelectableTree/INode';
export function parseAllCategories(state: IApplicationState): INode[] | undefined {
export function parseAllCategories(app: IApplication): INode[] | undefined {
const nodes = new Array<INode>();
for (const category of state.app.categories) {
const children = parseCategoryRecursively(category, state.selection);
for (const category of app.categories) {
const children = parseCategoryRecursively(category);
nodes.push(convertCategoryToNode(category, children));
}
return nodes;
}
export function parseSingleCategory(categoryId: number, state: IApplicationState): INode[] | undefined {
const category = state.app.findCategory(categoryId);
export function parseSingleCategory(categoryId: number, app: IApplication): INode[] | undefined {
const category = app.findCategory(categoryId);
if (!category) {
throw new Error(`Category with id ${categoryId} does not exist`);
}
const tree = parseCategoryRecursively(category, state.selection);
const tree = parseCategoryRecursively(category);
return tree;
}
export function getScriptNodeId(script: IScript): string {
return script.id;
}
export function getCategoryNodeId(category: ICategory): string {
return `Category${category.id}`;
}
function parseCategoryRecursively(
parentCategory: ICategory,
selection: IUserSelection): INode[] {
parentCategory: ICategory): INode[] {
if (!parentCategory) { throw new Error('parentCategory is undefined'); }
if (!selection) { throw new Error('selection is undefined'); }
const nodes = new Array<INode>();
if (parentCategory.subCategories && parentCategory.subCategories.length > 0) {
for (const subCategory of parentCategory.subCategories) {
const subCategoryNodes = parseCategoryRecursively(subCategory, selection);
const subCategoryNodes = parseCategoryRecursively(subCategory);
nodes.push(convertCategoryToNode(subCategory, subCategoryNodes));
}
}
if (parentCategory.scripts && parentCategory.scripts.length > 0) {
for (const script of parentCategory.scripts) {
nodes.push(convertScriptToNode(script, selection));
nodes.push(convertScriptToNode(script));
}
}
return nodes;
@@ -44,19 +49,17 @@ function parseCategoryRecursively(
function convertCategoryToNode(
category: ICategory, children: readonly INode[]): INode {
return {
id: `${category.id}`,
id: getCategoryNodeId(category),
text: category.name,
selected: false,
children,
documentationUrls: category.documentationUrls,
};
}
function convertScriptToNode(script: IScript, selection: IUserSelection): INode {
function convertScriptToNode(script: IScript): INode {
return {
id: `${script.id}`,
id: getScriptNodeId(script),
text: script.name,
selected: selection.isSelected(script),
children: undefined,
documentationUrls: script.documentationUrls,
};

View File

@@ -1,12 +1,12 @@
<template>
<span id="container">
<span v-if="nodes != null && nodes.length > 0">
<SelectableTree
:nodes="nodes"
:selectedNodeIds="selectedNodeIds"
:filterPredicate="filterPredicate"
:filterText="filterText"
v-on:nodeSelected="checkNodeAsync($event)">
<SelectableTree
:initialNodes="nodes"
:selectedNodeIds="selectedNodeIds"
:filterPredicate="filterPredicate"
:filterText="filterText"
v-on:nodeSelected="checkNodeAsync($event)">
</SelectableTree>
</span>
<span v-else>Nooo 😢</span>
@@ -19,9 +19,10 @@
import { Category } from '@/domain/Category';
import { IRepository } from '@/infrastructure/Repository/IRepository';
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
import { IFilterMatches } from '@/application/State/Filter/IFilterMatches';
import { parseAllCategories, parseSingleCategory } from './ScriptNodeParser';
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId } from './ScriptNodeParser';
import SelectableTree, { FilterPredicate } from './SelectableTree/SelectableTree.vue';
import { INode } from './SelectableTree/INode';
@@ -33,15 +34,15 @@
export default class ScriptsTree extends StatefulVue {
@Prop() public categoryId?: number;
public nodes?: INode[] = null;
public selectedNodeIds?: string[] = null;
public nodes?: ReadonlyArray<INode> = null;
public selectedNodeIds?: ReadonlyArray<string> = [];
public filterText?: string = null;
private matches?: IFilterMatches;
private filtered?: IFilterResult;
public async mounted() {
// React to state changes
const state = await this.getCurrentStateAsync();
// React to state changes
state.selection.changed.on(this.handleSelectionChanged);
state.filter.filterRemoved.on(this.handleFilterRemoved);
state.filter.filtered.on(this.handleFiltered);
@@ -54,7 +55,7 @@
return; // only interested in script nodes
}
const state = await this.getCurrentStateAsync();
if (node.selected) {
if (!this.selectedNodeIds.some((id) => id === node.id)) {
state.selection.addSelectedScript(node.id);
} else {
state.selection.removeSelectedScript(node.id);
@@ -65,40 +66,36 @@
public async initializeNodesAsync(categoryId?: number) {
const state = await this.getCurrentStateAsync();
if (categoryId) {
this.nodes = parseSingleCategory(categoryId, state);
this.nodes = parseSingleCategory(categoryId, state.app);
} else {
this.nodes = parseAllCategories(state);
this.nodes = parseAllCategories(state.app);
}
this.selectedNodeIds = state.selection.selectedScripts
.map((script) => getScriptNodeId(script));
}
public filterPredicate(node: INode): boolean {
return this.matches.scriptMatches.some((script: IScript) => script.id === node.id);
return this.filtered.scriptMatches.some(
(script: IScript) => node.id === getScriptNodeId(script))
|| this.filtered.categoryMatches.some(
(category: ICategory) => node.id === getCategoryNodeId(category));
}
private handleSelectionChanged(selectedScripts: ReadonlyArray<IScript>) {
this.nodes = this.nodes.map((node: INode) => updateNodeSelection(node, selectedScripts));
private handleSelectionChanged(selectedScripts: ReadonlyArray<IScript>): void {
this.selectedNodeIds = selectedScripts
.map((node) => node.id);
}
private handleFilterRemoved() {
this.filterText = '';
}
private handleFiltered(matches: IFilterMatches) {
this.filterText = matches.query;
this.matches = matches;
private handleFiltered(result: IFilterResult) {
this.filterText = result.query;
this.filtered = result;
}
}
function updateNodeSelection(node: INode, selectedScripts: ReadonlyArray<IScript>): INode {
return {
id: node.id,
text: node.text,
selected: selectedScripts.some((script) => script.id === node.id),
children: node.children ? node.children.map((child) => updateNodeSelection(child, selectedScripts)) : [],
documentationUrls: node.documentationUrls,
};
}
</script>
<style scoped lang="scss">

View File

@@ -3,5 +3,4 @@ export interface INode {
readonly text: string;
readonly documentationUrls: ReadonlyArray<string>;
readonly children?: ReadonlyArray<INode>;
readonly selected: boolean;
}

View File

@@ -7,7 +7,7 @@
<a :href="url"
:alt="url"
target="_blank" class="docs"
v-tooltip.top-center="url"
v-tooltip.top-center="url"
v-on:click.stop>
<font-awesome-icon :icon="['fas', 'info-circle']" />
</a>

View File

@@ -0,0 +1,32 @@
import { ILiquorTreeNewNode, ILiquorTreeExistingNode } from 'liquor-tree';
import { INode } from './INode';
// Functions to translate INode to LiqourTree models and vice versa for anti-corruption
export function convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode): INode {
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
return {
id: liquorTreeNode.id,
text: liquorTreeNode.data.text,
// selected: liquorTreeNode.states && liquorTreeNode.states.checked,
children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0)
? [] : liquorTreeNode.children.map((childNode) => convertExistingToNode(childNode)),
documentationUrls: liquorTreeNode.data.documentationUrls,
};
}
export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
if (!node) { throw new Error('node is undefined'); }
return {
id: node.id,
text: node.text,
state: {
checked: false,
},
children: (!node.children || node.children.length === 0) ? [] :
node.children.map((childNode) => toNewLiquorTreeNode(childNode)),
data: {
documentationUrls: node.documentationUrls,
},
};
}

View File

@@ -1,8 +1,8 @@
<template>
<span>
<span v-if="initialNodes != null && initialNodes.length > 0">
<span v-if="initialLiquourTreeNodes != null && initialLiquourTreeNodes.length > 0">
<tree :options="liquorTreeOptions"
:data="this.initialNodes"
:data="initialLiquourTreeNodes"
v-on:node:checked="nodeSelected($event)"
v-on:node:unchecked="nodeSelected($event)"
ref="treeElement"
@@ -18,9 +18,10 @@
<script lang="ts">
import { Component, Prop, Vue, Emit, Watch } from 'vue-property-decorator';
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree } from 'liquor-tree';
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeOptions } from 'liquor-tree';
import Node from './Node.vue';
import { INode } from './INode';
import { convertExistingToNode, toNewLiquorTreeNode } from './NodeTranslator';
export type FilterPredicate = (node: INode) => boolean;
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
@@ -33,28 +34,31 @@
export default class SelectableTree extends Vue {
@Prop() public filterPredicate?: FilterPredicate;
@Prop() public filterText?: string;
@Prop() public nodes?: INode[];
@Prop() public selectedNodeIds?: ReadonlyArray<string>;
@Prop() public initialNodes?: ReadonlyArray<INode>;
public initialNodes?: ILiquorTreeNewNode[] = null;
public liquorTreeOptions = this.getLiquorTreeOptions();
public initialLiquourTreeNodes?: ILiquorTreeNewNode[] = null;
public liquorTreeOptions = this.getDefaults();
public convertExistingToNode = convertExistingToNode;
public mounted() {
// console.log('Mounted', 'initial nodes', this.nodes);
// console.log('Mounted', 'initial model', this.getLiquorTreeApi().model);
if (this.nodes) {
this.initialNodes = this.nodes.map((node) => this.toLiquorTreeNode(node));
if (this.initialNodes) {
const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node));
if (this.selectedNodeIds) {
recurseDown(initialNodes,
(node) => node.state.checked = this.selectedNodeIds.includes(node.id));
}
this.initialLiquourTreeNodes = initialNodes;
} else {
throw new Error('Initial nodes are null or empty');
}
if (this.filterText) {
this.updateFilterText(this.filterText);
}
}
public nodeSelected(node: ILiquorTreeExistingNode) {
this.$emit('nodeSelected', this.convertExistingToNode(node));
this.$emit('nodeSelected', convertExistingToNode(node));
return;
}
@@ -64,104 +68,28 @@
if (!filterText) {
api.clearFilter();
} else {
api.filter('filtered'); // text does not matter, it'll trigger the predicate
api.filter('filtered'); // text does not matter, it'll trigger the filterPredicate
}
}
@Watch('nodes', {deep: true})
public setSelectedStatus(nodes: |ReadonlyArray<INode>) {
if (!nodes || nodes.length === 0) {
throw new Error('Updated nodes are null or empty');
@Watch('selectedNodeIds')
public setSelectedStatus(selectedNodeIds: ReadonlyArray<string>) {
if (!selectedNodeIds) {
throw new Error('Selected nodes are undefined');
}
// Update old node properties, re-setting it changes expanded status etc.
// It'll not be needed when this is merged: https://github.com/amsik/liquor-tree/pull/141
const updateCheckedState = (
oldNodes: ReadonlyArray<ILiquorTreeExistingNode>,
updatedNodes: ReadonlyArray<INode>): ILiquorTreeNewNode[] => {
const newNodes = new Array<ILiquorTreeNewNode>();
for (const oldNode of oldNodes) {
for (const updatedNode of updatedNodes) {
if (oldNode.id === updatedNode.id) {
const newState = oldNode.states;
newState.checked = updatedNode.selected;
newNodes.push({
id: oldNode.id,
text: updatedNode.text,
children: oldNode.children == null ? [] :
updateCheckedState(
oldNode.children,
updatedNode.children),
state: newState,
data: {
documentationUrls: oldNode.data.documentationUrls,
},
});
}
}
}
return newNodes;
};
const newModel = updateCheckedState(
this.getLiquorTreeApi().model, nodes);
this.getLiquorTreeApi().setModel(newModel);
}
private convertItem(liquorTreeNode: ILiquorTreeNewNode): INode {
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
return {
id: liquorTreeNode.id,
text: liquorTreeNode.text,
selected: liquorTreeNode.state && liquorTreeNode.state.checked,
children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0)
? [] : liquorTreeNode.children.map((childNode) => this.convertItem(childNode)),
documentationUrls: liquorTreeNode.data.documentationUrls,
};
}
private convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode): INode {
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
return {
id: liquorTreeNode.id,
text: liquorTreeNode.data.text,
selected: liquorTreeNode.states && liquorTreeNode.states.checked,
children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0)
? [] : liquorTreeNode.children.map((childNode) => this.convertExistingToNode(childNode)),
documentationUrls: liquorTreeNode.data.documentationUrls,
};
}
private toLiquorTreeNode(node: INode): ILiquorTreeNewNode {
if (!node) { throw new Error('node is undefined'); }
return {
id: node.id,
text: node.text,
state: {
checked: node.selected,
},
children: (!node.children || node.children.length === 0) ? [] :
node.children.map((childNode) => this.toLiquorTreeNode(childNode)),
data: {
documentationUrls: node.documentationUrls,
},
};
}
private getLiquorTreeOptions(): any {
return {
checkbox: true,
checkOnSelect: true,
deletion: (node) => !node.children || node.children.length === 0,
filter: {
matcher: (query: string, node: ILiquorTreeExistingNode) => {
if (!this.filterPredicate) {
throw new Error('Cannot filter as predicate is null');
}
return this.filterPredicate(this.convertExistingToNode(node));
},
emptyText: '🕵Hmm.. Can not see one 🧐',
},
};
const newNodes = updateCheckedState(this.getLiquorTreeApi().model, selectedNodeIds);
this.getLiquorTreeApi().setModel(newNodes);
/* Alternative:
this.getLiquorTreeApi().recurseDown((node) => {
node.states.checked = selectedNodeIds.includes(node.id);
});
Problem: Does not check their parent if all children are checked, because it does not
trigger update on parent as we work with scripts not categories. */
/* Alternative:
this.getLiquorTreeApi().recurseDown((node) => {
if(selectedNodeIds.includes(node.id)) { node.select(); } else { node.unselect(); }
});
Problem: Emits nodeSelected() event again which will cause an infinite loop. */
}
private getLiquorTreeApi(): ILiquorTree {
@@ -170,8 +98,61 @@
}
return (this.$refs.treeElement as any).tree;
}
private getDefaults(): ILiquorTreeOptions {
return {
multiple: true,
checkbox: true,
checkOnSelect: true,
autoCheckChildren: true,
parentSelect: false,
keyboardNavigation: true,
deletion: (node) => !node.children || node.children.length === 0,
filter: {
matcher: (query: string, node: ILiquorTreeExistingNode) => {
if (!this.filterPredicate) {
throw new Error('Cannot filter as predicate is null');
}
return this.filterPredicate(convertExistingToNode(node));
},
emptyText: '🕵Hmm.. Can not see one 🧐',
},
};
}
}
function recurseDown(
nodes: ReadonlyArray<ILiquorTreeNewNode>,
handler: (node: ILiquorTreeNewNode) => void) {
for (const node of nodes) {
handler(node);
if (node.children) {
recurseDown(node.children, handler);
}
}
}
function updateCheckedState(
oldNodes: ReadonlyArray<ILiquorTreeExistingNode>,
selectedNodeIds: ReadonlyArray<string>): ReadonlyArray<ILiquorTreeNewNode> {
const result = new Array<ILiquorTreeNewNode>();
for (const oldNode of oldNodes) {
const newState = oldNode.states;
newState.checked = selectedNodeIds.some((id) => id === oldNode.id);
const newNode: ILiquorTreeNewNode = {
id: oldNode.id,
text: oldNode.data.text,
data: {
documentationUrls: oldNode.data.documentationUrls,
},
children: oldNode.children == null ? [] :
updateCheckedState(oldNode.children, selectedNodeIds),
state: newState,
};
result.push(newNode);
}
return result;
}
</script>

View File

@@ -1,12 +1,25 @@
<template>
<div>
<div class="help-container">
<TheSelector class="left" />
<TheGrouper class="right"
v-on:groupingChanged="onGroupingChanged($event)" />
<TheSelector />
<TheGrouper
v-on:groupingChanged="onGroupingChanged($event)"
v-show="!this.isSearching" />
</div>
<div class="scripts">
<div v-if="!isSearching || searchHasMatches">
<CardList v-if="this.showCards" />
<div v-else-if="this.showList" class="tree">
<div v-if="this.isSearching" class="search-query">
Searching for "{{this.searchQuery | threeDotsTrim}}"</div>
<ScriptsTree />
</div>
</div>
<div v-else class="search-no-matches">
Sorry, no matches for "{{this.searchQuery | threeDotsTrim}}" 😞
Feel free to extend the scripts <a :href="repositoryUrl" target="_blank" class="child github" >here</a>.
</div>
</div>
<CardList v-if="showCards" />
<ScriptsTree v-if="showList" />
</div>
</template>
@@ -14,11 +27,13 @@
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
import { Category } from '@/domain/Category';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Grouping } from './Grouping/Grouping';
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
import TheGrouper from '@/presentation/Scripts/Grouping/TheGrouper.vue';
import TheSelector from '@/presentation/Scripts/Selector/TheSelector.vue';
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
import { Grouping } from './Grouping/Grouping';
/** Shows content of single category or many categories */
@Component({
@@ -28,40 +43,86 @@
ScriptsTree,
CardList,
},
filters: {
threeDotsTrim(query: string) {
const threshold = 30;
if (query.length <= threshold - 3) {
return query;
}
return `${query.substr(0, threshold)}...`;
},
},
})
export default class TheScripts extends StatefulVue {
public showCards = true;
public showCards = false;
public showList = false;
public repositoryUrl = '';
private searchQuery = '';
private isSearching = false;
private searchHasMatches = false;
@Prop() public data!: Category | Category[];
private currentGrouping: Grouping;
public async mounted() {
const state = await this.getCurrentStateAsync();
this.repositoryUrl = state.app.repositoryUrl;
state.filter.filterRemoved.on(() => {
this.isSearching = false;
this.updateGroups();
});
state.filter.filtered.on((result: IFilterResult) => {
this.searchQuery = result.query;
this.isSearching = true;
this.searchHasMatches = result.hasAnyMatches();
this.updateGroups();
});
}
public onGroupingChanged(group: Grouping) {
switch (group) {
case Grouping.Cards:
this.showCards = true;
this.showList = false;
break;
case Grouping.None:
this.showCards = false;
this.showList = true;
break;
default:
throw new Error('Unknown grouping');
}
this.currentGrouping = group;
this.updateGroups();
}
private updateGroups(): void {
this.showCards = !this.isSearching && this.currentGrouping === Grouping.Cards;
this.showList = this.isSearching || this.currentGrouping === Grouping.None;
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
.scripts {
margin-top:10px;
.search-no-matches {
word-break:break-word;
color: $white;
text-transform: uppercase;
color: $light-gray;
font-size: 1.5em;
background-color: $slate;
padding:5%;
text-align:center;
> a {
color: $gray;
}
}
.tree {
padding-left: 3%;
padding-top: 15px;
padding-bottom: 15px;
.search-query {
display: flex;
justify-content: center;
color: $gray;
}
}
}
.help-container {
display: flex;
justify-content: space-between;
.left {
justify-content: flex-start;
}
.right {
justify-content: flex-end;
}
flex-wrap: wrap;
}
</style>

View File

@@ -1,20 +1,41 @@
<template>
<div id="footer">
{{text}}
<div class="footer">
<div class="item">
<a :href="releaseUrl" target="_blank">{{ version }}</a>
</div>
<div class="item">
<a @click="$modal.show(modalName)">Privacy</a> <!-- href to #privacy to avoid scrolling to top -->
</div>
<modal :name="modalName" height="auto" :scrollable="true" :adaptive="true">
<div class="modal">
<ThePrivacyPolicy class="modal__content"/>
<div class="modal__close-button">
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(modalName)"/>
</div>
</div>
</modal>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue';
import ThePrivacyPolicy from './ThePrivacyPolicy.vue';
@Component
@Component({
components: {
ThePrivacyPolicy,
},
})
export default class TheFooter extends StatefulVue {
private text: string = '';
private readonly modalName = 'privacy-policy';
private version: string = '';
private releaseUrl: string = '';
public async mounted() {
const state = await this.getCurrentStateAsync();
this.text = `v${state.app.version}`;
this.version = `v${state.app.version}`;
this.releaseUrl = `${state.app.repositoryUrl}/releases/tag/${state.app.version}`;
}
}
</script>
@@ -22,10 +43,45 @@ export default class TheFooter extends StatefulVue {
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
#footer {
color: $gray;
font-size: 0.7em;
font-family: $artistic-font;
text-align: center;
.footer {
display: flex;
color: $dark-gray;
font-size: 1rem;
font-family: $normal-font;
align-self: center;
a {
color:inherit;
text-decoration: underline;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.item:not(:first-child)::before {
content: "|";
padding: 0 5px;
}
}
.modal {
margin-bottom: 10px;
display: flex;
flex-direction: row;
&__content {
width: 100%;
}
&__close-button {
width: auto;
font-size: 1.5em;
margin-right:0.25em;
align-self: flex-start;
cursor: pointer;
&:hover {
opacity: 0.9;
}
}
}
</style>

View File

@@ -2,7 +2,7 @@
<div id="container">
<h1 class="child title" >{{ title }}</h1>
<h2 class="child subtitle">{{ subtitle }}</h2>
<a :href="githubUrl" target="_blank" class="child github" >
<a :href="repositoryUrl" target="_blank" class="child github" >
<font-awesome-icon :icon="['fab', 'github']" size="3x" />
</a>
</div>
@@ -14,14 +14,15 @@ import { StatefulVue } from './StatefulVue';
@Component
export default class TheHeader extends StatefulVue {
private title: string = '';
private subtitle: string = '';
@Prop() private githubUrl!: string;
public title = '';
public subtitle = '';
public repositoryUrl = '';
public async mounted() {
const state = await this.getCurrentStateAsync();
this.title = state.app.name;
this.subtitle = 'Enforce privacy & security on Windows';
this.repositoryUrl = state.app.repositoryUrl;
}
}
</script>

View File

@@ -0,0 +1,70 @@
<template>
<div class="privacy-policy">
<div class="line">
<div class="line__emoji">🚫🍪</div>
<div>No cookies!</div>
</div>
<div class="line">
<div class="line__emoji">🚫👀</div>
<div>No user behavior / IP adress collection!</div>
</div>
<div class="line">
<div class="line__emoji">🤖</div>
<div>Website is deployed automatically from master branch
of the <a :href="repositoryUrl" target="_blank">source code</a> with no changes.</div>
</div>
<div class="line">
<div class="line__emoji">📈</div>
<div>Basic <a href="https://aws.amazon.com/cloudfront/reporting/" target="_blank">CDN statistics</a>
are collected by AWS but they cannot be related to you or your behavior.</div>
</div>
<div class="line">
<div class="line__emoji">🎉</div>
<div>As almost no data is colected, the website gets better only with your active feedback.
Feel free to <a :href="feedbackUrl" target="_blank">create an issue</a> 😊</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
@Component
export default class TheFooter extends StatefulVue {
private repositoryUrl: string = '';
private feedbackUrl: string = '';
public async mounted() {
const state = await this.getCurrentStateAsync();
this.repositoryUrl = state.app.repositoryUrl;
this.feedbackUrl = `${state.app.repositoryUrl}/issues`;
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
.privacy-policy {
display: flex;
flex-direction: column;
font-family: $normal-font;
text-align:center;
.line {
display: flex;
flex-direction: column;
&:not(:first-child) {
margin-top:0.2rem;
}
}
a {
color:inherit;
&:hover {
opacity: 0.8;
}
}
}
</style>

View File

@@ -1,13 +1,11 @@
<template>
<div class="container">
<div class="search">
<input type="text" class="searchTerm" placeholder="Search for configurations"
@input="updateFilterAsync($event.target.value)" >
<div class="iconWrapper">
<font-awesome-icon :icon="['fas', 'search']" />
</div>
</div>
</div>
<div class="search">
<input type="search" class="searchTerm" placeholder="Search"
@input="updateFilterAsync($event.target.value)" >
<div class="iconWrapper">
<font-awesome-icon :icon="['fas', 'search']" />
</div>
</div>
</template>
<script lang="ts">
@@ -16,8 +14,6 @@ import { StatefulVue } from './StatefulVue';
@Component
export default class TheSearchBar extends StatefulVue {
public async updateFilterAsync(filter: |string) {
const state = await this.getCurrentStateAsync();
if (!filter) {
@@ -34,13 +30,6 @@ export default class TheSearchBar extends StatefulVue {
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
.container {
padding-top: 30px;
padding-right: 30%;
padding-left: 30%;
font: $normal-font;
}
.search {
width: 100%;
position: relative;
@@ -49,6 +38,7 @@ export default class TheSearchBar extends StatefulVue {
.searchTerm {
width: 100%;
min-width: 60px;
border: 1.5px solid $gray;
border-right: none;
height: 36px;
@@ -57,6 +47,8 @@ export default class TheSearchBar extends StatefulVue {
padding-right:10px;
outline: none;
color: $gray;
font-family: $normal-font;
font-size:1em;
}
.searchTerm:focus{

View File

@@ -11,7 +11,7 @@ describe('Application', () => {
new ScriptStub('S3').withIsRecommended(true),
new ScriptStub('S4').withIsRecommended(true),
];
const sut = new Application('name', 2, [
const sut = new Application('name', 'repo', '0.1.0', [
new CategoryStub(3).withScripts(expected[0], new ScriptStub('S1').withIsRecommended(false)),
new CategoryStub(2).withScripts(expected[1], new ScriptStub('S2').withIsRecommended(false)),
]);
@@ -28,7 +28,7 @@ describe('Application', () => {
const categories = [];
// act
function construct() { return new Application('name', 2, categories); }
function construct() { return new Application('name', 'repo', '0.1.0', categories); }
// assert
expect(construct).to.throw('Application must consist of at least one category');
@@ -41,7 +41,7 @@ describe('Application', () => {
];
// act
function construct() { return new Application('name', 2, categories); }
function construct() { return new Application('name', 'repo', '0.1.0', categories); }
// assert
expect(construct).to.throw('Application must consist of at least one script');
@@ -54,7 +54,7 @@ describe('Application', () => {
];
// act
function construct() { return new Application('name', 2, categories); }
function construct() { return new Application('name', 'repo', '0.1.0', categories); }
// assert
expect(construct).to.throw('Application must consist of at least one recommended script');

View File

@@ -4,7 +4,8 @@ export class ApplicationStub implements IApplication {
public readonly totalScripts = 0;
public readonly totalCategories = 0;
public readonly name = 'StubApplication';
public readonly version = 1;
public readonly repositoryUrl = 'https://privacy.sexy';
public readonly version = '0.1.0';
public readonly categories = new Array<ICategory>();
public withCategory(category: ICategory): IApplication {
@@ -23,4 +24,7 @@ export class ApplicationStub implements IApplication {
public getAllScripts(): ReadonlyArray<IScript> {
throw new Error('Method not implemented.');
}
public getAllCategories(): ReadonlyArray<ICategory> {
throw new Error('Method not implemented.');
}
}

1
vue.config.js Normal file
View File

@@ -0,0 +1 @@
process.env.VUE_APP_VERSION = require('./package.json').version;