Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f19b2528a | ||
|
|
1f11c39773 | ||
|
|
b6ccb5927a | ||
|
|
1d465ee318 | ||
|
|
3ab48b1cf5 | ||
|
|
de4ac978bd | ||
|
|
8df5faf4ef | ||
|
|
99a2035fdb | ||
|
|
a0d61728ea | ||
|
|
312bf6102c | ||
|
|
f4885b6f1c | ||
|
|
ca63a0979e | ||
|
|
1f266c3353 | ||
|
|
c7b2a70312 | ||
|
|
255133af4d | ||
|
|
db74531cd4 | ||
|
|
f36d8bfc78 | ||
|
|
3b31ace726 | ||
|
|
6badfef9da | ||
|
|
8c38dd73d8 | ||
|
|
b8682a852a | ||
|
|
8c17929151 | ||
|
|
bb92c9ec28 | ||
|
|
b4aacea2a3 | ||
|
|
8bbe6ebf75 | ||
|
|
a23d28f2cf | ||
|
|
f51e8859ee | ||
|
|
d235dee955 | ||
|
|
2afef4ea3d | ||
|
|
f709d6a566 | ||
|
|
532915b95d | ||
|
|
456e40bedf | ||
|
|
018b7e270f | ||
|
|
f8b8b4c97a | ||
|
|
978d7d0863 | ||
|
|
594a14d6ca | ||
|
|
c628aa9aef | ||
|
|
3060ebf79c | ||
|
|
1a34c7374b | ||
|
|
c262681011 | ||
|
|
f8ba5c46e4 | ||
|
|
b789250cb8 | ||
|
|
5df458739d | ||
|
|
d6fa9a2a03 | ||
|
|
ec15af01dd | ||
|
|
7073336f81 | ||
|
|
3d3380f27e | ||
|
|
c69998c7cb | ||
|
|
1663bfeac7 | ||
|
|
afc3bfb3b8 | ||
|
|
b6bfc25727 | ||
|
|
7fac0fe79f | ||
|
|
5967347b80 | ||
|
|
855a445c1a | ||
|
|
1cc12195a3 | ||
|
|
66d4d39d5b | ||
|
|
a5dbe66fc1 | ||
|
|
4c8be45e28 | ||
|
|
6049a2b834 | ||
|
|
831c014f97 | ||
|
|
5c15a7a64a | ||
|
|
e43992b278 | ||
|
|
5963d2bac5 | ||
|
|
45816a2bcc | ||
|
|
60a5a2aa40 | ||
|
|
04b9b59e14 | ||
|
|
4ff4b52202 | ||
|
|
73c426844a | ||
|
|
25ce236a77 | ||
|
|
9b20175545 | ||
|
|
92a7118d1c | ||
|
|
a9f9e90443 | ||
|
|
31d2067f07 | ||
|
|
dd7e1416b4 | ||
|
|
1d5225de07 | ||
|
|
9c063d59de | ||
|
|
57028987f1 | ||
|
|
9e722ddfb3 | ||
|
|
646a8e0b9f | ||
|
|
f27a2871d7 | ||
|
|
909c44d72a | ||
|
|
53cf595e17 | ||
|
|
2c4eb78c3f | ||
|
|
d7a1325c0b | ||
|
|
30efbcc621 | ||
|
|
628c16eb95 | ||
|
|
d8552c62ff | ||
|
|
df84083536 | ||
|
|
461a4f122b | ||
|
|
c937af8ee7 | ||
|
|
636d4279c8 | ||
|
|
019b838925 | ||
|
|
0fc18459cd | ||
|
|
583c5660d6 | ||
|
|
52d5713a99 | ||
|
|
b34a66f270 | ||
|
|
eed996f608 | ||
|
|
b96c5d0557 | ||
|
|
aab8f21a8d | ||
|
|
c668a97950 | ||
|
|
bb98d20637 | ||
|
|
e2ab124fb7 | ||
|
|
0d2efe5b05 | ||
|
|
156a6554ef | ||
|
|
4a91e8ccd8 | ||
|
|
997be7113f | ||
|
|
3e3bc07576 | ||
|
|
691f989682 | ||
|
|
226074c534 | ||
|
|
97b7e03233 | ||
|
|
749a140eb8 | ||
|
|
4739a4ac40 | ||
|
|
4800340b9b | ||
|
|
074734242b | ||
|
|
802b36bdd8 | ||
|
|
0c39a06be5 | ||
|
|
e63ac4ae67 | ||
|
|
edd076fade | ||
|
|
0ce354ea09 | ||
|
|
19813b6917 | ||
|
|
97a7747933 | ||
|
|
92f1a36bcb | ||
|
|
31364bdfec | ||
|
|
5b743a67a4 | ||
|
|
16a7327750 | ||
|
|
5ea46ecbf5 | ||
|
|
e3f82e069e | ||
|
|
95baf3175b | ||
|
|
89862b2775 | ||
|
|
fab87378a2 | ||
|
|
cafe6e809a | ||
|
|
e0b080af69 | ||
|
|
ec6b3c5407 | ||
|
|
6825001c61 | ||
|
|
ed872ef3d9 | ||
|
|
4bc13e1192 | ||
|
|
ab28f4ed85 | ||
|
|
cfd888f3af | ||
|
|
eee0e785ec | ||
|
|
99576340b6 | ||
|
|
47e5560c92 | ||
|
|
5cf8614b17 | ||
|
|
a7da75d442 | ||
|
|
246e753ddc | ||
|
|
60e6348dc8 | ||
|
|
10a34fae2f | ||
|
|
2cf9214b14 | ||
|
|
cced601d68 | ||
|
|
3140cc663b | ||
|
|
20020af7c1 | ||
|
|
2aa3742e30 | ||
|
|
5ccc7c5952 | ||
|
|
aaea47e7d1 | ||
|
|
57037aaefc | ||
|
|
c359f1d89c | ||
|
|
d38f6cd6a8 | ||
|
|
c646c10273 | ||
|
|
aff463dd64 | ||
|
|
8d05b03c9f | ||
|
|
7b4277d770 | ||
|
|
beb3c8339f | ||
|
|
e99f210c9d | ||
|
|
090e831909 | ||
|
|
a229aca68a |
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist_electron
|
||||||
|
.vs
|
||||||
|
.vscode
|
||||||
|
.github
|
||||||
|
.git
|
||||||
|
docs
|
||||||
|
docker
|
||||||
91
.github/workflows/build-and-deploy.yaml
vendored
@@ -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}}
|
|
||||||
17
.github/workflows/bump-and-release.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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.
|
||||||
32
.github/workflows/deploy-desktop.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Deploy desktop
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created] # will be triggered when a NON-draft release is created and published.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-desktop-app:
|
||||||
|
name: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: master # otherwise it defaults to the version tag missing bump commit
|
||||||
|
fetch-depth: 0 # fetch all history
|
||||||
|
- name: Checkout to bump commit
|
||||||
|
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: '14.x'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Run tests
|
||||||
|
run: npm run test:unit
|
||||||
|
- name: Publish desktop app
|
||||||
|
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
117
.github/workflows/deploy-site.yaml
vendored
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
name: Deploy site
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created] # will be triggered when a NON-draft release is created and published.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
|
||||||
|
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 ci
|
||||||
|
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 }}
|
||||||
26
.github/workflows/quality-checks.yaml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Quality checks
|
||||||
|
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
lint-command:
|
||||||
|
- npm run lint:vue
|
||||||
|
- npm run lint:yaml
|
||||||
|
- npm run lint:md
|
||||||
|
- npm run lint:md:relative-urls
|
||||||
|
- npm run lint:md:consistency
|
||||||
|
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
|
||||||
|
run: ${{ matrix.lint-command }}
|
||||||
22
.github/workflows/security-checks.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Security checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
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
|
||||||
@@ -1,26 +1,22 @@
|
|||||||
name: Run tests
|
name: Test
|
||||||
|
|
||||||
on:
|
on: push
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '*'
|
|
||||||
- '!master'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
run-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v2
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '11.x'
|
node-version: '14.x'
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm install
|
run: npm ci
|
||||||
-
|
-
|
||||||
name: Run tests
|
name: Run tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
4
.gitignore
vendored
@@ -1,4 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
.vs
|
.vs
|
||||||
.vscode
|
.vscode
|
||||||
|
#Electron-builder output
|
||||||
|
/dist_electron
|
||||||
4
.markdownlint.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"default": true,
|
||||||
|
"MD013": false
|
||||||
|
}
|
||||||
234
CHANGELOG.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.7.2 (2020-09-06)
|
||||||
|
|
||||||
|
* update onesync documentation and do not recommend it as it breaks other apps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f36d8bfc7848bb65ac0c641e318a689bf3816ccf)
|
||||||
|
* add reversibility for biometric disabling and do not recommend it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/db74531cd4139615c6d595959217d3651f099019)
|
||||||
|
* fix bad highlighting of selected nodes when using keyboard navigation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/255133af4dfae40171406648a3e2920f16d71cb3)
|
||||||
|
* add reversibility to removing bloatware | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c7b2a703128470a05f12c9c6e8002444def37ef8)
|
||||||
|
* fix indeterminate state being lost | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f266c33535f72b69c65985bf2eff27cd2c5a104)
|
||||||
|
* fix wording in default text in text area | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ca63a0979ef55d07d09d9443e5cea9aa888870a5)
|
||||||
|
* add best practice suggestion to come back | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f4885b6f1c82752f2143934e336d6d1b1af03015)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.1...0.7.2)
|
||||||
|
|
||||||
|
## 0.7.1 (2020-09-04)
|
||||||
|
|
||||||
|
* fix some browsers (including firefox) downloading the script as a text file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8c17929151f9c4fa5f48564492bbf400ced95eea)
|
||||||
|
* rename screenshot image file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b8682a852a14ed6cf49986695d9510b840ac9d3d)
|
||||||
|
* fix new/changed script higlighting not working on production builds | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8c38dd73d8c7b77d8d341c0389f4d7229f9b97fd)
|
||||||
|
* refactor unused imports | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6badfef9daace0c5de3fd33652a82bfe22261b11)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.0...0.7.1)
|
||||||
|
|
||||||
|
## 0.7.0 (2020-09-02)
|
||||||
|
|
||||||
|
* [search] better (multilined) message when there are no results | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ec15af01dd020b364c2174fe562fd66227c2320c)
|
||||||
|
* [search] added clear/close button | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d6fa9a2a03c0ebe68b94f0b80cc52b4e200c9213)
|
||||||
|
* move script generation to /generation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5df458739d076719e350ba194c4f3f772884fcdb)
|
||||||
|
* add auto-highlighting of selected/updated code | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b789250cb89e2130b08e1a927df8181cf945dfeb)
|
||||||
|
* prompt admin priviliges automatically | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f8ba5c46e4923d9c35f200f8a08aa6437f7c0ecc)
|
||||||
|
* add removal of ghost (default0) telemetry user | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c262681011f39b4412669b6cf233476f676ca550)
|
||||||
|
* add more windows defender tweaks, categorization and reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1a34c7374ba56bafa0209bbb55c81b233bb419ed)
|
||||||
|
* fix NTP script documentation is on wrong place | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3060ebf79cf242370433495cc3e1878b7581b202)
|
||||||
|
* updated dependencies to latest and audit fixes (#25) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c628aa9aef8ab7c815661d3c1711e7fbc65c69a2)
|
||||||
|
* categorize, fix and extend windows log files cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/594a14d6ca76cbd27a21877b8c373c1930589ca6)
|
||||||
|
* add more OneDrive cleanup scripts and categorize them | [commit](https://github.com/undergroundwires/privacy.sexy/commit/978d7d08638dd161082f239ed088b12302f29458)
|
||||||
|
* add disabling firefox telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f8b8b4c97ab734d5ba7370894b694993924388da)
|
||||||
|
* add disabling ccleaner telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/018b7e270f207aac926cb12f8069ebfcdce193ce)
|
||||||
|
* Add disabling of PowerShell 7+ telemetry (#29) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/456e40bedf9afcc846f9b13f1ea144cef6115cf6)
|
||||||
|
* categorize, fix, make scripts reversible in "UI for privacy", "security improvements" and "configure browsers" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/532915b95da9fecd6b981d91bf489359e4e53caa)
|
||||||
|
* fix "Configure Defender" being in wrong category #28 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f709d6a566ed7846b677b383863deda9680a2a9c)
|
||||||
|
* do not hardcode capability versions and make them reversible | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2afef4ea3d0d3d09aa1fa1eedba8493680bd8f10)
|
||||||
|
* exclude paint, wordpad and notepad from bloatware removal | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d235dee95514a01745aef9479d07f88ffb4b40b8)
|
||||||
|
* add reversibility on category level | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f51e8859eeb32c944126d692cfe03a0320c8b568)
|
||||||
|
* refactor unused imports & variables | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a23d28f2cfa2d64d45460697cf5ee9d6b5920752)
|
||||||
|
* fix search (got broken in b789250) with tests and refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8bbe6ebf750f1a1cbab493fb99b5ea91f4e21609)
|
||||||
|
* update the screenshot to show off highlighting | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b4aacea2a3e0bbcf2d8a79ff67f51c0f19e888a6)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.2...0.7.0)
|
||||||
|
|
||||||
|
## 0.6.2 (2020-08-16)
|
||||||
|
|
||||||
|
* 🐛 fixed disabling error reporting for november 2019 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5967347b80976a519f6f4eb1972a62f3e600df2b)
|
||||||
|
* 🐛 fixed blank screen and icons on mac | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7fac0fe79f252e8f9dda4f6f83cd6fa4ba2b539f)
|
||||||
|
* 🐛 fixed removing onedrive does not delete scheduled tasks | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b6bfc2572740c0cd46d3bc0058fa767dd5fa862e)
|
||||||
|
* ⚙️ enhanced tweak to disable for office telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/afc3bfb3b8896f332c9a196973ded3dce8fd21e4)
|
||||||
|
* ✨ added script to clear dotnet telemery | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1663bfeac7b6580b1335ca5fcf3587b69c080c72)
|
||||||
|
* 🐛 fixed changing time server not working | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c69998c7cb29ffcf40f0af03b73150736581da69)
|
||||||
|
* 🔥 removed disabling ClickToRun as it breaks office | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3d3380f27ebeea53f17f49974aaa89300ffaf2dd)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.1...0.6.2)
|
||||||
|
|
||||||
|
## 0.6.1 (2020-08-09)
|
||||||
|
|
||||||
|
* updated documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5963d2bac551083f9d16cce6b851abf0e8b88ce7)
|
||||||
|
* fixed typo in footer | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5c15a7a64aaf24578a32713dec491bf494216303)
|
||||||
|
* more scripts can be reverted | [commit](https://github.com/undergroundwires/privacy.sexy/commit/831c014f977515454ee6dc664d77a8c434495501)
|
||||||
|
* moved windows connect now to security & recommended | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6049a2b834d8d17af741f8d8f8b07cd15153b001)
|
||||||
|
* fixed mac / linux download links | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c8be45e287b5ea009d6f828f7f327f37850569e)
|
||||||
|
* tweaks to disable webcam, speech and compatibility telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a5dbe66fc175e39397f296ab2ff703e9b0ab4d7c)
|
||||||
|
* refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/66d4d39d5bf3db305450514c6b6224654dafbfb2)
|
||||||
|
* fixed removing onedrive does not clean start menu / quick access | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1cc12195a3e9a11c590d3ed64d80299b50f74838)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.0...0.6.1)
|
||||||
|
|
||||||
|
## 0.6.0 (2020-07-26)
|
||||||
|
|
||||||
|
* fixed dead links in documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/25ce236a7737decaf2eb9b8c29a4c4f34d43f770)
|
||||||
|
* runs tests on each push on the repository | [commit](https://github.com/undergroundwires/privacy.sexy/commit/73c426844a0330718a9ab7de12b61ca05e853323)
|
||||||
|
* code area now shows "how" before "why" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ff4b52202b1c5dbfe2b80580bbe7d93132ab05c)
|
||||||
|
* support for desktop versions #20 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/04b9b59e14766ccd251474ad3710baf1f682fd49)
|
||||||
|
* reworked on footer & removed github icon | [commit](https://github.com/undergroundwires/privacy.sexy/commit/60a5a2aa4026d384bef9e6a203f1b7514a269c33)
|
||||||
|
* updated dependencies to latest | [commit](https://github.com/undergroundwires/privacy.sexy/commit/45816a2bccb3d11a50e3f2bc19c0a6cc2587deaa)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.5.0...0.6.0)
|
||||||
|
|
||||||
|
## 0.5.0 (2020-07-19)
|
||||||
|
|
||||||
|
* added ability to revert (#21) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9c063d59defa6297c64f50b49403e8bd10620de9)
|
||||||
|
* search placeholder shows total scripts | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1d5225de07186f853f4cf7aedd4998f5d00c107a)
|
||||||
|
* do not collapse card when on "Search" and "Select" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/dd7e1416b4df54bf71b719d4654db88769dc0994)
|
||||||
|
* opening a card scrolls to its content div | [commit](https://github.com/undergroundwires/privacy.sexy/commit/31d2067f076c3159483baec49975617dddbd158d)
|
||||||
|
* all cards in same line now have same height | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a9f9e9044385d9aed3b5551fc6c6823e813fd1e5)
|
||||||
|
* patched loadash vulnerability (#18) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/92a7118d1c5013312772e075b9ee5a79c93710b8)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.10...0.5.0)
|
||||||
|
|
||||||
|
## 0.4.10 (2020-07-15)
|
||||||
|
|
||||||
|
* fixed script errors & added tests | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9e722ddfb3825fb29d6298025baaaa033120d017)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.9...0.4.10)
|
||||||
|
|
||||||
|
## 0.4.9 (2020-07-14)
|
||||||
|
|
||||||
|
* disable office telemetry Disassembler0/Win10-Initial-Setup-Script#288 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/53cf595e1726ee3de79137fd566978fd512d218f)
|
||||||
|
* updated to may 2020 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/909c44d72a4a602ee8f27d06b6ec706c1e432ce1)
|
||||||
|
* simplified docker builds | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f27a2871d74e5117fc029be82caef12246e10879)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.8...0.4.9)
|
||||||
|
|
||||||
|
## 0.4.8 (2020-07-11)
|
||||||
|
|
||||||
|
* added more scripts #16 (#17) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d8552c62ffea13ce62abce836c7dd4980eef6bb9)
|
||||||
|
* stopping services before disabling #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/628c16eb952495f5b3f6d794161b355f4b08b819)
|
||||||
|
* can disable features, capabilities & remove onedrive #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/30efbcc621eb83dd5a9c1e66b8f1f5350eb95006)
|
||||||
|
* updated one more typo (#19) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d7a1325c0b7665ce712dc411965d00fc1d6fa384)
|
||||||
|
* more tweaks #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2c4eb78c3f156cb0d033977cffbe7464697680f5)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.7...0.4.8)
|
||||||
|
|
||||||
|
## 0.4.7 (2020-06-30)
|
||||||
|
|
||||||
|
* removed HKU tweak as all HKU's are changed #10 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c937af8ee7da9aa95131e56abf7bf24800390fe6)
|
||||||
|
* Fixed types + script in "Clear Windows log files" (#15) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/461a4f122b342369db5cc08c5e30961c64e68cdd)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.6...0.4.7)
|
||||||
|
|
||||||
|
## 0.4.6 (2020-06-16)
|
||||||
|
|
||||||
|
* Fixed Some More Issues (#12) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/52d5713a99422cdf900aba819e49e998abac33cc)
|
||||||
|
* removed failing continuous deployment #14 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/583c5660d6ac934b845a044e013357aa91f61c15)
|
||||||
|
* Updated Some Tweaks (#11) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0fc18459cde57684f00764815062f838f932aed5)
|
||||||
|
* Updated Some More Tweaks (#13) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/019b838925e963b7ec052ac76c6faf5650b9eb67)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.5...0.4.6)
|
||||||
|
|
||||||
|
## 0.4.5 (2020-06-13)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.4...0.4.5)
|
||||||
|
|
||||||
|
## 0.4.4 (2020-05-24)
|
||||||
|
|
||||||
|
* 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)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.3...0.4.4)
|
||||||
|
|
||||||
|
## 0.4.3 (2020-05-23)
|
||||||
|
|
||||||
|
* 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/4a91e8ccd8a707bc6bea34ee28cff7fa4f66ee2f)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.2...0.4.3)
|
||||||
|
|
||||||
|
## 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)
|
||||||
45
CONTRIBUTING.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
- Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||||
|
- Reporting a bug
|
||||||
|
- Discussing the current state of the code
|
||||||
|
- Submitting a fix
|
||||||
|
- Proposing new features
|
||||||
|
- Becoming a maintainer
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) is used
|
||||||
|
- Your pull requests are actively welcomed.
|
||||||
|
- The steps:
|
||||||
|
1. Fork the repo and create your branch from master.
|
||||||
|
2. If you've added code that should be tested, add tests.
|
||||||
|
3. If you've changed APIs, update the documentation.
|
||||||
|
4. Ensure the test suite passes.
|
||||||
|
5. Make sure your code lints.
|
||||||
|
6. Issue that pull request!
|
||||||
|
- 🙏 DO
|
||||||
|
- Document your changes in the pull request
|
||||||
|
- ❗ DON'T
|
||||||
|
- Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
### Extend scripts
|
||||||
|
|
||||||
|
- Create a [pull request](#Pull-Request-Process) for [application.yaml](./src/application/application.yaml)
|
||||||
|
- 🙏 For any new script, try to add `revertCode` that'll revert the changes caused by the script.
|
||||||
|
- See [typings](./src/application/application.yaml.d.ts) for documentation as code.
|
||||||
|
|
||||||
|
### Handle the state in presentation layer
|
||||||
|
|
||||||
|
- There are two types of components:
|
||||||
|
- **Stateless**, extends `Vue`
|
||||||
|
- **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts)
|
||||||
|
- The source of truth for the state lies in application layer (`./src/application/`) and must be updated from the views if they're mutating the state
|
||||||
|
- They mutate or/and reacts to changes in [application state](src/application/State/ApplicationState.ts).
|
||||||
|
- You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0.
|
||||||
12
Dockerfile
@@ -1,20 +1,12 @@
|
|||||||
# +-+-+-+-+-+ +-+-+-+-+-+
|
# Build
|
||||||
# |B|u|i|l|d| |S|t|a|g|e|
|
|
||||||
# +-+-+-+-+-+ +-+-+-+-+-+
|
|
||||||
FROM node:lts-alpine as build-stage
|
FROM node:lts-alpine as build-stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
# For testing purposes, it's easy to run http-server on lts-alpine such as continuing from here:
|
|
||||||
# RUN npm install -g http-server
|
|
||||||
# EXPOSE 8080
|
|
||||||
# CMD [ "http-server", "dist" ]
|
|
||||||
|
|
||||||
# +-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+
|
# Production stage
|
||||||
# |P|r|o|d|u|c|t|i|o|n| |S|t|a|g|e|
|
|
||||||
# +-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+
|
|
||||||
FROM nginx:stable-alpine as production-stage
|
FROM nginx:stable-alpine as production-stage
|
||||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
139
README.md
@@ -1,119 +1,88 @@
|
|||||||
# privacy.sexy
|
# privacy.sexy
|
||||||
|
|
||||||
Privacy & security generator tool for Windows.
|
> Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
> because privacy is sexy 🍑🍆
|
[](./CONTRIBUTING.md)
|
||||||
|
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
||||||
|
[](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
|
||||||
|
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
||||||
|
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
||||||
|
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
||||||
|
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
||||||
|
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
||||||
|
[](https://github.com/undergroundwires/bump-everywhere)
|
||||||
|
|
||||||
[https://privacy.sexy](https://privacy.sexy)
|
## Get started
|
||||||
|
|
||||||
In this repo you find:
|
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
||||||
|
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-Setup-0.7.2.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.dmg)
|
||||||
|
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
||||||
|
|
||||||
- Application & infrastructure code of privacy.sexy, simply everything is code & open-sourced.
|
[](https://privacy.sexy)
|
||||||
- Fully automated CI/CD pipeline to AWS for provisioning serverless infrastructure using GitHub actions.
|
|
||||||
- Vue.js application in conjunction with domain-driven design, event-driven architecture & data-driven programming.
|
|
||||||
|
|
||||||
## Commands
|
## Why
|
||||||
|
|
||||||
- Setup and run
|
- You don't need to run any compiled software that has access to your system, just run the generated scripts.
|
||||||
- For development:
|
- Have full visibility into what the tweaks do as you enable them.
|
||||||
- `npm install` to project setup.
|
- Ability to revert applied scripts
|
||||||
- `npm run serve` to compile & hot-reload for development.
|
- Easily extendable
|
||||||
- Production (using Docker):
|
- Everything is open-sourced including both application and infrastructure
|
||||||
- Build `docker build -t undergroundwires/privacy.sexy .`
|
- Fully automated CI/CD pipeline using GitHub actions
|
||||||
- Run `docker run -it -p 8080:8080 --rm --name privacy.sexy-1 undergroundwires/privacy.sexy`
|
- to AWS for provisioning serverless infrastructure
|
||||||
- Prepare for production: `npm run build`
|
- for building and sharing the desktop applications
|
||||||
- Run tests: `npm run test:unit`
|
|
||||||
- Lint and fix files: `npm run lint`
|
|
||||||
|
|
||||||
## Extend scripts
|
## 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 👌
|
||||||
|
- 📖 More: [extend scripts | CONTRIBUTING.md](./CONTRIBUTING.md#extend-scripts)
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- Project setup: `npm install`
|
||||||
|
- Testing
|
||||||
|
- Run unit tests: `npm run test:unit`
|
||||||
|
- Lint: `npm run lint`
|
||||||
|
- **Desktop app**
|
||||||
|
- Development: `npm run electron:serve`
|
||||||
|
- Production: `npm run electron:build` to build an executable
|
||||||
|
- **Webpage**
|
||||||
|
- Development: `npm run serve` to compile & hot-reload for development.
|
||||||
|
- Production: `npm run build` to prepare files for distribution.
|
||||||
|
- Or run using Docker:
|
||||||
|
1. Build: `docker build -t undergroundwires/privacy.sexy:0.7.2 .`
|
||||||
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.7.2 undergroundwires/privacy.sexy:0.7.2`
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Application
|
### Application
|
||||||
|
|
||||||
- Powered by **TypeScript** + **Vue.js** 💪
|
- Powered by **TypeScript**, **Vue.js** and **Electron** 💪
|
||||||
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
||||||
- Application uses highly decoupled models & services in different DDD layers.
|
- Application uses highly decoupled models & services in different DDD layers.
|
||||||
- **Domain layer** is where the application is modelled with validation logic.
|
- **Domain layer** is where the application is modelled with validation logic.
|
||||||
- **Presentation Layer**
|
- **Presentation Layer**
|
||||||
- Consists of Vue.js components & UI stuff.
|
- Consists of Vue.js components and other UI-related code.
|
||||||
|
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
||||||
- Event driven as in components simply listens to events from the state and act accordingly.
|
- Event driven as in components simply listens to events from the state and act accordingly.
|
||||||
- **Application Layer**
|
- **Application Layer**
|
||||||
- Keeps the application state
|
- Keeps the application state
|
||||||
- The [state](src/application/State/ApplicationState.ts) is a mutable singleton & event producer.
|
- 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))
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### AWS Infrastructure
|
### AWS Infrastructure
|
||||||
|
|
||||||
- The application runs in AWS 100% serverless and automatically provisioned using [CloudFormation files](/aws) and GitHub Actions.
|
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
||||||
- Maximum security & automation and minimum AWS costs were the highest priorities of the design.
|
|
||||||
|
|
||||||

|
- 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
|
#### 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.
|
- 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)
|
|
||||||
|
|
||||||

|
[](.github/workflows/)
|
||||||
|
|
||||||
##### CloudFormation
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|||||||
@@ -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">7Vvtd6I4F/9r+lEOEMLLR23rzJzdme3qPLMvXzwUomaLxA2x1f3rnwSIEoIWFdqd3bXnKLmEJNz7u69Jb8DtavuBhuvlZxKj5MY24+0NuLux7SAA/FsQdgXBD8yCsKA4LkjWgTDFf6GSKLttcIwypSMjJGF4rRIjkqYoYgotpJS8qN3mJFFnXYcLpBGmUZjo1F9wzJblW0DzQP+I8GIpZ7bM8s4qlJ1LQrYMY/JSIYH7G3BLCWHF1Wp7ixLBO8mX4rnxkbv7hVGUsjYPfB19Hf0+Wf6+DcbR9Mtksvm2hANYjPIcJpvyhe/IKsQpp03QAmeMhrRcPttJnqwJThmi9898ZsFe6waM9m9n8kYcZksUl40lWyWyE6PkCd2ShFBOSUnKBxzNcZJI0o0NoCn+OD0JH1HyQDLMMEn5vQiJSfmNZ0QZ5hL6sdbhkTBGVpUOwwQvxA1G1pxKNizBKZ9dAkVMEpZd9oPz91iL11xtFwLPBpnPcYSMDNFn/psZVHJlVpL4M7okSuGIhaBthVRK5gMiK8Tojncp79qeXzxSqgmQ+H85gA66BWlZwRu0S6iXMF/sRz4ggV+UYDgDGJatIUPDASWbNN7L+WWJGZquQ8GSuxfOPFX2e0xYNZmXMFCxwYEQe8GjaXbDXslNyd1A564r9bTKXuCYffEXvM5fiUW8yo1UlZvNEH9VM3KdGoXR0yKXXYXb8/zDu+STDbN1RUdkY463QoKjcj13S8aEFR4KTtjjKE5tA3M7PMccFdSI+Iz2OA5ZyH8EPeO/4Sr8i6SD8CUbZAylEU4ENTea4y/8xrRQqtmUT4m55s0ist7NZsNfprPbhGxijkvfWKeLDjAxMA1gBpWP76kgcW0dJIGOEUm7BiJ/bp/idPL8FMApjNFk+8N6+/NA18CcBWNK+DvXsdJo3ea8Z0XG/G8s5h9x0xZjpNwLHHg3tiv37jDlAxX4SQkVHNBM9RCYI9iku3s0KZ7gQsOt2eiKHog3LIMGy5btkiuN2K3bd45Ehxv1jGxohD5FYj0j3iyu1F6R4P485343RslXrZJrAg1w0i1UASdpnQNON0lTTjJHm+gJdY841xwC4J2HONvzLMv91yAuA90gDToq0mDgGa7zvmDTI0+JM/OBJDjadQu45mDzCJ7Kzn87KB0JvHV4rQsOdgIexwlU8EDdTEHfkAMr0WkH0VMjeFwNPBOODsRJcnH/uca3NFRUcH8GOzJXAKjmyrGC97VVnh6JCXHw+DTMQfc5THlErCfKjcjrW+4NYgRGdFjvbFWsdmbrsPVgcHvKfHZhTmquCDiNrggYsCHUBj0J2NcEfJ/OCYc9z07cRMR7j1y67kJcffz69WGqiRrFCyS1S5gEsiBpmNwfqCM1Wz70+ZEIkeai+wMxtit1NtwwogqW85zufq02fhODGVA277bl4EVrV209IIo5rwRY7k4m1YW6n+BVmZawkC4Qe11pBGNO4oKiJGT4Wa23NUm4fPRBOMFKFO2qlRPoBgb0LMf2i29PHbB4vXKMarWsNqzt2IbFAed5rmfbZmB558xSMEebJcfm/tUvh6ulB08TFOdeKXsPZBZOTtZH7avg5baEl/028OKiNZQigQoDD14GL+Aq8PIDeHrcngEli0//smpUil4GOxTSgYhyko1YnyALfzSukAZm0GHdybUMT5U2MC3dATZUI72+wmlbz/xVbq4pfg6jnZGhrZ6XfWcGRkK7amFO7Y30bWGAr+ZXPN9SXIt7oQfbF7nLce2gBp++bYqjgSqf63vyUKdw8aqHclvip5SXaXCnUD5zLaQsqxdI2Y5v+JWBaoUB4LYC2JDScFfptt6j4piC1HZvHLuG12LE2tNyeDKfZ6gfhOtpA0/1GE43ZMPfx4zROiG6vbw+Edhi9qsM7vl1JQ3grUMWIBoyCejUgrbVAHCuBlhWYCuyHgTXKcQbgECPxT9gttw8fv8BlPlqAEUiJi8tUxj88SqkT4NFzoAuYya3VhFym6sGPQVNz2vPu/92v8tcb/fTty+TzAzmA0uT+08UL/KjE8MoQpmwAJ9i/s6YvUkt+9azgDX+59WyKUkOpScNMg3AOn7IwndUhwVB220Qx+kJRXri9TG8JEK6JCaqHtnh6o0jxVdUoGXmH9FrQ5/3hzgajXtnNaXjOnfCf0DLUVMrv5N4qn46B/r2ZQEUcLiP24dh/FsZ1gXtykjnBlB8veryPbdNACXPSfhNHO3UsTbKWk8fhkkizvMJcYbxu6QRHUK+IYa6APID0zDBlTGS3CSGgeF7PAfJt0UDUxW7C4Im5J6rAC6oQd6EhgUq9bVeFMDR9NdvowD9Y1zfx3wz69+XKQc6ro/j/xSubUv11t2kxrC2twi9GuTaIhlaavbrgnbllXOxq53daGe8Zf4kz3H2j2V9k3TyXmb6WLLbV6xyXBF62I/oagcCng5FeisWNns53RL+L0M6eN6ldtIleNq6/cLcdQ4fLZEOLoQPdKChhtqOC7jN5jmV5zqO67mB86YIatjDeAU95xxYF42HkPGENc0pttn2GPujz1l1QNFVCSw0a9Jr2DgKGmogltnBGeVmruvVr32U/kI5f/tU4b3a/la506UKnwTapQ7gEOFAt7YRuPfWV2q5452y7bB+jKa1zgNvf8xvd5jJdDzXCuR3K6V/yxjb1l3Lp+FnTuAOhmro/K8w17owF8Zxfiaw2TZ2Yu4CDW+O/p87jfW6vqq+tqdBphrQlkKtVtM6s3DmSQunVPOOV/C6Co1bm8YTHa+Ibq6SoYxtNYvwgfumtSZfDmDWlF80Ofma6s/9CEVRk4bWFXmF4zhPfpqiEtVltghM3jHWsM/XPHHaZv9vsIXZP/wvMbj/Pw==</diagram></mxfile>
|
|
||||||
@@ -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 ]]
|
|
||||||
@@ -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 ]]
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
|
|
||||||
5
build/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# build
|
||||||
|
|
||||||
|
- These are the file that are used by electron.
|
||||||
|
- Logos are created by from the [PNG icon](./../public/icon.png)
|
||||||
|
- by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`
|
||||||
BIN
build/icons/1024x1024.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
build/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
build/icons/16x16.png
Normal file
|
After Width: | Height: | Size: 740 B |
BIN
build/icons/24x24.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
build/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
build/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
build/icons/48x48.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
build/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
build/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
build/icons/icon.icns
Normal file
BIN
build/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 240 KiB |
@@ -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">5Vpbb9s2FP41AbYHC7zp9hjHdVqsGwpkQNu9FIxEy2xlUaWo2N6v36FEOZLlXNw6S7IpQUwd3j9+/M4hnTN6sdpcal4uf1epyM8ISjdndHZGSOQT+GsN29YwIRFqLZmWaWvDt4Yr+bdwxq5YLVNRDQoapXIjy6ExUUUhEjOwca3VelhsofJhryXPxMhwlfB8bP0oU7Ps5oVu7W+FzJZdzxi5nBXvCjtDteSpWvdM9M0ZvdBKmTa12lyI3ILX4dLWm9+RuxuYFoV5TAWx2swv/wy+49kff33Kok8f39/8NnHLc8Pz2k14JspcbStbsVhoXhldJ6bWwk3CbDtktKqLVNjG0RmdKm2WKlMFz98rVYIRg/GrMGbr1pTXRoFpaVa5y015tWzqdy8fuDFCF42FIGsdz9FNu1K1TsQ9E2OOK1xnwtxTDvttQZEOeOAgvBRqJYzeQgHH6AnyCGVBW8mRmro2tMi5kTdD5nBHwGzX1K71D0rCtHZN05B6FB4/ZiGNCQoHneAgxF7Mho22MLh2+mu/17QfIA8F8e0zbBrHyMOxH0IyojgkZNhLC+LDvezNWC0WlRjUgEQP01tTw9QjWEtHrJ3WMk+rRgdSoGqQw5pPrzWkMptqSQ3Za3FtR1iWuUxgrVTxH6V1eDSrEQuGhCPsNKxm2KMIIYZY5Ef0dJwOgsALaLij9HBPYqC71yN87B/J6adnMRux+FKat7Ul6HliuVmNyAk+pLRJuWrcVp92N0Ib4HR+nsvMcs1Yuu6s7/m1yD+oSjacp7NrZYxaQYHcZkx58i1riH+hcqWbvuiieaBI09l5Vbbu1e4K3r0s5MZSferGM1saY/3yucWKzJO0YJ4Ez7yQsKW0l0CPZJ5yw+HD2iv7qYou6WPiMiaVSiTPJ5k0y/p6gknklUUGHS1knndjLFRhMViowlw5iA44RWeyQIjNY/YCCXwvYOj2YUNikcA59PVtPAADbG3LfizQGQ9tlgGZjmWOP2LOuwJeJEArGhHkyfKFOezHKhE4IhoPEacnEqKYgl7c6QMp6IUfhjTwGWUhi+kPelofeywaNs380KMEiERiGuGXKEXBiFAXuarTuVZuTH0eqdrkshAXu4gbuV3YEw/4mdv+p5nmqRSDvJj5sznp5c2kFokTpsLycm+jQx3/nKKpD3aIR9U3cUimduxEj9G9w3LJ3VsCoxJ6yP5GZ9qtgclIdw6IYifWq01mz0YeX1fM06Jl0rvEjmcKr21qWCqx6C8a9J1E9+awG1zaww1abXA7gQAepDAdi154QPPCp5K8cMTQK3pyZgbonNLwOGaSMMQ4+N8ws6I2b+dJcNtae7C108tyXlUuDf5eJi59El5SL9p3xpH/vLyMRryciZvXH7eRB+O2QqwnW8H1xPImr128Og9ggeY90wTFvfDtTikb0fvUUZ0PJ5z+Q/d5FI95hA7wCD0Rj+IRj8q6qaXF91pUNgyQ1fhguxIQO6TNpVhz51RZ9J4j8IOV0dtProvm5bN98fzudbbpZ862D+nCg0dht/MePAqz4wJQOApTTIcyc6KjMEN0cLLA+xEo82gckBgHPmE4+tEINACZJHfGuYwhj4Uh8eMwBK8eBi8uGO2ufnt74fULKn1QUMVKfZUTbPnaqugXmGO5/VKtZC6295+BD8Qyu1GfQDztsexY9SQH1JM8lXpiPKLMmDPQjiwrC9d6KY24KnmjL2sIbvbiqWGEt1iIIEkORXhpGF8j9ATeiu0Dvne5hSPidRrVw5ziMeb0yTAff3cwjshfE+bB/ZhD/PnMgI+vvdmrBjxi9wJOGX1mwMc3tPg1A473g+ARw8eH/H8X8PHFZhfyViUvBsAH32v7zSkcMhvgzm14ll3/AkODX+ge9VK/2qT1hMhCOlm7udg6oS3XtdVF1e3ll9Ir9xVROwSYUjuKttCICT97+zCnjEVH3otNLzD1X+7tw8EY6afvxXYL82TXCqA9h/zriW4W7Al597V/GzXf/vMEffMP</diagram></mxfile>
|
|
||||||
BIN
docs/ci-cd.png
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
1
img/architecture/gitops.drawio
Normal file
BIN
img/architecture/gitops.png
Normal file
|
After Width: | Height: | Size: 483 KiB |
BIN
img/screenshot.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
8613
package-lock.json
generated
72
package.json
@@ -1,42 +1,66 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.1.0",
|
"version": "0.7.2",
|
||||||
|
"author": "undergroundwires",
|
||||||
|
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"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",
|
||||||
|
"electron:build": "vue-cli-service electron:build",
|
||||||
|
"electron:serve": "vue-cli-service electron:serve",
|
||||||
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
|
"lint:vue": "vue-cli-service lint --no-fix",
|
||||||
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
|
"postinstall": "electron-builder install-app-deps",
|
||||||
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
|
"main": "background.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.12.0",
|
"@fortawesome/free-brands-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.12.0",
|
"@fortawesome/free-regular-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/vue-fontawesome": "^0.1.9",
|
"@fortawesome/vue-fontawesome": "^0.1.10",
|
||||||
"ace-builds": "^1.4.7",
|
"ace-builds": "^1.4.12",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"inversify": "^5.0.1",
|
"inversify": "^5.0.1",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
"v-tooltip": "^2.0.2",
|
"v-tooltip": "2.0.2",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.12",
|
||||||
"vue-class-component": "^7.1.0",
|
"vue-class-component": "^7.2.5",
|
||||||
"vue-property-decorator": "^8.3.0"
|
"vue-js-modal": "^2.0.0-rc.6",
|
||||||
|
"vue-property-decorator": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.7",
|
"@types/ace": "0.0.43",
|
||||||
"@types/mocha": "^5.2.7",
|
"@types/chai": "^4.2.12",
|
||||||
"@types/ace": "0.0.42",
|
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@vue/cli-plugin-typescript": "^4.1.1",
|
"@types/mocha": "^8.0.3",
|
||||||
"@vue/cli-plugin-unit-mocha": "^4.1.1",
|
"@vue/cli-plugin-typescript": "^4.5.4",
|
||||||
"@vue/cli-service": "^4.1.1",
|
"@vue/cli-plugin-unit-mocha": "^4.5.4",
|
||||||
"@vue/test-utils": "1.0.0-beta.30",
|
"@vue/cli-service": "^4.5.4",
|
||||||
|
"@vue/test-utils": "1.0.4",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"sass": "^1.24.0",
|
"electron": "^10.1.0",
|
||||||
"sass-loader": "^8.0.0",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
|
"electron-log": "^4.2.4",
|
||||||
|
"electron-updater": "^4.3.4",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"typescript": "^3.7.4",
|
"markdownlint-cli": "^0.23.2",
|
||||||
"vue-template-compiler": "^2.6.11"
|
"remark-cli": "^8.0.1",
|
||||||
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
|
"remark-preset-lint-consistent": "^3.0.1",
|
||||||
|
"remark-validate-links": "^10.0.2",
|
||||||
|
"sass": "^1.26.10",
|
||||||
|
"sass-loader": "^10.0.1",
|
||||||
|
"typescript": "^4.0.2",
|
||||||
|
"vue-cli-plugin-electron-builder": "^2.0.0-rc.4",
|
||||||
|
"vue-template-compiler": "^2.6.12",
|
||||||
|
"yaml-lint": "^1.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -4,18 +4,32 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows</title>
|
||||||
|
<meta name="robots" content="index,follow" />
|
||||||
|
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
<title>Privacy is sexy 🍑🍆</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>
|
<style>
|
||||||
The page does not work without JavaScript enabled.
|
#javascriptDisabled {
|
||||||
Please enable it to continue.
|
background:#eceef1;
|
||||||
There's no shady stuff as 100% of the website is open source.
|
margin: 5rem auto;
|
||||||
</strong>
|
max-width: 800px;
|
||||||
|
font-size: 7px;
|
||||||
|
padding: 3rem;
|
||||||
|
border: 1px solid#333a45;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 150%;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="javascriptDisabled">
|
||||||
|
<h1>Problem loading page</h1>
|
||||||
|
<p>The page does not work without JavaScript enabled. Please enable it to use privacy.sexy. There's no shady stuff as 100% of the website is open source.</p>
|
||||||
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
48
src/App.vue
@@ -1,41 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<TheHeader
|
<TheHeader class="row" />
|
||||||
class="row"
|
<TheSearchBar class="row" />
|
||||||
github-url="https://github.com/undergroundwires/privacy.sexy"/>
|
<TheScripts class="row"/>
|
||||||
<!-- <TheSearchBar> </TheSearchBar> -->
|
<TheCodeArea class="row" theme="xcode" />
|
||||||
<!-- <div style="display: flex; justify-content: space-between;"> -->
|
<TheCodeButtons class="row code-buttons" />
|
||||||
<!-- <TheGrouper></TheGrouper> -->
|
<TheFooter />
|
||||||
<TheSelector class="row" />
|
|
||||||
<!-- </div> -->
|
|
||||||
<CardList />
|
|
||||||
<TheCodeArea class="row" theme="xcode"/>
|
|
||||||
<TheCodeButtons class="row" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import { ApplicationState, IApplicationState } from '@/application/State/ApplicationState';
|
import TheHeader from '@/presentation/TheHeader.vue';
|
||||||
import TheHeader from './presentation/TheHeader.vue';
|
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
||||||
import TheCodeArea from './presentation/TheCodeArea.vue';
|
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
||||||
import TheCodeButtons from './presentation/TheCodeButtons.vue';
|
import TheCodeButtons from '@/presentation/TheCodeButtons.vue';
|
||||||
import TheSearchBar from './presentation/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
||||||
import TheSelector from './presentation/Scripts/Selector/TheSelector.vue';
|
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
|
||||||
import TheGrouper from './presentation/Scripts/TheGrouper.vue';
|
|
||||||
import CardList from './presentation/Scripts/Cards/CardList.vue';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
TheHeader,
|
TheHeader,
|
||||||
TheCodeArea,
|
TheCodeArea,
|
||||||
TheCodeButtons,
|
TheCodeButtons,
|
||||||
|
TheScripts,
|
||||||
TheSearchBar,
|
TheSearchBar,
|
||||||
TheGrouper,
|
TheFooter,
|
||||||
CardList,
|
|
||||||
TheSelector,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class App extends Vue {
|
export default class App extends Vue {
|
||||||
@@ -53,7 +45,7 @@ export default class App extends Vue {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
background: $light-gray;
|
background: $light-gray;
|
||||||
font-family: 'Slabo 27px', serif;
|
font-family: $main-font;
|
||||||
color: $slate;
|
color: $slate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +66,10 @@ body {
|
|||||||
.row {
|
.row {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-buttons {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
import { Category } from '../domain/Category';
|
|
||||||
import { Application } from '../domain/Application';
|
|
||||||
import { Script } from '@/domain/Script';
|
|
||||||
// import applicationFile from 'js-yaml-loader!@/application/application.yaml';
|
|
||||||
// import applicationFile from 'json-loader!yaml-loader!@/application/application.yaml';
|
|
||||||
import applicationFile, { YamlCategory, YamlScript, YamlDocumentable } from 'js-yaml-loader!./application.yaml';
|
|
||||||
// import test from './test-loader!./test.txt';
|
|
||||||
|
|
||||||
interface ApplicationResult {
|
|
||||||
readonly application: Application;
|
|
||||||
readonly selectedScripts: Script[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ApplicationParser {
|
|
||||||
|
|
||||||
public static buildApplication(): ApplicationResult {
|
|
||||||
const name = applicationFile.name as string;
|
|
||||||
const version = applicationFile.version as number;
|
|
||||||
const categories = new Array<Category>();
|
|
||||||
const selectedScripts = new Array<Script>();
|
|
||||||
if (!applicationFile.actions || applicationFile.actions.length <= 0) {
|
|
||||||
throw new Error('Application does not define any action');
|
|
||||||
}
|
|
||||||
for (const action of applicationFile.actions) {
|
|
||||||
const category = ApplicationParser.parseCategory(action, selectedScripts);
|
|
||||||
categories.push(category);
|
|
||||||
}
|
|
||||||
const app = new Application(name, version, categories);
|
|
||||||
return {application: app, selectedScripts};
|
|
||||||
}
|
|
||||||
private static categoryIdCounter = 0;
|
|
||||||
|
|
||||||
private static parseCategory(category: YamlCategory, selectedScripts: Script[]): Category {
|
|
||||||
if (!category.children || category.children.length <= 0) {
|
|
||||||
throw Error('Category has no children');
|
|
||||||
}
|
|
||||||
const subCategories = new Array<Category>();
|
|
||||||
const subScripts = new Array<Script>();
|
|
||||||
for (const categoryOrScript of category.children) {
|
|
||||||
if (ApplicationParser.isCategory(categoryOrScript)) {
|
|
||||||
const subCategory = ApplicationParser.parseCategory(categoryOrScript as YamlCategory, selectedScripts);
|
|
||||||
subCategories.push(subCategory);
|
|
||||||
} else if (ApplicationParser.isScript(categoryOrScript)) {
|
|
||||||
const yamlScript = categoryOrScript as YamlScript;
|
|
||||||
const script = new Script(
|
|
||||||
/* name */ yamlScript.name,
|
|
||||||
/* code */ yamlScript.code,
|
|
||||||
/* docs */ this.parseDocUrls(yamlScript));
|
|
||||||
subScripts.push(script);
|
|
||||||
if (yamlScript.default === true) {
|
|
||||||
selectedScripts.push(script);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`Child element is neither a category or a script.
|
|
||||||
Parent: ${category.category}, element: ${categoryOrScript}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Category(
|
|
||||||
/*id*/ ApplicationParser.categoryIdCounter++,
|
|
||||||
/*name*/ category.category,
|
|
||||||
/*docs*/ this.parseDocUrls(category),
|
|
||||||
/*categories*/ subCategories,
|
|
||||||
/*scripts*/ subScripts,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> {
|
|
||||||
if (!documentable.docs) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const docs = documentable.docs;
|
|
||||||
const result = new Array<string>();
|
|
||||||
const addDoc = (doc: string) => {
|
|
||||||
if (!doc) {
|
|
||||||
throw new Error('Documentiton url is null or empty');
|
|
||||||
}
|
|
||||||
if (doc.includes('\n')) {
|
|
||||||
throw new Error('Documentation url cannot be multi-lined.');
|
|
||||||
}
|
|
||||||
result.push(doc);
|
|
||||||
};
|
|
||||||
if (docs instanceof Array) {
|
|
||||||
for (const doc of docs) {
|
|
||||||
if (typeof doc !== 'string') {
|
|
||||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
|
||||||
}
|
|
||||||
addDoc(doc as string);
|
|
||||||
}
|
|
||||||
} else if (typeof docs === 'string') {
|
|
||||||
addDoc(docs as string);
|
|
||||||
} else {
|
|
||||||
throw new Error('Docs field (documentation url) must a string or array of strings');
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isScript(categoryOrScript: any): boolean {
|
|
||||||
return categoryOrScript.code && categoryOrScript.code.length > 0;
|
|
||||||
}
|
|
||||||
private static isCategory(categoryOrScript: any): boolean {
|
|
||||||
return categoryOrScript.category && categoryOrScript.category.length > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
54
src/application/Environment/BrowserOs/BrowserOsDetector.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { OperatingSystem } from '../OperatingSystem';
|
||||||
|
import { DetectorBuilder } from './DetectorBuilder';
|
||||||
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
|
||||||
|
export class BrowserOsDetector implements IBrowserOsDetector {
|
||||||
|
private readonly detectors = BrowserDetectors;
|
||||||
|
public detect(userAgent: string): OperatingSystem {
|
||||||
|
if (!userAgent) {
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
for (const detector of this.detectors) {
|
||||||
|
const os = detector.detect(userAgent);
|
||||||
|
if (os !== OperatingSystem.Unknown) {
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
||||||
|
const BrowserDetectors =
|
||||||
|
[
|
||||||
|
define(OperatingSystem.KaiOS, (b) =>
|
||||||
|
b.mustInclude('KAIOS')),
|
||||||
|
define(OperatingSystem.ChromeOS, (b) =>
|
||||||
|
b.mustInclude('CrOS')),
|
||||||
|
define(OperatingSystem.BlackBerryOS, (b) =>
|
||||||
|
b.mustInclude('BlackBerry')),
|
||||||
|
define(OperatingSystem.BlackBerryTabletOS, (b) =>
|
||||||
|
b.mustInclude('RIM Tablet OS')),
|
||||||
|
define(OperatingSystem.BlackBerry, (b) =>
|
||||||
|
b.mustInclude('BB10')),
|
||||||
|
define(OperatingSystem.Android, (b) =>
|
||||||
|
b.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.Android, (b) =>
|
||||||
|
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.iOS, (b) =>
|
||||||
|
b.mustInclude('like Mac OS X')),
|
||||||
|
define(OperatingSystem.Linux, (b) =>
|
||||||
|
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||||
|
define(OperatingSystem.Windows, (b) =>
|
||||||
|
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.WindowsPhone, (b) =>
|
||||||
|
b.mustInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.macOS, (b) =>
|
||||||
|
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||||
|
];
|
||||||
|
|
||||||
|
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
|
||||||
|
const builder = new DetectorBuilder(os);
|
||||||
|
applyRules(builder);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
53
src/application/Environment/BrowserOs/DetectorBuilder.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
import { OperatingSystem } from '../OperatingSystem';
|
||||||
|
|
||||||
|
export class DetectorBuilder {
|
||||||
|
private readonly existingPartsInUserAgent = new Array<string>();
|
||||||
|
private readonly notExistingPartsInUserAgent = new Array<string>();
|
||||||
|
|
||||||
|
constructor(private readonly os: OperatingSystem) { }
|
||||||
|
|
||||||
|
public mustInclude(str: string): DetectorBuilder {
|
||||||
|
return this.add(str, this.existingPartsInUserAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public mustNotInclude(str: string): DetectorBuilder {
|
||||||
|
return this.add(str, this.notExistingPartsInUserAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): IBrowserOsDetector {
|
||||||
|
if (!this.existingPartsInUserAgent.length) {
|
||||||
|
throw new Error('Must include at least a part');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
detect: (agent) => this.detect(agent),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private detect(userAgent: string): OperatingSystem {
|
||||||
|
if (!userAgent) {
|
||||||
|
throw new Error('User agent is null or undefined');
|
||||||
|
}
|
||||||
|
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
return this.os;
|
||||||
|
}
|
||||||
|
|
||||||
|
private add(part: string, array: string[]): DetectorBuilder {
|
||||||
|
if (!part) {
|
||||||
|
throw new Error('part is empty or undefined');
|
||||||
|
}
|
||||||
|
if (this.existingPartsInUserAgent.includes(part)) {
|
||||||
|
throw new Error(`part ${part} is already included as existing part`);
|
||||||
|
}
|
||||||
|
if (this.notExistingPartsInUserAgent.includes(part)) {
|
||||||
|
throw new Error(`part ${part} is already included as not existing part`);
|
||||||
|
}
|
||||||
|
array.push(part);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { OperatingSystem } from '../OperatingSystem';
|
||||||
|
|
||||||
|
export interface IBrowserOsDetector {
|
||||||
|
detect(userAgent: string): OperatingSystem;
|
||||||
|
}
|
||||||
80
src/application/Environment/Environment.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
|
import { IEnvironment } from './IEnvironment';
|
||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
|
interface IEnvironmentVariables {
|
||||||
|
readonly window: Window & typeof globalThis;
|
||||||
|
readonly process: NodeJS.Process;
|
||||||
|
readonly navigator: Navigator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Environment implements IEnvironment {
|
||||||
|
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
||||||
|
window,
|
||||||
|
process,
|
||||||
|
navigator,
|
||||||
|
});
|
||||||
|
public readonly isDesktop: boolean;
|
||||||
|
public readonly os: OperatingSystem;
|
||||||
|
protected constructor(
|
||||||
|
variables: IEnvironmentVariables,
|
||||||
|
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
|
||||||
|
if (!variables) {
|
||||||
|
throw new Error('variables is null or empty');
|
||||||
|
}
|
||||||
|
this.isDesktop = isDesktop(variables);
|
||||||
|
this.os = this.isDesktop ?
|
||||||
|
getDesktopOsType(getProcessPlatform(variables))
|
||||||
|
: browserOsDetector.detect(getUserAgent(variables));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserAgent(variables: IEnvironmentVariables): string {
|
||||||
|
if (!variables.window || !variables.window.navigator) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return variables.window.navigator.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
||||||
|
if (!variables.process || !variables.process.platform) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return variables.process.platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDesktopOsType(processPlatform: string): OperatingSystem {
|
||||||
|
// https://nodejs.org/api/process.html#process_process_platform
|
||||||
|
if (processPlatform === 'darwin') {
|
||||||
|
return OperatingSystem.macOS;
|
||||||
|
} else if (processPlatform === 'win32') {
|
||||||
|
return OperatingSystem.Windows;
|
||||||
|
} else if (processPlatform === 'linux') {
|
||||||
|
return OperatingSystem.Linux;
|
||||||
|
}
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||||
|
// More: https://github.com/electron/electron/issues/2288
|
||||||
|
// Renderer process
|
||||||
|
if (variables.window
|
||||||
|
&& variables.window.process
|
||||||
|
&& variables.window.process.type === 'renderer') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Main process
|
||||||
|
if (variables.process
|
||||||
|
&& variables.process.versions
|
||||||
|
&& Boolean(variables.process.versions.electron)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||||
|
if (variables.navigator
|
||||||
|
&& variables.navigator.userAgent
|
||||||
|
&& variables.navigator.userAgent.includes('Electron')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
6
src/application/Environment/IEnvironment.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
|
export interface IEnvironment {
|
||||||
|
isDesktop: boolean;
|
||||||
|
os: OperatingSystem;
|
||||||
|
}
|
||||||
14
src/application/Environment/OperatingSystem.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export enum OperatingSystem {
|
||||||
|
macOS,
|
||||||
|
Windows,
|
||||||
|
Linux,
|
||||||
|
KaiOS,
|
||||||
|
ChromeOS,
|
||||||
|
BlackBerryOS,
|
||||||
|
BlackBerry,
|
||||||
|
BlackBerryTabletOS,
|
||||||
|
Android,
|
||||||
|
iOS,
|
||||||
|
WindowsPhone,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
29
src/application/Parser/ApplicationParser.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Category } from '@/domain/Category';
|
||||||
|
import { Application } from '@/domain/Application';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
|
||||||
|
import { parseCategory } from './CategoryParser';
|
||||||
|
|
||||||
|
export function parseApplication(content: ApplicationYaml): IApplication {
|
||||||
|
validate(content);
|
||||||
|
const categories = new Array<Category>();
|
||||||
|
for (const action of content.actions) {
|
||||||
|
const category = parseCategory(action);
|
||||||
|
categories.push(category);
|
||||||
|
}
|
||||||
|
const app = new Application(
|
||||||
|
content.name,
|
||||||
|
content.repositoryUrl,
|
||||||
|
process.env.VUE_APP_VERSION,
|
||||||
|
categories);
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(content: ApplicationYaml): void {
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('application is null or undefined');
|
||||||
|
}
|
||||||
|
if (!content.actions || content.actions.length <= 0) {
|
||||||
|
throw new Error('application does not define any action');
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/application/Parser/CategoryParser.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { YamlCategory, YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
|
import { Script } from '@/domain/Script';
|
||||||
|
import { Category } from '@/domain/Category';
|
||||||
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
|
import { parseScript } from './ScriptParser';
|
||||||
|
|
||||||
|
let categoryIdCounter: number = 0;
|
||||||
|
|
||||||
|
interface ICategoryChildren {
|
||||||
|
subCategories: Category[];
|
||||||
|
subScripts: Script[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCategory(category: YamlCategory): Category {
|
||||||
|
ensureValid(category);
|
||||||
|
const children: ICategoryChildren = {
|
||||||
|
subCategories: new Array<Category>(),
|
||||||
|
subScripts: new Array<Script>(),
|
||||||
|
};
|
||||||
|
for (const categoryOrScript of category.children) {
|
||||||
|
parseCategoryChild(categoryOrScript, children, category);
|
||||||
|
}
|
||||||
|
return new Category(
|
||||||
|
/*id*/ categoryIdCounter++,
|
||||||
|
/*name*/ category.category,
|
||||||
|
/*docs*/ parseDocUrls(category),
|
||||||
|
/*categories*/ children.subCategories,
|
||||||
|
/*scripts*/ children.subScripts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValid(category: YamlCategory) {
|
||||||
|
if (!category) {
|
||||||
|
throw Error('category is null or undefined');
|
||||||
|
}
|
||||||
|
if (!category.children || category.children.length === 0) {
|
||||||
|
throw Error('category has no children');
|
||||||
|
}
|
||||||
|
if (!category.category || category.category.length === 0) {
|
||||||
|
throw Error('category has no name');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCategoryChild(
|
||||||
|
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) {
|
||||||
|
if (isCategory(categoryOrScript)) {
|
||||||
|
const subCategory = parseCategory(categoryOrScript as YamlCategory);
|
||||||
|
children.subCategories.push(subCategory);
|
||||||
|
} else if (isScript(categoryOrScript)) {
|
||||||
|
const yamlScript = categoryOrScript as YamlScript;
|
||||||
|
const script = parseScript(yamlScript);
|
||||||
|
children.subScripts.push(script);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Child element is neither a category or a script.
|
||||||
|
Parent: ${parent.category}, element: ${JSON.stringify(categoryOrScript)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isScript(categoryOrScript: any): boolean {
|
||||||
|
return categoryOrScript.code && categoryOrScript.code.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCategory(categoryOrScript: any): boolean {
|
||||||
|
return categoryOrScript.category && categoryOrScript.category.length > 0;
|
||||||
|
}
|
||||||
61
src/application/Parser/DocumentationParser.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { YamlDocumentable, DocumentationUrls } from 'js-yaml-loader!./application.yaml';
|
||||||
|
|
||||||
|
export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> {
|
||||||
|
if (!documentable) {
|
||||||
|
throw new Error('documentable is null or undefined');
|
||||||
|
}
|
||||||
|
const docs = documentable.docs;
|
||||||
|
if (!docs || !docs.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let result = new DocumentationUrlContainer();
|
||||||
|
result = addDocs(docs, result);
|
||||||
|
return result.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDocs(docs: DocumentationUrls, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
||||||
|
if (docs instanceof Array) {
|
||||||
|
urls.addUrls(docs);
|
||||||
|
} else if (typeof docs === 'string') {
|
||||||
|
urls.addUrl(docs);
|
||||||
|
} else {
|
||||||
|
throw new Error('Docs field (documentation url) must a string or array of strings');
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DocumentationUrlContainer {
|
||||||
|
private readonly urls = new Array<string>();
|
||||||
|
|
||||||
|
public addUrl(url: string) {
|
||||||
|
validateUrl(url);
|
||||||
|
this.urls.push(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addUrls(urls: any[]) {
|
||||||
|
for (const url of urls) {
|
||||||
|
if (typeof url !== 'string') {
|
||||||
|
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||||
|
}
|
||||||
|
this.addUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAll(): ReadonlyArray<string> {
|
||||||
|
return this.urls;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateUrl(docUrl: string): void {
|
||||||
|
if (!docUrl) {
|
||||||
|
throw new Error('Documentation url is null or empty');
|
||||||
|
}
|
||||||
|
if (docUrl.includes('\n')) {
|
||||||
|
throw new Error('Documentation url cannot be multi-lined.');
|
||||||
|
}
|
||||||
|
const res = docUrl.match(
|
||||||
|
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g);
|
||||||
|
if (res == null) {
|
||||||
|
throw new Error(`Invalid documentation url: ${docUrl}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/application/Parser/ScriptParser.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Script } from '@/domain/Script';
|
||||||
|
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
|
|
||||||
|
export function parseScript(yamlScript: YamlScript): Script {
|
||||||
|
if (!yamlScript) {
|
||||||
|
throw new Error('script is null or undefined');
|
||||||
|
}
|
||||||
|
const script = new Script(
|
||||||
|
/* name */ yamlScript.name,
|
||||||
|
/* code */ yamlScript.code,
|
||||||
|
/* revertCode */ yamlScript.revertCode,
|
||||||
|
/* docs */ parseDocUrls(yamlScript),
|
||||||
|
/* isRecommended */ yamlScript.recommend);
|
||||||
|
return script;
|
||||||
|
}
|
||||||
@@ -3,26 +3,28 @@ import { IUserFilter } from './Filter/IUserFilter';
|
|||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
import { UserSelection } from './Selection/UserSelection';
|
import { UserSelection } from './Selection/UserSelection';
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
import { IUserSelection } from './Selection/IUserSelection';
|
||||||
import { AsyncLazy } from '../../infrastructure/Threading/AsyncLazy';
|
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||||
import { Signal } from '../../infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
import { ICategory } from '../../domain/ICategory';
|
import { parseApplication } from '../Parser/ApplicationParser';
|
||||||
import { ApplicationParser } from '../ApplicationParser';
|
|
||||||
import { IApplicationState } from './IApplicationState';
|
import { IApplicationState } from './IApplicationState';
|
||||||
import { Script } from '../../domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Application } from '../../domain/Application';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
|
||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
|
||||||
/** Mutatable singleton application state that's the single source of truth throughout the application */
|
/** Mutatable singleton application state that's the single source of truth throughout the application */
|
||||||
export class ApplicationState implements IApplicationState {
|
export class ApplicationState implements IApplicationState {
|
||||||
/** Get singleton application state */
|
/** Get singleton application state */
|
||||||
public static GetAsync(): Promise<IApplicationState> {
|
public static GetAsync(): Promise<IApplicationState> {
|
||||||
return ApplicationState.instance.getValueAsync();
|
return ApplicationState.instance.getValueAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Application instance with all scripts. */
|
/** Application instance with all scripts. */
|
||||||
private static instance = new AsyncLazy<IApplicationState>(() => {
|
private static instance = new AsyncLazy<IApplicationState>(() => {
|
||||||
const app = ApplicationParser.buildApplication();
|
const application = parseApplication(applicationFile);
|
||||||
const state = new ApplicationState(app.application, app.selectedScripts);
|
const selectedScripts = new Array<Script>();
|
||||||
|
const state = new ApplicationState(application, selectedScripts);
|
||||||
return Promise.resolve(state);
|
return Promise.resolve(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,33 +35,11 @@ export class ApplicationState implements IApplicationState {
|
|||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
/** Inner instance of the all scripts */
|
/** Inner instance of the all scripts */
|
||||||
private readonly app: Application,
|
public readonly app: IApplication,
|
||||||
/** Initially selected scripts */
|
/** Initially selected scripts */
|
||||||
public readonly defaultScripts: Script[]) {
|
public readonly defaultScripts: Script[]) {
|
||||||
this.selection = new UserSelection(app, defaultScripts);
|
this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false)));
|
||||||
this.code = new ApplicationCode(this.selection, app.version.toString());
|
this.code = new ApplicationCode(this.selection, app.version);
|
||||||
this.filter = new UserFilter(app);
|
this.filter = new UserFilter(app);
|
||||||
}
|
|
||||||
|
|
||||||
public getCategory(categoryId: number): ICategory | undefined {
|
|
||||||
return this.app.findCategory(categoryId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get categories(): ReadonlyArray<ICategory> {
|
|
||||||
return this.app.categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get appName(): string {
|
|
||||||
return this.app.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get appVersion(): number {
|
|
||||||
return this.app.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get appTotalScripts(): number {
|
|
||||||
return this.app.totalScripts;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { IApplicationState, IUserFilter };
|
|
||||||
|
|||||||
@@ -1,27 +1,38 @@
|
|||||||
import { CodeBuilder } from './CodeBuilder';
|
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
import { IUserSelection } from './../Selection/IUserSelection';
|
import { CodePosition } from './Position/CodePosition';
|
||||||
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
||||||
|
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
import { IApplicationCode } from './IApplicationCode';
|
import { IApplicationCode } from './IApplicationCode';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||||
|
|
||||||
export class ApplicationCode implements IApplicationCode {
|
export class ApplicationCode implements IApplicationCode {
|
||||||
public readonly changed = new Signal<string>();
|
public readonly changed = new Signal<ICodeChangedEvent>();
|
||||||
public current: string;
|
public current: string;
|
||||||
|
|
||||||
private readonly codeBuilder: CodeBuilder;
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
|
|
||||||
constructor(userSelection: IUserSelection, private readonly version: string) {
|
constructor(
|
||||||
|
userSelection: IUserSelection,
|
||||||
|
private readonly version: string,
|
||||||
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
||||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||||
if (!version) { throw new Error('version is null or undefined'); }
|
if (!version) { throw new Error('version is null or undefined'); }
|
||||||
this.codeBuilder = new CodeBuilder();
|
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||||
this.setCode(userSelection.selectedScripts);
|
this.setCode(userSelection.selectedScripts);
|
||||||
userSelection.changed.on((scripts) => {
|
userSelection.changed.on((scripts) => {
|
||||||
this.setCode(scripts);
|
this.setCode(scripts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCode(scripts: ReadonlyArray<IScript>) {
|
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||||
this.current = this.codeBuilder.buildCode(scripts, this.version);
|
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||||
this.changed.notify(this.current);
|
const code = this.generator.buildCode(scripts, this.version);
|
||||||
|
this.current = code.code;
|
||||||
|
this.scriptPositions = code.scriptPositions;
|
||||||
|
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||||
|
this.changed.notify(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { AdminRightsFunctionRenderer } from './Renderer/AdminRightsFunctionRenderer';
|
|
||||||
import { AsciiArtRenderer } from './Renderer/AsciiArtRenderer';
|
|
||||||
import { FunctionRenderer } from './Renderer/FunctionRenderer';
|
|
||||||
import { Script } from '@/domain/Script';
|
|
||||||
|
|
||||||
export class CodeBuilder {
|
|
||||||
private readonly functionRenderer: FunctionRenderer;
|
|
||||||
private readonly adminRightsFunctionRenderer: AdminRightsFunctionRenderer;
|
|
||||||
private readonly asciiArtRenderer: AsciiArtRenderer;
|
|
||||||
|
|
||||||
public constructor() {
|
|
||||||
this.functionRenderer = new FunctionRenderer();
|
|
||||||
this.adminRightsFunctionRenderer = new AdminRightsFunctionRenderer();
|
|
||||||
this.asciiArtRenderer = new AsciiArtRenderer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public buildCode(scripts: ReadonlyArray<Script>, version: string): string {
|
|
||||||
if (!scripts) { throw new Error('scripts is undefined'); }
|
|
||||||
if (!version) { throw new Error('version is undefined'); }
|
|
||||||
return `@echo off\n\n${this.asciiArtRenderer.renderAsciiArt(version)}\n\n`
|
|
||||||
+ `${this.adminRightsFunctionRenderer.renderAdminRightsFunction()}\n\n`
|
|
||||||
+ scripts.map((script) => this.functionRenderer.renderFunction(script.name, script.code)).join('\n\n')
|
|
||||||
+ '\n\n'
|
|
||||||
+ 'pause\n'
|
|
||||||
+ 'exit /b 0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
64
src/application/State/Code/Event/CodeChangedEvent.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
|
public readonly code: string;
|
||||||
|
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
|
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
|
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
|
private readonly scripts: Map<IScript, ICodePosition>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
code: string,
|
||||||
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
scripts: Map<SelectedScript, ICodePosition>) {
|
||||||
|
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
||||||
|
this.code = code;
|
||||||
|
const newScripts = Array.from(scripts.keys());
|
||||||
|
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
||||||
|
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
||||||
|
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
||||||
|
this.scripts = new Map<IScript, ICodePosition>();
|
||||||
|
scripts.forEach((position, selection) => {
|
||||||
|
this.scripts.set(selection.script, position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
return this.scripts.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||||
|
return this.scripts.get(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||||
|
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||||
|
for (const position of positions) {
|
||||||
|
if (position.endLine > totalLines) {
|
||||||
|
throw new Error(`script end line (${position.endLine}) is out of range.` +
|
||||||
|
`(total code lines: ${totalLines}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChangedScripts(
|
||||||
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
|
||||||
|
return newScripts
|
||||||
|
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||||
|
&& oldScript.revert !== newScript.revert ))
|
||||||
|
.map((selection) => selection.script);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectIfNotExists(
|
||||||
|
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||||
|
test: ReadonlyArray<SelectedScript>) {
|
||||||
|
return selectableContainer
|
||||||
|
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||||
|
.map((selection) => selection.script);
|
||||||
|
}
|
||||||
11
src/application/State/Code/Event/ICodeChangedEvent.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
|
export interface ICodeChangedEvent {
|
||||||
|
readonly code: string;
|
||||||
|
addedScripts: ReadonlyArray<IScript>;
|
||||||
|
removedScripts: ReadonlyArray<IScript>;
|
||||||
|
changedScripts: ReadonlyArray<IScript>;
|
||||||
|
isEmpty(): boolean;
|
||||||
|
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||||
|
}
|
||||||
63
src/application/State/Code/Generation/CodeBuilder.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const NewLine = '\n';
|
||||||
|
const TotalFunctionSeparatorChars = 58;
|
||||||
|
|
||||||
|
export class CodeBuilder {
|
||||||
|
private readonly lines = new Array<string>();
|
||||||
|
|
||||||
|
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
||||||
|
public get currentLine(): number {
|
||||||
|
return this.lines.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendLine(code?: string): CodeBuilder {
|
||||||
|
if (!code) {
|
||||||
|
this.lines.push('');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
|
for (const line of lines) {
|
||||||
|
this.lines.push(line);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendTrailingHyphensCommentLine(
|
||||||
|
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
||||||
|
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendCommentLine(commentLine?: string): CodeBuilder {
|
||||||
|
this.lines.push(`:: ${commentLine}`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendFunction(name: string, code: string): CodeBuilder {
|
||||||
|
if (!name) { throw new Error('name cannot be empty or null'); }
|
||||||
|
if (!code) { throw new Error('code cannot be empty or null'); }
|
||||||
|
return this
|
||||||
|
.appendLine()
|
||||||
|
.appendCommentLineWithHyphensAround(name)
|
||||||
|
.appendLine(`echo --- ${name}`)
|
||||||
|
.appendLine(code)
|
||||||
|
.appendTrailingHyphensCommentLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendCommentLineWithHyphensAround(
|
||||||
|
sectionName: string,
|
||||||
|
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
||||||
|
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
|
||||||
|
if (sectionName.length >= totalRepeatHyphens) {
|
||||||
|
return this.appendCommentLine(sectionName);
|
||||||
|
}
|
||||||
|
const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2));
|
||||||
|
const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2));
|
||||||
|
return this
|
||||||
|
.appendTrailingHyphensCommentLine()
|
||||||
|
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
|
||||||
|
.appendTrailingHyphensCommentLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString(): string {
|
||||||
|
return this.lines.join(NewLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/application/State/Code/Generation/IUserScript.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
|
export interface IUserScript {
|
||||||
|
code: string;
|
||||||
|
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
|
export interface IUserScriptGenerator {
|
||||||
|
buildCode(
|
||||||
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
version: string): IUserScript;
|
||||||
|
}
|
||||||
68
src/application/State/Code/Generation/UserScriptGenerator.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
|
import { CodeBuilder } from './CodeBuilder';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
|
|
||||||
|
export const adminRightsScript = {
|
||||||
|
name: 'Ensure admin privileges',
|
||||||
|
code: 'fltmc >nul 2>&1 || (\n' +
|
||||||
|
' echo Administrator privileges are required.\n' +
|
||||||
|
' PowerShell Start -Verb RunAs \'%0\' 2> nul || (\n' +
|
||||||
|
' echo Right-click on the script and select "Run as administrator".\n' +
|
||||||
|
' pause & exit 1\n' +
|
||||||
|
' )\n' +
|
||||||
|
' exit 0\n' +
|
||||||
|
')',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||||
|
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): IUserScript {
|
||||||
|
if (!selectedScripts) { throw new Error('scripts is undefined'); }
|
||||||
|
if (!version) { throw new Error('version is undefined'); }
|
||||||
|
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||||
|
if (!selectedScripts.length) {
|
||||||
|
return { code: '', scriptPositions };
|
||||||
|
}
|
||||||
|
const builder = initializeCode(version);
|
||||||
|
for (const selection of selectedScripts) {
|
||||||
|
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||||
|
}
|
||||||
|
const code = finalizeCode(builder);
|
||||||
|
return { code, scriptPositions };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeCode(version: string): CodeBuilder {
|
||||||
|
return new CodeBuilder()
|
||||||
|
.appendLine('@echo off')
|
||||||
|
.appendCommentLine(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)
|
||||||
|
.appendFunction(adminRightsScript.name, adminRightsScript.code)
|
||||||
|
.appendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeCode(builder: CodeBuilder): string {
|
||||||
|
return builder.appendLine()
|
||||||
|
.appendLine('pause')
|
||||||
|
.appendLine('exit /b 0')
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSelection(
|
||||||
|
selection: SelectedScript,
|
||||||
|
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||||
|
builder: CodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||||
|
const startPosition = builder.currentLine + 1;
|
||||||
|
appendCode(selection, builder);
|
||||||
|
const endPosition = builder.currentLine - 1;
|
||||||
|
builder.appendLine();
|
||||||
|
scriptPositions.set(selection, new CodePosition(startPosition, endPosition));
|
||||||
|
return scriptPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendCode(selection: SelectedScript, builder: CodeBuilder) {
|
||||||
|
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||||
|
const scriptCode = selection.revert ? selection.script.revertCode : selection.script.code;
|
||||||
|
builder.appendFunction(name, scriptCode);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
|
|
||||||
export interface IApplicationCode {
|
export interface IApplicationCode {
|
||||||
readonly changed: ISignal<string>;
|
readonly changed: ISignal<ICodeChangedEvent>;
|
||||||
readonly current: string;
|
readonly current: string;
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/application/State/Code/Position/CodePosition.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { ICodePosition } from './ICodePosition';
|
||||||
|
export class CodePosition implements ICodePosition {
|
||||||
|
|
||||||
|
public get totalLines(): number {
|
||||||
|
return this.endLine - this.startLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly startLine: number,
|
||||||
|
public readonly endLine: number) {
|
||||||
|
if (startLine < 0) {
|
||||||
|
throw new Error('Code cannot start in a negative line');
|
||||||
|
}
|
||||||
|
if (endLine < 0) {
|
||||||
|
throw new Error('Code cannot end in a negative line');
|
||||||
|
}
|
||||||
|
if (endLine === startLine) {
|
||||||
|
throw new Error('Empty code');
|
||||||
|
}
|
||||||
|
if (endLine < startLine) {
|
||||||
|
throw new Error('End line cannot be less than start line');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/application/State/Code/Position/ICodePosition.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ICodePosition {
|
||||||
|
readonly startLine: number;
|
||||||
|
readonly endLine: number;
|
||||||
|
readonly totalLines: number;
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { FunctionRenderer } from './FunctionRenderer';
|
|
||||||
|
|
||||||
export class AdminRightsFunctionRenderer {
|
|
||||||
private readonly functionRenderer: FunctionRenderer;
|
|
||||||
constructor() {
|
|
||||||
this.functionRenderer = new FunctionRenderer();
|
|
||||||
}
|
|
||||||
public renderAdminRightsFunction() {
|
|
||||||
const name = 'Ensure admin priviliges';
|
|
||||||
const code = 'fltmc >nul 2>&1 || (\n' +
|
|
||||||
' echo This batch script requires administrator privileges. Right-click on\n' +
|
|
||||||
' echo the script and select "Run as administrator".\n' +
|
|
||||||
' pause\n' +
|
|
||||||
' exit 1\n' +
|
|
||||||
')';
|
|
||||||
return this.functionRenderer.renderFunction(name, code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { CodeRenderer } from './CodeRenderer';
|
|
||||||
|
|
||||||
export class AsciiArtRenderer extends CodeRenderer {
|
|
||||||
public renderAsciiArt(version: string): string {
|
|
||||||
if (!version) { throw new Error('Version is not defined'); }
|
|
||||||
return (
|
|
||||||
'██████╗ ██████╗ ██╗██╗ ██╗ █████╗ ██████╗██╗ ██╗███████╗███████╗██╗ ██╗██╗ ██╗\n' +
|
|
||||||
'██╔══██╗██╔══██╗██║██║ ██║██╔══██╗██╔════╝╚██╗ ██╔╝██╔════╝██╔════╝╚██╗██╔╝╚██╗ ██╔╝\n' +
|
|
||||||
'██████╔╝██████╔╝██║██║ ██║███████║██║ ╚████╔╝ ███████╗█████╗ ╚███╔╝ ╚████╔╝ \n' +
|
|
||||||
'██╔═══╝ ██╔══██╗██║╚██╗ ██╔╝██╔══██║██║ ╚██╔╝ ╚════██║██╔══╝ ██╔██╗ ╚██╔╝ \n' +
|
|
||||||
'██║ ██║ ██║██║ ╚████╔╝ ██║ ██║╚██████╗ ██║██╗███████║███████╗██╔╝ ██╗ ██║ \n' +
|
|
||||||
'╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ')
|
|
||||||
.split('\n').map((line) => this.renderComment(line)).join('\n')
|
|
||||||
+ `\n${this.renderComment(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
export abstract class CodeRenderer {
|
|
||||||
|
|
||||||
protected readonly totalFunctionSeparatorChars = 58;
|
|
||||||
|
|
||||||
protected readonly trailingHyphens = '-'.repeat(this.totalFunctionSeparatorChars);
|
|
||||||
|
|
||||||
protected renderComment(line?: string): string {
|
|
||||||
return line ? `:: ${line}` : ':: ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { CodeRenderer } from './CodeRenderer';
|
|
||||||
|
|
||||||
export class FunctionRenderer extends CodeRenderer {
|
|
||||||
public renderFunction(name: string, code: string) {
|
|
||||||
if (!name) { throw new Error('name cannot be empty or null'); }
|
|
||||||
if (!code) { throw new Error('code cannot be empty or null'); }
|
|
||||||
return this.renderFunctionStartComment(name) + '\n'
|
|
||||||
+ `echo --- ${name}` + '\n'
|
|
||||||
+ code + '\n'
|
|
||||||
+ this.renderFunctionEndComment();
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderFunctionStartComment(functionName: string): string {
|
|
||||||
if (functionName.length >= this.totalFunctionSeparatorChars) {
|
|
||||||
return this.renderComment(functionName);
|
|
||||||
}
|
|
||||||
return this.renderComment(this.trailingHyphens) + '\n' +
|
|
||||||
this.renderFunctionName(functionName) + '\n' +
|
|
||||||
this.renderComment(this.trailingHyphens);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderFunctionName(functionName: string) {
|
|
||||||
const autoFirstHypens = '-'.repeat(Math.floor((this.totalFunctionSeparatorChars - functionName.length) / 2));
|
|
||||||
const secondHypens = '-'.repeat(Math.ceil((this.totalFunctionSeparatorChars - functionName.length) / 2));
|
|
||||||
return `${this.renderComment()}${autoFirstHypens}${functionName}${secondHypens}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderFunctionEndComment(): string {
|
|
||||||
return this.renderComment(this.trailingHyphens);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
src/application/State/Filter/FilterResult.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { IFilterResult } from './IFilterResult';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { IScript, ICategory } from '@/domain/ICategory';
|
import { IScript, ICategory } from '@/domain/ICategory';
|
||||||
|
|
||||||
export interface IFilterMatches {
|
export interface IFilterResult {
|
||||||
readonly scriptMatches: ReadonlyArray<IScript>;
|
|
||||||
readonly categoryMatches: ReadonlyArray<ICategory>;
|
readonly categoryMatches: ReadonlyArray<ICategory>;
|
||||||
|
readonly scriptMatches: ReadonlyArray<IScript>;
|
||||||
readonly query: string;
|
readonly query: string;
|
||||||
|
hasAnyMatches(): boolean;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { IFilterMatches } from './IFilterMatches';
|
import { IFilterResult } from './IFilterResult';
|
||||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
import { ISignal } from '@/infrastructure/Events/Signal';
|
||||||
|
|
||||||
export interface IUserFilter {
|
export interface IUserFilter {
|
||||||
readonly filtered: ISignal<IFilterMatches>;
|
readonly filtered: ISignal<IFilterResult>;
|
||||||
readonly filterRemoved: ISignal<void>;
|
readonly filterRemoved: ISignal<void>;
|
||||||
setFilter(filter: string): void;
|
setFilter(filter: string): void;
|
||||||
removeFilter(): void;
|
removeFilter(): void;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { IFilterMatches } from './IFilterMatches';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { Application } from '../../../domain/Application';
|
import { FilterResult } from './FilterResult';
|
||||||
|
import { IFilterResult } from './IFilterResult';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IUserFilter } from './IUserFilter';
|
import { IUserFilter } from './IUserFilter';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
|
|
||||||
export class UserFilter implements IUserFilter {
|
export class UserFilter implements IUserFilter {
|
||||||
public readonly filtered = new Signal<IFilterMatches>();
|
public readonly filtered = new Signal<IFilterResult>();
|
||||||
public readonly filterRemoved = new Signal<void>();
|
public readonly filterRemoved = new Signal<void>();
|
||||||
|
|
||||||
constructor(private application: Application) {
|
constructor(private application: IApplication) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,15 +17,17 @@ export class UserFilter implements IUserFilter {
|
|||||||
if (!filter) {
|
if (!filter) {
|
||||||
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the 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(
|
const filteredScripts = this.application.getAllScripts().filter(
|
||||||
(script) => script.name.toLowerCase().includes(filter.toLowerCase())
|
(script) => isScriptAMatch(script, filterLowercase));
|
||||||
|| script.code.toLowerCase().includes(filter.toLowerCase()));
|
const filteredCategories = this.application.getAllCategories().filter(
|
||||||
|
(category) => category.name.toLowerCase().includes(filterLowercase));
|
||||||
|
|
||||||
const matches: IFilterMatches = {
|
const matches = new FilterResult(
|
||||||
scriptMatches: filteredScripts,
|
filteredScripts,
|
||||||
categoryMatches: null,
|
filteredCategories,
|
||||||
query: filter,
|
filter,
|
||||||
};
|
);
|
||||||
|
|
||||||
this.filtered.notify(matches);
|
this.filtered.notify(matches);
|
||||||
}
|
}
|
||||||
@@ -32,3 +36,16 @@ export class UserFilter implements IUserFilter {
|
|||||||
this.filterRemoved.notify();
|
this.filterRemoved.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isScriptAMatch(script: IScript, filterLowercase: string) {
|
||||||
|
if (script.name.toLowerCase().includes(filterLowercase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (script.code.toLowerCase().includes(filterLowercase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (script.revertCode) {
|
||||||
|
return script.revertCode.toLowerCase().includes(filterLowercase);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { IApplication } from './../../domain/IApplication';
|
||||||
import { IUserFilter } from './Filter/IUserFilter';
|
import { IUserFilter } from './Filter/IUserFilter';
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
import { IUserSelection } from './Selection/IUserSelection';
|
||||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
import { ICategory, IScript } from '@/domain/ICategory';
|
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
export { IUserSelection, IApplicationCode, IUserFilter };
|
export { IUserSelection, IApplicationCode, IUserFilter };
|
||||||
|
|
||||||
@@ -10,12 +10,6 @@ export interface IApplicationState {
|
|||||||
readonly code: IApplicationCode;
|
readonly code: IApplicationCode;
|
||||||
readonly filter: IUserFilter;
|
readonly filter: IUserFilter;
|
||||||
readonly stateChanged: ISignal<IApplicationState>;
|
readonly stateChanged: ISignal<IApplicationState>;
|
||||||
readonly categories: ReadonlyArray<ICategory>;
|
|
||||||
readonly appName: string;
|
|
||||||
readonly appVersion: number;
|
|
||||||
readonly appTotalScripts: number;
|
|
||||||
readonly selection: IUserSelection;
|
readonly selection: IUserSelection;
|
||||||
readonly defaultScripts: ReadonlyArray<IScript>;
|
readonly app: IApplication;
|
||||||
getCategory(categoryId: number): ICategory | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
import { ISignal } from '@/infrastructure/Events/Signal';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
export interface IUserSelection {
|
export interface IUserSelection {
|
||||||
readonly changed: ISignal<ReadonlyArray<IScript>>;
|
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
||||||
readonly selectedScripts: ReadonlyArray<IScript>;
|
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||||
readonly totalSelected: number;
|
readonly totalSelected: number;
|
||||||
addSelectedScript(scriptId: string): void;
|
removeAllInCategory(categoryId: number): void;
|
||||||
|
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||||
|
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
|
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
removeSelectedScript(scriptId: string): void;
|
removeSelectedScript(scriptId: string): void;
|
||||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||||
isSelected(script: IScript): boolean;
|
isSelected(scriptId: string): boolean;
|
||||||
selectAll(): void;
|
selectAll(): void;
|
||||||
deselectAll(): void;
|
deselectAll(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/application/State/Selection/SelectedScript.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
|
export class SelectedScript extends BaseEntity<string> {
|
||||||
|
constructor(
|
||||||
|
public readonly script: IScript,
|
||||||
|
public readonly revert: boolean,
|
||||||
|
) {
|
||||||
|
super(script.id);
|
||||||
|
if (revert && !script.canRevert()) {
|
||||||
|
throw new Error('cannot revert an irreversible script');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IUserSelection } from './IUserSelection';
|
import { IUserSelection } from './IUserSelection';
|
||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
import { IScript } from '@/domain/Script';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
|
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||||
|
|
||||||
export class UserSelection implements IUserSelection {
|
export class UserSelection implements IUserSelection {
|
||||||
public readonly changed = new Signal<ReadonlyArray<IScript>>();
|
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
|
||||||
|
private readonly scripts: IRepository<string, SelectedScript>;
|
||||||
private readonly scripts = new InMemoryRepository<string, IScript>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly app: IApplication,
|
private readonly app: IApplication,
|
||||||
/** Initially selected scripts */
|
selectedScripts: ReadonlyArray<SelectedScript>) {
|
||||||
selectedScripts: ReadonlyArray<IScript>) {
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
if (selectedScripts && selectedScripts.length > 0) {
|
if (selectedScripts && selectedScripts.length > 0) {
|
||||||
for (const script of selectedScripts) {
|
for (const script of selectedScripts) {
|
||||||
this.scripts.addItem(script);
|
this.scripts.addItem(script);
|
||||||
@@ -20,28 +21,64 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add a script to users application */
|
public removeAllInCategory(categoryId: number): void {
|
||||||
public addSelectedScript(scriptId: string): void {
|
const category = this.app.findCategory(categoryId);
|
||||||
|
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||||
|
.filter((script) => this.scripts.exists(script.id));
|
||||||
|
if (!scriptsToRemove.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const script of scriptsToRemove) {
|
||||||
|
this.scripts.removeItem(script.id);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
||||||
|
const category = this.app.findCategory(categoryId);
|
||||||
|
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
||||||
|
.filter((script) =>
|
||||||
|
!this.scripts.exists(script.id)
|
||||||
|
|| this.scripts.getById(script.id).revert !== revert,
|
||||||
|
);
|
||||||
|
if (!scriptsToAddOrUpdate.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const script of scriptsToAddOrUpdate) {
|
||||||
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
const script = this.app.findScript(scriptId);
|
const script = this.app.findScript(scriptId);
|
||||||
if (!script) {
|
if (!script) {
|
||||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
||||||
}
|
}
|
||||||
this.scripts.addItem(script);
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
|
this.scripts.addItem(selectedScript);
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
|
const script = this.app.findScript(scriptId);
|
||||||
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove a script from users application */
|
|
||||||
public removeSelectedScript(scriptId: string): void {
|
public removeSelectedScript(scriptId: string): void {
|
||||||
this.scripts.removeItem(scriptId);
|
this.scripts.removeItem(scriptId);
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSelected(script: IScript): boolean {
|
public isSelected(scriptId: string): boolean {
|
||||||
return this.scripts.exists(script);
|
return this.scripts.exists(scriptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get users scripts based on his/her selections */
|
/** Get users scripts based on his/her selections */
|
||||||
public get selectedScripts(): ReadonlyArray<IScript> {
|
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
||||||
return this.scripts.getItems();
|
return this.scripts.getItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +88,9 @@ export class UserSelection implements IUserSelection {
|
|||||||
|
|
||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
for (const script of this.app.getAllScripts()) {
|
for (const script of this.app.getAllScripts()) {
|
||||||
if (!this.scripts.exists(script)) {
|
if (!this.scripts.exists(script.id)) {
|
||||||
this.scripts.addItem(script);
|
const selection = new SelectedScript(script, false);
|
||||||
|
this.scripts.addItem(selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
@@ -78,9 +116,11 @@ export class UserSelection implements IUserSelection {
|
|||||||
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
||||||
}
|
}
|
||||||
// Select from unselected scripts
|
// Select from unselected scripts
|
||||||
scripts
|
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
|
||||||
.filter((script) => !this.scripts.exists(script))
|
for (const toSelect of unselectedScripts) {
|
||||||
.forEach((script) => this.scripts.addItem(script));
|
const selection = new SelectedScript(toSelect, false);
|
||||||
|
this.scripts.addItem(selection);
|
||||||
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/application/application.yaml.d.ts
vendored
@@ -1,23 +1,29 @@
|
|||||||
declare module 'js-yaml-loader!*' {
|
declare module 'js-yaml-loader!*' {
|
||||||
type CategoryOrScript = YamlCategory | YamlScript;
|
export type CategoryOrScript = YamlCategory | YamlScript;
|
||||||
type DocumentationUrls = ReadonlyArray<string> | string;
|
export type DocumentationUrls = ReadonlyArray<string> | string;
|
||||||
|
|
||||||
export interface YamlDocumentable {
|
export interface YamlDocumentable {
|
||||||
docs?: DocumentationUrls;
|
docs?: DocumentationUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YamlScript extends YamlDocumentable {
|
export interface YamlScript extends YamlDocumentable {
|
||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
default: boolean;
|
revertCode: string;
|
||||||
|
recommend: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YamlCategory extends YamlDocumentable {
|
export interface YamlCategory extends YamlDocumentable {
|
||||||
children: ReadonlyArray<CategoryOrScript>;
|
children: ReadonlyArray<CategoryOrScript>;
|
||||||
category: string;
|
category: string;
|
||||||
}
|
}
|
||||||
interface ApplicationYaml {
|
|
||||||
|
export interface ApplicationYaml {
|
||||||
name: string;
|
name: string;
|
||||||
version: number;
|
repositoryUrl: string;
|
||||||
actions: ReadonlyArray<YamlCategory>;
|
actions: ReadonlyArray<YamlCategory>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content: ApplicationYaml;
|
const content: ApplicationYaml;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|||||||
133
src/background.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { app, protocol, BrowserWindow, shell } from 'electron';
|
||||||
|
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
||||||
|
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
|
||||||
|
import path from 'path';
|
||||||
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
|
||||||
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
|
declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172
|
||||||
|
|
||||||
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
|
let win: BrowserWindow | null;
|
||||||
|
|
||||||
|
// Scheme must be registered before the app is ready
|
||||||
|
protocol.registerSchemesAsPrivileged([
|
||||||
|
{ scheme: 'app', privileges: { secure: true, standard: true } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Setup logging
|
||||||
|
autoUpdater.logger = log; // https://www.electron.build/auto-update#debugging
|
||||||
|
log.transports.file.level = 'silly';
|
||||||
|
if (!process.env.IS_TEST) {
|
||||||
|
Object.assign(console, log.functions); // override console.log, console.warn etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
// Create the browser window.
|
||||||
|
win = new BrowserWindow({
|
||||||
|
width: 1350,
|
||||||
|
height: 955,
|
||||||
|
webPreferences: {
|
||||||
|
// Use pluginOptions.nodeIntegration, leave this alone
|
||||||
|
// See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
|
||||||
|
nodeIntegration: (process.env
|
||||||
|
.ELECTRON_NODE_INTEGRATION as unknown) as boolean,
|
||||||
|
},
|
||||||
|
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#set-tray-icon
|
||||||
|
icon: path.join(__static, 'icon.png'),
|
||||||
|
});
|
||||||
|
|
||||||
|
win.setMenuBarVisibility(false);
|
||||||
|
configureExternalsUrlsOpenBrowser(win);
|
||||||
|
loadApplication(win);
|
||||||
|
|
||||||
|
win.on('closed', () => {
|
||||||
|
win = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit when all windows are closed.
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
// On macOS it is common for applications and their menu bar
|
||||||
|
// to stay active until the user quits explicitly with Cmd + Q
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
// On macOS it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
if (win === null) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This method will be called when Electron has finished
|
||||||
|
// initialization and is ready to create browser windows.
|
||||||
|
// Some APIs can only be used after this event occurs.
|
||||||
|
app.on('ready', async () => {
|
||||||
|
if (isDevelopment && !process.env.IS_TEST) {
|
||||||
|
// Install Vue Devtools
|
||||||
|
try {
|
||||||
|
await installExtension(VUEJS_DEVTOOLS);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Vue Devtools failed to install:', e.toString()); // tslint:disable-line:no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
// See electron-builder issue "checkForUpdatesAndNotify updates but does not notify on Windows 10"
|
||||||
|
// https://github.com/electron-userland/electron-builder/issues/2700
|
||||||
|
// https://github.com/electron/electron/issues/10864
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/shell/appid#how-to-form-an-application-defined-appusermodelid
|
||||||
|
app.setAppUserModelId('Undergroundwires.PrivacySexy');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit cleanly on request from parent process in development mode.
|
||||||
|
if (isDevelopment) {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
process.on('message', (data) => {
|
||||||
|
if (data === 'graceful-exit') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadApplication(window: BrowserWindow) {
|
||||||
|
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||||
|
// Load the url of the dev server if in development mode
|
||||||
|
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string);
|
||||||
|
if (!process.env.IS_TEST) {
|
||||||
|
win.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
createProtocol('app');
|
||||||
|
// Load the index.html when not in development
|
||||||
|
win.loadURL('app://./index.html');
|
||||||
|
// tslint:disable-next-line:max-line-length
|
||||||
|
autoUpdater.checkForUpdatesAndNotify(); // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#check-for-updates-in-background-js-ts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
|
||||||
|
window.webContents.on('new-window', (event, url) => { // handle redirect
|
||||||
|
if (url !== win.webContents.getURL()) {
|
||||||
|
event.preventDefault();
|
||||||
|
shell.openExternal(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,115 +4,113 @@ import { IScript } from './IScript';
|
|||||||
import { IApplication } from './IApplication';
|
import { IApplication } from './IApplication';
|
||||||
|
|
||||||
export class Application implements IApplication {
|
export class Application implements IApplication {
|
||||||
private static mustHaveCategories(categories: ReadonlyArray<ICategory>) {
|
public get totalScripts(): number { return this.flattened.allScripts.length; }
|
||||||
if (!categories || categories.length === 0) {
|
public get totalCategories(): number { return this.flattened.allCategories.length; }
|
||||||
throw new Error('an application must consist of at least one category');
|
|
||||||
}
|
private readonly flattened: IFlattenedApplication;
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Checks all categories against duplicates, throws exception if it find any duplicates
|
|
||||||
* @return {number} Total unique categories
|
|
||||||
*/
|
|
||||||
/** Checks all categories against duplicates, throws exception if it find any duplicates returns total categories */
|
|
||||||
private static mustNotHaveDuplicatedCategories(categories: ReadonlyArray<ICategory>): number {
|
|
||||||
return Application.ensureNoDuplicateEntities(categories, Application.visitAllCategoriesOnce);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Checks all scripts against duplicates, throws exception if it find any scripts duplicates total scripts.
|
|
||||||
* @return {number} Total unique scripts
|
|
||||||
*/
|
|
||||||
private static mustNotHaveDuplicatedScripts(categories: ReadonlyArray<ICategory>): number {
|
|
||||||
return Application.ensureNoDuplicateEntities(categories, Application.visitAllScriptsOnce);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Checks entities against duplicates using a visit function, throws exception if it find any duplicates.
|
|
||||||
* @return {number} Result from the visit function
|
|
||||||
*/
|
|
||||||
private static ensureNoDuplicateEntities<TKey>(
|
|
||||||
categories: ReadonlyArray<ICategory>,
|
|
||||||
visitFunction: (categories: ReadonlyArray<ICategory>,
|
|
||||||
handler: (entity: IEntity<TKey>) => any) => number): number {
|
|
||||||
const totalOccurencesById = new Map<TKey, number>();
|
|
||||||
const totalVisited = visitFunction(categories,
|
|
||||||
(entity) =>
|
|
||||||
totalOccurencesById.set(entity.id,
|
|
||||||
(totalOccurencesById.get(entity.id) || 0) + 1));
|
|
||||||
const duplicatedIds = new Array<TKey>();
|
|
||||||
totalOccurencesById.forEach((count, id) => {
|
|
||||||
if (count > 1) {
|
|
||||||
duplicatedIds.push(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (duplicatedIds.length > 0) {
|
|
||||||
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
|
||||||
throw new Error(
|
|
||||||
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`);
|
|
||||||
}
|
|
||||||
return totalVisited;
|
|
||||||
}
|
|
||||||
// Runs handler on each category and returns sum of total visited categories
|
|
||||||
private static visitAllCategoriesOnce(
|
|
||||||
categories: ReadonlyArray<ICategory>, handler: (category: ICategory) => any): number {
|
|
||||||
let total = 0;
|
|
||||||
for (const category of categories) {
|
|
||||||
handler(category);
|
|
||||||
total++;
|
|
||||||
if (category.subCategories && category.subCategories.length > 0) {
|
|
||||||
total += Application.visitAllCategoriesOnce(
|
|
||||||
category.subCategories as ReadonlyArray<ICategory>, handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
// Runs handler on each script and returns sum of total visited scripts
|
|
||||||
private static visitAllScriptsOnce(
|
|
||||||
categories: ReadonlyArray<ICategory>, handler: (script: IScript) => any): number {
|
|
||||||
let total = 0;
|
|
||||||
Application.visitAllCategoriesOnce(categories, (category) => {
|
|
||||||
if (category.scripts) {
|
|
||||||
for (const script of category.scripts) {
|
|
||||||
handler(script);
|
|
||||||
total++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
public readonly totalScripts: number;
|
|
||||||
public readonly totalCategories: number;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly version: number,
|
public readonly repositoryUrl: string,
|
||||||
public readonly categories: ReadonlyArray<ICategory>) {
|
public readonly version: string,
|
||||||
Application.mustHaveCategories(categories);
|
public readonly actions: ReadonlyArray<ICategory>) {
|
||||||
this.totalCategories = Application.mustNotHaveDuplicatedCategories(categories);
|
if (!name) { throw Error('Application has no name'); }
|
||||||
this.totalScripts = Application.mustNotHaveDuplicatedScripts(categories);
|
if (!repositoryUrl) { throw Error('Application has no repository url'); }
|
||||||
|
if (!version) { throw Error('Version cannot be empty'); }
|
||||||
|
this.flattened = flatten(actions);
|
||||||
|
ensureValid(this.flattened);
|
||||||
|
ensureNoDuplicates(this.flattened.allCategories);
|
||||||
|
ensureNoDuplicates(this.flattened.allScripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findCategory(categoryId: number): ICategory | undefined {
|
public findCategory(categoryId: number): ICategory | undefined {
|
||||||
let result: ICategory | undefined;
|
return this.flattened.allCategories.find((category) => category.id === categoryId);
|
||||||
Application.visitAllCategoriesOnce(this.categories, (category) => {
|
|
||||||
if (category.id === categoryId) {
|
|
||||||
result = category;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getRecommendedScripts(): readonly IScript[] {
|
||||||
|
return this.flattened.allScripts.filter((script) => script.isRecommended);
|
||||||
|
}
|
||||||
|
|
||||||
public findScript(scriptId: string): IScript | undefined {
|
public findScript(scriptId: string): IScript | undefined {
|
||||||
let result: IScript | undefined;
|
return this.flattened.allScripts.find((script) => script.id === scriptId);
|
||||||
Application.visitAllScriptsOnce(this.categories, (script) => {
|
|
||||||
if (script.id === scriptId) {
|
|
||||||
result = script;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllScripts(): IScript[] {
|
public getAllScripts(): IScript[] {
|
||||||
const result = new Array<IScript>();
|
return this.flattened.allScripts;
|
||||||
Application.visitAllScriptsOnce(this.categories, (script) => {
|
}
|
||||||
result.push(script);
|
|
||||||
});
|
public getAllCategories(): ICategory[] {
|
||||||
return result;
|
return this.flattened.allCategories;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||||
|
const totalOccurencesById = new Map<TKey, number>();
|
||||||
|
for (const entity of entities) {
|
||||||
|
totalOccurencesById.set(entity.id, (totalOccurencesById.get(entity.id) || 0) + 1);
|
||||||
|
}
|
||||||
|
const duplicatedIds = new Array<TKey>();
|
||||||
|
totalOccurencesById.forEach((index, id) => {
|
||||||
|
if (index > 1) {
|
||||||
|
duplicatedIds.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (duplicatedIds.length > 0) {
|
||||||
|
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||||
|
throw new Error(
|
||||||
|
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFlattenedApplication {
|
||||||
|
allCategories: ICategory[];
|
||||||
|
allScripts: IScript[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValid(application: IFlattenedApplication) {
|
||||||
|
if (!application.allCategories || application.allCategories.length === 0) {
|
||||||
|
throw new Error('Application must consist of at least one category');
|
||||||
|
}
|
||||||
|
if (!application.allScripts || application.allScripts.length === 0) {
|
||||||
|
throw new Error('Application must consist of at least one script');
|
||||||
|
}
|
||||||
|
if (application.allScripts.filter((script) => script.isRecommended).length === 0) {
|
||||||
|
throw new Error('Application must consist of at least one recommended script');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenCategories(
|
||||||
|
categories: ReadonlyArray<ICategory>,
|
||||||
|
flattened: IFlattenedApplication): IFlattenedApplication {
|
||||||
|
if (!categories || categories.length === 0) {
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
for (const category of categories) {
|
||||||
|
flattened.allCategories.push(category);
|
||||||
|
flattened = flattenScripts(category.scripts, flattened);
|
||||||
|
flattened = flattenCategories(category.subCategories, flattened);
|
||||||
|
}
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenScripts(
|
||||||
|
scripts: ReadonlyArray<IScript>,
|
||||||
|
flattened: IFlattenedApplication): IFlattenedApplication {
|
||||||
|
if (!scripts) {
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
for (const script of scripts) {
|
||||||
|
flattened.allScripts.push(script);
|
||||||
|
}
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flatten(
|
||||||
|
categories: ReadonlyArray<ICategory>): IFlattenedApplication {
|
||||||
|
let flattened: IFlattenedApplication = {
|
||||||
|
allCategories: new Array<ICategory>(),
|
||||||
|
allScripts: new Array<IScript>(),
|
||||||
|
};
|
||||||
|
flattened = flattenCategories(categories, flattened);
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,15 +3,7 @@ import { IScript } from './IScript';
|
|||||||
import { ICategory } from './ICategory';
|
import { ICategory } from './ICategory';
|
||||||
|
|
||||||
export class Category extends BaseEntity<number> implements ICategory {
|
export class Category extends BaseEntity<number> implements ICategory {
|
||||||
private static validate(category: ICategory) {
|
private allSubScripts: ReadonlyArray<IScript> = undefined;
|
||||||
if (!category.name) {
|
|
||||||
throw new Error('name is null or empty');
|
|
||||||
}
|
|
||||||
if ((!category.subCategories || category.subCategories.length === 0)
|
|
||||||
&& (!category.scripts || category.scripts.length === 0)) {
|
|
||||||
throw new Error('A category must have at least one sub-category or scripts');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: number,
|
id: number,
|
||||||
@@ -20,6 +12,27 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||||
public readonly scripts?: ReadonlyArray<IScript>) {
|
public readonly scripts?: ReadonlyArray<IScript>) {
|
||||||
super(id);
|
super(id);
|
||||||
Category.validate(this);
|
validateCategory(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllScriptsRecursively(): readonly IScript[] {
|
||||||
|
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
||||||
|
return [
|
||||||
|
...category.scripts,
|
||||||
|
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCategory(category: ICategory) {
|
||||||
|
if (!category.name) {
|
||||||
|
throw new Error('undefined or empty name');
|
||||||
|
}
|
||||||
|
if ((!category.subCategories || category.subCategories.length === 0) &&
|
||||||
|
(!category.scripts || category.scripts.length === 0)) {
|
||||||
|
throw new Error('A category must have at least one sub-category or script');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ import { IScript } from '@/domain/IScript';
|
|||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
|
||||||
export interface IApplication {
|
export interface IApplication {
|
||||||
readonly categories: ReadonlyArray<ICategory>;
|
readonly name: string;
|
||||||
|
readonly repositoryUrl: string;
|
||||||
|
readonly version: string;
|
||||||
|
readonly totalScripts: number;
|
||||||
|
readonly totalCategories: number;
|
||||||
|
readonly actions: ReadonlyArray<ICategory>;
|
||||||
|
|
||||||
|
getRecommendedScripts(): ReadonlyArray<IScript>;
|
||||||
findCategory(categoryId: number): ICategory | undefined;
|
findCategory(categoryId: number): ICategory | undefined;
|
||||||
findScript(scriptId: string): IScript | undefined;
|
findScript(scriptId: string): IScript | undefined;
|
||||||
getAllScripts(): ReadonlyArray<IScript>;
|
getAllScripts(): ReadonlyArray<IScript>;
|
||||||
|
getAllCategories(): ReadonlyArray<ICategory>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { IScript } from '@/domain/IScript';
|
export { IScript } from '@/domain/IScript';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ICategory extends IEntity<number>, IDocumentable {
|
|||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
readonly subCategories?: ReadonlyArray<ICategory>;
|
||||||
readonly scripts?: ReadonlyArray<IScript>;
|
readonly scripts?: ReadonlyArray<IScript>;
|
||||||
|
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { IEntity } from '../infrastructure/Entity/IEntity';
|
export { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { IEntity } from './../infrastructure/Entity/IEntity';
|
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
import { IDocumentable } from './IDocumentable';
|
import { IDocumentable } from './IDocumentable';
|
||||||
|
|
||||||
export interface IScript extends IEntity<string>, IDocumentable {
|
export interface IScript extends IEntity<string>, IDocumentable {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly code: string;
|
readonly isRecommended: boolean;
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly documentationUrls: ReadonlyArray<string>;
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode: string;
|
||||||
|
canRevert(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,50 +2,59 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
|||||||
import { IScript } from './IScript';
|
import { IScript } from './IScript';
|
||||||
|
|
||||||
export class Script extends BaseEntity<string> implements IScript {
|
export class Script extends BaseEntity<string> implements IScript {
|
||||||
private static ensureNoEmptyLines(name: string, code: string): void {
|
constructor(
|
||||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
public readonly name: string,
|
||||||
throw Error(`Script has empty lines "${name}"`);
|
public readonly code: string,
|
||||||
}
|
public readonly revertCode: string,
|
||||||
}
|
public readonly documentationUrls: ReadonlyArray<string>,
|
||||||
|
public readonly isRecommended: boolean) {
|
||||||
private static ensureCodeHasUniqueLines(name: string, code: string): void {
|
|
||||||
const lines = code.split('\n');
|
|
||||||
if (lines.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const checkForDuplicates = (line: string) => {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (trimmed.length === 1 && trimmed === ')' || trimmed === '(') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
const duplicateLines = new Array<string>();
|
|
||||||
const uniqueLines = new Set<string>();
|
|
||||||
let validatedLineCount = 0;
|
|
||||||
for (const line of lines) {
|
|
||||||
if (!checkForDuplicates(line)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
uniqueLines.add(line);
|
|
||||||
if (uniqueLines.size !== validatedLineCount + 1) {
|
|
||||||
duplicateLines.push(line);
|
|
||||||
}
|
|
||||||
validatedLineCount++;
|
|
||||||
}
|
|
||||||
if (duplicateLines.length !== 0) {
|
|
||||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(public name: string, public code: string, public documentationUrls: ReadonlyArray<string>) {
|
|
||||||
super(name);
|
super(name);
|
||||||
if (code == null || code.length === 0) {
|
validateCode(name, code);
|
||||||
throw new Error('Code is empty or null');
|
if (revertCode) {
|
||||||
|
validateCode(name, revertCode);
|
||||||
|
if (code === revertCode) {
|
||||||
|
throw new Error(`${name}: Code itself and its reverting code cannot be the same`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Script.ensureCodeHasUniqueLines(name, code);
|
}
|
||||||
Script.ensureNoEmptyLines(name, code);
|
public canRevert(): boolean {
|
||||||
|
return Boolean(this.revertCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { IScript } from './IScript';
|
function validateCode(name: string, code: string): void {
|
||||||
|
if (!code || code.length === 0) {
|
||||||
|
throw new Error(`Code of ${name} is empty or null`);
|
||||||
|
}
|
||||||
|
ensureCodeHasUniqueLines(name, code);
|
||||||
|
ensureNoEmptyLines(name, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoEmptyLines(name: string, code: string): void {
|
||||||
|
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||||
|
throw Error(`Script has empty lines "${name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mayBeUniqueLine(codeLine: string): boolean {
|
||||||
|
const trimmed = codeLine.trim();
|
||||||
|
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (codeLine.startsWith(':: ') || codeLine.startsWith('REM ')) { // Is comment?
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCodeHasUniqueLines(name: string, code: string): void {
|
||||||
|
const lines = code.split('\n')
|
||||||
|
.filter((line) => mayBeUniqueLine(line));
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||||
|
if (duplicateLines.length !== 0) {
|
||||||
|
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
59
src/global.d.ts
vendored
@@ -1,59 +0,0 @@
|
|||||||
|
|
||||||
// Two ways of typing other libraries: https://stackoverflow.com/a/53070501
|
|
||||||
|
|
||||||
declare module 'liquor-tree' {
|
|
||||||
import { PluginObject } from 'vue';
|
|
||||||
import { VueClass } from 'vue-class-component/lib/declarations';
|
|
||||||
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Tree.js
|
|
||||||
export interface ILiquorTree {
|
|
||||||
readonly model: ReadonlyArray<ILiquorTreeExistingNode>;
|
|
||||||
filter(query: string): void;
|
|
||||||
clearFilter(): void;
|
|
||||||
setModel(nodes: ReadonlyArray<ILiquorTreeNewNode>): void;
|
|
||||||
}
|
|
||||||
interface ICustomLiquorTreeData {
|
|
||||||
documentationUrls: ReadonlyArray<string>;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Returned from Node tree view events.
|
|
||||||
* See constructor in https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
|
||||||
*/
|
|
||||||
export interface ILiquorTreeExistingNode {
|
|
||||||
id: string;
|
|
||||||
data: ILiquorTreeNodeData;
|
|
||||||
states: ILiquorTreeNodeState | undefined;
|
|
||||||
children: ReadonlyArray<ILiquorTreeExistingNode> | undefined;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Sent to liquor tree to define of new nodes.
|
|
||||||
* https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
|
||||||
*/
|
|
||||||
export interface ILiquorTreeNewNode {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
state: ILiquorTreeNodeState | undefined;
|
|
||||||
children: ReadonlyArray<ILiquorTreeNewNode> | undefined;
|
|
||||||
data: ICustomLiquorTreeData;
|
|
||||||
}
|
|
||||||
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
|
||||||
interface ILiquorTreeNodeState {
|
|
||||||
checked: boolean;
|
|
||||||
}
|
|
||||||
interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
|
|
||||||
interface ILiquorTreeOptions {
|
|
||||||
checkbox: boolean;
|
|
||||||
checkOnSelect: boolean;
|
|
||||||
filter: ILiquorTreeFilter;
|
|
||||||
deletion(node: ILiquorTreeNewNode): boolean;
|
|
||||||
}
|
|
||||||
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
|
|
||||||
interface ILiquorTreeFilter {
|
|
||||||
emptyText: string;
|
|
||||||
matcher(query: string, node: ILiquorTreeNewNode): boolean;
|
|
||||||
}
|
|
||||||
const LiquorTree: PluginObject<any> & VueClass<any>;
|
|
||||||
export default LiquorTree;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { IEntity } from './IEntity';
|
import { IEntity } from './IEntity';
|
||||||
|
|
||||||
export abstract class BaseEntity<TId> implements IEntity<TId> {
|
export abstract class BaseEntity<TId> implements IEntity<TId> {
|
||||||
constructor(public id: TId) {
|
protected constructor(public id: TId) {
|
||||||
if (typeof id !== 'number' && !id) {
|
if (typeof id !== 'number' && !id) {
|
||||||
throw new Error('Id cannot be null or empty');
|
throw new Error('Id cannot be null or empty');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public equals(otherId: TId): boolean {
|
public equals(otherId: TId): boolean {
|
||||||
return this.id === otherId;
|
return this.id === otherId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { IEntity } from '../Entity/IEntity';
|
|||||||
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
|
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||||
readonly length: number;
|
readonly length: number;
|
||||||
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
|
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
|
||||||
|
getById(id: TKey): TEntity | undefined;
|
||||||
addItem(item: TEntity): void;
|
addItem(item: TEntity): void;
|
||||||
|
addOrUpdateItem(item: TEntity): void;
|
||||||
removeItem(id: TKey): void;
|
removeItem(id: TKey): void;
|
||||||
exists(item: TEntity): boolean;
|
exists(id: TKey): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||