Full Code of anthonybudd/s3-from-scratch for AI

master ed3b50a45e32 cached
171 files
224.5 KB
62.9k tokens
24 symbols
1 requests
Download .txt
Showing preview only (261K chars total). Download the full file or copy to clipboard to get everything.
Repository: anthonybudd/s3-from-scratch
Branch: master
Commit: ed3b50a45e32
Files: 171
Total size: 224.5 KB

Directory structure:
gitextract_fbxgv27m/

├── .gitignore
├── ReadMe.md
├── ansible/
│   ├── .gitignore
│   ├── .yamllint
│   ├── README.md
│   ├── ansible.cfg
│   ├── inventory/
│   │   ├── .gitignore
│   │   └── example/
│   │       ├── group_vars/
│   │       │   └── all.yml
│   │       └── hosts.ini
│   ├── reset.yml
│   ├── roles/
│   │   ├── download/
│   │   │   └── tasks/
│   │   │       └── main.yml
│   │   ├── k3s/
│   │   │   ├── master/
│   │   │   │   ├── tasks/
│   │   │   │   │   └── main.yml
│   │   │   │   └── templates/
│   │   │   │       └── k3s.service.j2
│   │   │   └── node/
│   │   │       ├── tasks/
│   │   │       │   └── main.yml
│   │   │       └── templates/
│   │   │           └── k3s.service.j2
│   │   ├── prereq/
│   │   │   └── tasks/
│   │   │       └── main.yml
│   │   ├── raspberrypi/
│   │   │   ├── handlers/
│   │   │   │   └── main.yml
│   │   │   └── tasks/
│   │   │       ├── main.yml
│   │   │       └── prereq/
│   │   │           ├── CentOS.yml
│   │   │           ├── Raspbian.yml
│   │   │           ├── Ubuntu.yml
│   │   │           └── default.yml
│   │   └── reset/
│   │       └── tasks/
│   │           ├── main.yml
│   │           └── umount_with_children.yml
│   └── site.yml
├── api/
│   ├── .dockerignore
│   ├── .eslintignore
│   ├── .gitignore
│   ├── .gitlab-ci.yml
│   ├── .sequelizerc
│   ├── Dockerfile
│   ├── LICENSE
│   ├── ReadMe.md
│   ├── docker-compose.yml
│   ├── k8s/
│   │   ├── Deploy.md
│   │   ├── api.deployment.yml
│   │   ├── api.ingress.yml
│   │   ├── api.service.yml
│   │   ├── api.ssl.ingress.yml
│   │   ├── db.yml
│   │   ├── prod.clusterissuer.yml
│   │   ├── secrets.example.yml
│   │   └── sync.job.yml
│   ├── package.json
│   ├── postman.json
│   ├── requests.http
│   ├── src/
│   │   ├── database/
│   │   │   ├── migrations/
│   │   │   │   ├── 20180726090304-create-Users.js
│   │   │   │   ├── 20180726090404-create-Groups.js
│   │   │   │   ├── 20180726090405-create-GroupsUsers.js
│   │   │   │   ├── 20240411041313-create-Buckets.js
│   │   │   │   └── 20240430101608-create-Blacklist.js
│   │   │   └── seeders/
│   │   │       ├── 20180726092449-Users.js
│   │   │       ├── 20180726093449-Group.js
│   │   │       ├── 20180726093449-GroupsUsers.js
│   │   │       ├── 20240411041313-Buckets.js
│   │   │       └── 20240430101608-Blacklist.js
│   │   ├── index.js
│   │   ├── models/
│   │   │   ├── Blacklist.js
│   │   │   ├── Bucket.js
│   │   │   ├── Group.js
│   │   │   ├── GroupsUsers.js
│   │   │   ├── User.js
│   │   │   └── index.js
│   │   ├── providers/
│   │   │   ├── bucket.yml
│   │   │   ├── connections.js
│   │   │   ├── db.js
│   │   │   ├── errorHandler.js
│   │   │   ├── generateJWT.js
│   │   │   ├── hCaptcha.js
│   │   │   └── passport.js
│   │   ├── routes/
│   │   │   ├── Buckets.js
│   │   │   ├── auth.js
│   │   │   ├── groups.js
│   │   │   ├── middleware/
│   │   │   │   ├── canAccessBucket.js
│   │   │   │   ├── checkPassword.js
│   │   │   │   ├── hCaptcha.js
│   │   │   │   ├── index.js
│   │   │   │   ├── isGroupOwner.js
│   │   │   │   ├── isInGroup.js
│   │   │   │   └── isNotSelf.js
│   │   │   └── user.js
│   │   └── scripts/
│   │       ├── blacklist.js
│   │       ├── buckets.js
│   │       ├── deleteUser.js
│   │       ├── env
│   │       ├── forgotPassword.js
│   │       ├── generate.js
│   │       ├── generator/
│   │       │   ├── Migration.js
│   │       │   ├── Model.js
│   │       │   ├── Route.js
│   │       │   └── Seeder.js
│   │       ├── inviteUser.js
│   │       ├── jwt.js
│   │       ├── refresh
│   │       ├── resetPassword.js
│   │       ├── seed
│   │       ├── sync.js
│   │       └── users.js
│   └── tests/
│       ├── Auth.js
│       ├── Group.js
│       ├── HealthCheck.js
│       └── User.js
├── automation-test/
│   ├── .gitlab-ci.yml
│   ├── Dockerfile
│   ├── bucket.yml
│   └── deployment.yml
├── aws-sdk-test/
│   ├── .gitignore
│   ├── index.js
│   └── package.json
├── deployment-test/
│   ├── .gitlab-ci.yml
│   ├── Dockerfile
│   ├── index.html
│   └── k8s.yml
├── frontend/
│   ├── .browserslistrc
│   ├── .editorconfig
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── .gitlab-ci.yml
│   ├── Dockerfile
│   ├── ReadMe.md
│   ├── default.conf
│   ├── index.html
│   ├── jsconfig.json
│   ├── k8s/
│   │   ├── clusterissuer.yml
│   │   ├── frontend-ssl.ingress.yml
│   │   ├── frontend.deployment.yml
│   │   ├── frontend.ingress.yml
│   │   └── frontend.service.yml
│   ├── package.json
│   ├── src/
│   │   ├── App.vue
│   │   ├── api/
│   │   │   ├── Auth.js
│   │   │   ├── Buckets.js
│   │   │   ├── Service.js
│   │   │   ├── User.js
│   │   │   └── index.js
│   │   ├── components/
│   │   │   ├── CreateBucketForm.vue
│   │   │   └── TermsOfService.vue
│   │   ├── layouts/
│   │   │   └── default/
│   │   │       ├── AppBar.vue
│   │   │       ├── Auth.vue
│   │   │       ├── Default.vue
│   │   │       └── View.vue
│   │   ├── main.js
│   │   ├── plugins/
│   │   │   ├── errorHandler.js
│   │   │   ├── index.js
│   │   │   ├── router.js
│   │   │   ├── store.js
│   │   │   ├── vuetify.js
│   │   │   └── webfontloader.js
│   │   ├── styles/
│   │   │   └── settings.scss
│   │   └── views/
│   │       ├── Buckets.vue
│   │       ├── Login.vue
│   │       └── SignUp.vue
│   └── vite.config.js
├── k3s/
│   ├── alpine.deployment.yml
│   ├── echo.s3.ssl.yml
│   ├── echo.ssl.yml
│   └── echo.yml
├── longhorn/
│   ├── longhorn.ingress.yml
│   ├── longhorn.lb.yml
│   └── longhorn.storageclass.yml
├── node/
│   └── node-config-script.sh
└── sections/
    ├── automated-bucket-deployment.md
    ├── console.md
    ├── deploying-from-gitlab-to-k3s.md
    ├── gitlab.md
    ├── internet.md
    ├── networking.md
    ├── node.md
    ├── production-cluster.md
    ├── ssl.md
    └── storage-cluster.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.DS_Store

notes/*
*.crt
*.k8s

api_
frontend_
website_

================================================
FILE: ReadMe.md
================================================
# S3 From Scratch

<p align="center">
  <img width="300" src="https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/s3.png">
</p>

For the past few years I’ve been thinking about how I could build SaaS and deploy it on my own infrastructure without needing to use any cloud platforms like AWS or GCP. In this repo I document my progress on building a clone of AWS S3 that functions the same as S3 (automated bucket deployment, dynamically expanding volumes, security, etc) using an exclusively open-source technology stack.

- [Console](./sections/console.md)
- [Nodes](./sections/node.md)
- [Source Control: GitLab](./sections/gitlab.md)
- [K3s: Production Cluster](./sections/production-cluster.md)
- [Deploying From GitLab Registry To Local K3s Cluster](./sections/deploying-from-gitlab-to-k3s.md)
- [K3s: Storage Cluster](./sections/storage-cluster.md)
- [Automated Bucket Deployment](./sections/automated-bucket-deployment.md)
- [API](./api/ReadMe.md)
- [Frontend](./frontend/ReadMe.md)
- [Connecting to the Internet](./sections/internet.md)


### Live POC working with `@aws-sdk/client-s3`

<p align="center">
  <img width="500" src="https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/live-demo.gif">
</p>

```js
const { S3Client, ListObjectsV2Command, PutObjectCommand } = require("@aws-sdk/client-s3");

const Bucket = 'BUCKET_NAME_HERE';
const Namespace = 'NAMESPACE_HERE';
const accessKeyId = "xxxxxxxxxxxxxxxxxxxx";
const secretAccessKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

(async function () {
    const client = new S3Client({
        region: 'us-west-2',
        endpoint: `https://${Bucket}.${Namespace}.s3.anthonybudd.io`,
        forcePathStyle: true,
        sslEnabled: true,
        credentials: {
            accessKeyId,
            secretAccessKey
        },
    });

    const Key = `${Date.now().toString()}.txt`;
    await client.send(new PutObjectCommand({
        Bucket,
        Key,
        Body: `The time now is ${new Date().toLocaleString()}`,
        ACL: 'public-read',
        ContentType: 'text/plain',
    }));
    console.log(`New object successfully written to: ${Bucket}://${Key}\n`);

    const { Contents } = await client.send(new ListObjectsV2Command({ Bucket }));
    console.log("Bucket Contents:");
    console.log(Contents.map(({ Key }) => Key).join("\n"));
})();
```

### Technical Overview
<p align="center">
  <img src="https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/infrastructure.png?v=4">
</p>

### [Node](./sections/node.md)
<img height="200" src="https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/node.png">

Since this project needs to be "Enterprise-grade" we need a distinct and replicable compute unit that we can buy and build in bulk. I call this a "Node" which is a Raspberry Pi with a 1TB SSD and POE hat. I have also 3D printed a rack-mount solution (Source: [Merocle From UpTimeLabs](https://www.thingiverse.com/thing:4756812)) for easy install into a rack. 

### [Console](./sections/console.md)
<img height="200" src="https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/console-close-up.png">

We will need a "console" so we can locally interact with the infrastructure. In the past I have tried using a Raspberry Pi with a monitor and keyboard attached but I have found that using an old MacBook Pro works best for this. In this section I explain how to set-up the console so you can use it to store secrets, manage the network, provision K3s clusters and deploy pods.

### [Frontend](./frontend/ReadMe.md)
<img height="250" src="https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/frontend.gif">

This represents the AWS management console found at [aws.amazon.com/console](https://aws.amazon.com/console/). This is a Vue.js SPA that makes HTTP requests to the [S3 REST API](./api/ReadMe.md) for users to create, manage and delete their S3 buckets.


### [API](./api/ReadMe.md)
```sh
curl -X POST \
    -H 'Authorization: Bearer $JWT' \
    -H 'Content-Type: application/json' \
    -d '{ "name":"s3-test-bucket"}' \
    https://s3-api.anthonybudd.io/buckets
```

This API simulates the back-end of the AWS Console. A user can sign-up, login, create a bucket then delete the bucket.

### [Source Control](./sections/gitlab.md)
<img height="75" src="https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/gitlab-logo.svg">

We will need a way to store the code for the landing website, the front-end ui and the back-end REST API. We  will also need CI/CD to compile the code and deploy it into our infrastructure. GitLab will work perfectly for these two tasks. This will allow us to commit code to the local GitLab instance, compile it into a Docker image, store the image in a repository and deploy from it directy into our local kubernetes cluster. 

### [Networking](./sections/networking.md)
<img height="75" src="https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/openwrt_.png">

We will need a network for the nodes to communicate. For the a router I have chosen OpenWRT. This allows me to use a Raspberry Pi with a USB 3.0 Ethernet adapter so it can work as a router between the internet and the datacenter.

### [Automation](./sections/automated-bucket-deployment.md)
When you create an S3 bucket on AWS, everything is automated, there isn’t a human in a datacenter somewhere typing out CLI commands to get your bucket scheduled. I want my project to work the same way, when a user wants to create a bucket it must not require any human input to provision and deploy it.

### [Resource Utilization](./sections/storage-cluster.md)
AWS doesn't give each user their own dedicated server with a hard drive attached, instead the hardware is virtualized, allowing multiple tenants to share a single physical CPU. Similarly it would not be practical to assign a whole node and SSD to each bucket, to maximize resource utilization my platform must be able to allow multiple tenants to share the pool of compute and SSD storage space available. In addition, AWS S3 buckets can store an unlimited amount of data, so my platform will also need to allow a user to have a dynamically increasing volume that will auto-scale based on the storage space required.


### Notes
You will need to SSH into multiple devices simultaneously I have added an annotation (example: `[Console] nano /boot/config.txt`) to all commands in this repo, to show where you should be executing each command. Generally you will see `[Console]` and `[Node X]`.

Because this is still very much a work-in-progress you will see my notes in italics "_AB:_" throughout, please ignore.

================================================
FILE: ansible/.gitignore
================================================
Notes.md

================================================
FILE: ansible/.yamllint
================================================
---
extends: default

rules:
  line-length:
    max: 120
    level: warning
  truthy:
    allowed-values: ['true', 'false', 'yes', 'no']


================================================
FILE: ansible/README.md
================================================
# Ansible

Source: [https://github.com/k3s-io/k3s-ansible](https://github.com/k3s-io/k3s-ansible)


================================================
FILE: ansible/ansible.cfg
================================================
[defaults]
nocows = True
roles_path = ./roles
inventory  = ./hosts.ini

remote_tmp = $HOME/.ansible/tmp
local_tmp  = $HOME/.ansible/tmp
pipelining = True
become = True
host_key_checking = False
deprecation_warnings = False
callback_whitelist = profile_tasks


================================================
FILE: ansible/inventory/.gitignore
================================================
*-cluster/
!exmaple/
!.gitignore
!sample/

================================================
FILE: ansible/inventory/example/group_vars/all.yml
================================================
---
k3s_version: v1.26.9+k3s1
ansible_user: node
systemd_dir: /etc/systemd/system
master_ip: "{{ hostvars[groups['master'][0]]['ansible_host'] | default(groups['master'][0]) }}"
extra_server_args: ""
extra_agent_args: ""


================================================
FILE: ansible/inventory/example/hosts.ini
================================================
[master]
10.0.0.5

[node]
10.0.0.5
10.0.0.6
10.0.0.7

[k3s_cluster:children]
master
node


================================================
FILE: ansible/reset.yml
================================================
---

- hosts: k3s_cluster
  gather_facts: yes
  become: yes
  roles:
    - role: reset


================================================
FILE: ansible/roles/download/tasks/main.yml
================================================
---

- name: Download k3s binary x64
  get_url:
    url: https://github.com/k3s-io/k3s/releases/download/{{ k3s_version }}/k3s
    checksum: sha256:https://github.com/k3s-io/k3s/releases/download/{{ k3s_version }}/sha256sum-amd64.txt
    dest: /usr/local/bin/k3s
    owner: root
    group: root
    mode: 0755
  when: ansible_facts.architecture == "x86_64"

- name: Download k3s binary arm64
  get_url:
    url: https://github.com/k3s-io/k3s/releases/download/{{ k3s_version }}/k3s-arm64
    checksum: sha256:https://github.com/k3s-io/k3s/releases/download/{{ k3s_version }}/sha256sum-arm64.txt
    dest: /usr/local/bin/k3s
    owner: root
    group: root
    mode: 0755
  when:
    - ( ansible_facts.architecture is search("arm") and
        ansible_facts.userspace_bits == "64" ) or
      ansible_facts.architecture is search("aarch64")

- name: Download k3s binary armhf
  get_url:
    url: https://github.com/k3s-io/k3s/releases/download/{{ k3s_version }}/k3s-armhf
    checksum: sha256:https://github.com/k3s-io/k3s/releases/download/{{ k3s_version }}/sha256sum-arm.txt
    dest: /usr/local/bin/k3s
    owner: root
    group: root
    mode: 0755
  when:
    - ansible_facts.architecture is search("arm")
    - ansible_facts.userspace_bits == "32"


================================================
FILE: ansible/roles/k3s/master/tasks/main.yml
================================================
---

- name: Copy K3s service file
  register: k3s_service
  template:
    src: "k3s.service.j2"
    dest: "{{ systemd_dir }}/k3s.service"
    owner: root
    group: root
    mode: 0644

- name: Enable and check K3s service
  systemd:
    name: k3s
    daemon_reload: yes
    state: restarted
    enabled: yes

- name: Wait for node-token
  wait_for:
    path: /var/lib/rancher/k3s/server/node-token

- name: Register node-token file access mode
  stat:
    path: /var/lib/rancher/k3s/server
  register: p

- name: Change file access node-token
  file:
    path: /var/lib/rancher/k3s/server
    mode: "g+rx,o+rx"

- name: Read node-token from master
  slurp:
    src: /var/lib/rancher/k3s/server/node-token
  register: node_token

- name: Store Master node-token
  set_fact:
    token: "{{ node_token.content | b64decode | regex_replace('\n', '') }}"

- name: Restore node-token file access
  file:
    path: /var/lib/rancher/k3s/server
    mode: "{{ p.stat.mode }}"

- name: Create directory .kube
  file:
    path: ~{{ ansible_user }}/.kube
    state: directory
    owner: "{{ ansible_user }}"
    mode: "u=rwx,g=rx,o="

- name: Copy config file to user home directory
  copy:
    src: /etc/rancher/k3s/k3s.yaml
    dest: ~{{ ansible_user }}/.kube/config
    remote_src: yes
    owner: "{{ ansible_user }}"
    mode: "u=rw,g=,o="

- name: Replace https://localhost:6443 by https://master-ip:6443
  command: >-
    k3s kubectl config set-cluster default
      --server=https://{{ master_ip }}:6443
      --kubeconfig ~{{ ansible_user }}/.kube/config
  changed_when: true

- name: Create kubectl symlink
  file:
    src: /usr/local/bin/k3s
    dest: /usr/local/bin/kubectl
    state: link

- name: Create crictl symlink
  file:
    src: /usr/local/bin/k3s
    dest: /usr/local/bin/crictl
    state: link


================================================
FILE: ansible/roles/k3s/master/templates/k3s.service.j2
================================================
[Unit]
Description=Lightweight Kubernetes
Documentation=https://k3s.io
After=network-online.target

[Service]
Type=notify
ExecStartPre=-/sbin/modprobe br_netfilter
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/k3s server {{ extra_server_args | default("") }}
KillMode=process
Delegate=yes
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
TimeoutStartSec=0
Restart=always
RestartSec=5s

[Install]
WantedBy=multi-user.target


================================================
FILE: ansible/roles/k3s/node/tasks/main.yml
================================================
---

- name: Copy K3s service file
  template:
    src: "k3s.service.j2"
    dest: "{{ systemd_dir }}/k3s-node.service"
    owner: root
    group: root
    mode: 0755

- name: Enable and check K3s service
  systemd:
    name: k3s-node
    daemon_reload: yes
    state: restarted
    enabled: yes


================================================
FILE: ansible/roles/k3s/node/templates/k3s.service.j2
================================================
[Unit]
Description=Lightweight Kubernetes
Documentation=https://k3s.io
After=network-online.target

[Service]
Type=notify
ExecStartPre=-/sbin/modprobe br_netfilter
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/k3s agent --server https://{{ master_ip }}:6443 --token {{ hostvars[groups['master'][0]]['token'] }} {{ extra_agent_args | default("") }}
KillMode=process
Delegate=yes
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
TimeoutStartSec=0
Restart=always
RestartSec=5s

[Install]
WantedBy=multi-user.target


================================================
FILE: ansible/roles/prereq/tasks/main.yml
================================================
---
- name: Set SELinux to disabled state
  selinux:
    state: disabled
  when: ansible_distribution in ['CentOS', 'Red Hat Enterprise Linux']

- name: Enable IPv4 forwarding
  sysctl:
    name: net.ipv4.ip_forward
    value: "1"
    state: present
    reload: yes

- name: Enable IPv6 forwarding
  sysctl:
    name: net.ipv6.conf.all.forwarding
    value: "1"
    state: present
    reload: yes

- name: Add br_netfilter to /etc/modules-load.d/
  copy:
    content: "br_netfilter"
    dest: /etc/modules-load.d/br_netfilter.conf
    mode: "u=rw,g=,o="
  when: ansible_distribution in ['CentOS', 'Red Hat Enterprise Linux']

- name: Load br_netfilter
  modprobe:
    name: br_netfilter
    state: present
  when: ansible_distribution in ['CentOS', 'Red Hat Enterprise Linux']

- name: Set bridge-nf-call-iptables (just to be sure)
  sysctl:
    name: "{{ item }}"
    value: "1"
    state: present
    reload: yes
  when: ansible_distribution in ['CentOS', 'Red Hat Enterprise Linux']
  loop:
    - net.bridge.bridge-nf-call-iptables
    - net.bridge.bridge-nf-call-ip6tables

- name: Add /usr/local/bin to sudo secure_path
  lineinfile:
    line: 'Defaults    secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin'
    regexp: "Defaults(\\s)*secure_path(\\s)*="
    state: present
    insertafter: EOF
    path: /etc/sudoers
    validate: 'visudo -cf %s'
  when: ansible_distribution in ['CentOS', 'Red Hat Enterprise Linux']


================================================
FILE: ansible/roles/raspberrypi/handlers/main.yml
================================================
---
- name: reboot
  reboot:


================================================
FILE: ansible/roles/raspberrypi/tasks/main.yml
================================================
---
- name: Test for raspberry pi /proc/cpuinfo
  command: grep -E "Raspberry Pi|BCM2708|BCM2709|BCM2835|BCM2836" /proc/cpuinfo
  register: grep_cpuinfo_raspberrypi
  failed_when: false
  changed_when: false

- name: Test for raspberry pi /proc/device-tree/model
  command: grep -E "Raspberry Pi" /proc/device-tree/model
  register: grep_device_tree_model_raspberrypi
  failed_when: false
  changed_when: false

- name: Set raspberry_pi fact to true
  set_fact:
    raspberry_pi: true
  when:
    grep_cpuinfo_raspberrypi.rc == 0 or grep_device_tree_model_raspberrypi.rc == 0

- name: Set detected_distribution to Raspbian
  set_fact:
    detected_distribution: Raspbian
  when: >
    raspberry_pi|default(false) and
    ( ansible_facts.lsb.id|default("") == "Raspbian" or
      ansible_facts.lsb.description|default("") is match("[Rr]aspbian.*") )

- name: Set detected_distribution to Raspbian (ARM64 on Debian Buster)
  set_fact:
    detected_distribution: Raspbian
  when:
    - ansible_facts.architecture is search("aarch64")
    - raspberry_pi|default(false)
    - ansible_facts.lsb.description|default("") is match("Debian.*buster")

- name: Set detected_distribution_major_version
  set_fact:
    detected_distribution_major_version: "{{ ansible_facts.lsb.major_release }}"
  when:
    - detected_distribution | default("") == "Raspbian"

- name: execute OS related tasks on the Raspberry Pi
  include_tasks: "{{ item }}"
  with_first_found:
    - "prereq/{{ detected_distribution }}-{{ detected_distribution_major_version }}.yml"
    - "prereq/{{ detected_distribution }}.yml"
    - "prereq/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml"
    - "prereq/{{ ansible_distribution }}.yml"
    - "prereq/default.yml"
  when:
    - raspberry_pi|default(false)


================================================
FILE: ansible/roles/raspberrypi/tasks/prereq/CentOS.yml
================================================
---
- name: Enable cgroup via boot commandline if not already enabled for Centos
  lineinfile:
    path: /boot/cmdline.txt
    backrefs: yes
    regexp: '^((?!.*\bcgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory\b).*)$'
    line: '\1 cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory'
  notify: reboot


================================================
FILE: ansible/roles/raspberrypi/tasks/prereq/Raspbian.yml
================================================
---
- name: Activating cgroup support
  lineinfile:
    path: /boot/cmdline.txt
    regexp: '^((?!.*\bcgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory\b).*)$'
    line: '\1 cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory'
    backrefs: true
  notify: reboot

- name: Flush iptables before changing to iptables-legacy
  iptables:
    flush: true
  changed_when: false   # iptables flush always returns changed

- name: Changing to iptables-legacy
  alternatives:
    path: /usr/sbin/iptables-legacy
    name: iptables
  register: ip4_legacy

- name: Changing to ip6tables-legacy
  alternatives:
    path: /usr/sbin/ip6tables-legacy
    name: ip6tables
  register: ip6_legacy


================================================
FILE: ansible/roles/raspberrypi/tasks/prereq/Ubuntu.yml
================================================
---
- name: Enable cgroup via boot commandline if not already enabled for Ubuntu on a Raspberry Pi
  lineinfile:
    path: /boot/firmware/cmdline.txt
    backrefs: yes
    regexp: '^((?!.*\bcgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory\b).*)$'
    line: '\1 cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory'
  notify: reboot


================================================
FILE: ansible/roles/raspberrypi/tasks/prereq/default.yml
================================================
---


================================================
FILE: ansible/roles/reset/tasks/main.yml
================================================
---
- name: Disable services
  systemd:
    name: "{{ item }}"
    state: stopped
    enabled: no
  failed_when: false
  with_items:
    - k3s
    - k3s-node

- name: pkill -9 -f "k3s/data/[^/]+/bin/containerd-shim-runc"
  register: pkill_containerd_shim_runc
  command: pkill -9 -f "k3s/data/[^/]+/bin/containerd-shim-runc"
  changed_when: "pkill_containerd_shim_runc.rc == 0"
  failed_when: false

- name: Umount k3s filesystems
  include_tasks: umount_with_children.yml
  with_items:
    - /run/k3s
    - /var/lib/kubelet
    - /run/netns
    - /var/lib/rancher/k3s
  loop_control:
    loop_var: mounted_fs

- name: Remove service files, binaries and data
  file:
    name: "{{ item }}"
    state: absent
  with_items:
    - /usr/local/bin/k3s
    - "{{ systemd_dir }}/k3s.service"
    - "{{ systemd_dir }}/k3s-node.service"
    - /etc/rancher/k3s
    - /var/lib/kubelet
    - /var/lib/rancher/k3s

- name: daemon_reload
  systemd:
    daemon_reload: yes


================================================
FILE: ansible/roles/reset/tasks/umount_with_children.yml
================================================
---
- name: Get the list of mounted filesystems
  shell: set -o pipefail && cat /proc/mounts | awk '{ print $2}' | grep -E "^{{ mounted_fs }}"
  register: get_mounted_filesystems
  args:
    executable: /bin/bash
  failed_when: false
  changed_when: get_mounted_filesystems.stdout | length > 0
  check_mode: false

- name: Umount filesystem
  mount:
    path: "{{ item }}"
    state: unmounted
  with_items:
    "{{ get_mounted_filesystems.stdout_lines | reverse | list }}"


================================================
FILE: ansible/site.yml
================================================
---

- hosts: k3s_cluster
  gather_facts: yes
  become: yes
  roles:
    - role: prereq
    - role: download
    - role: raspberrypi

- hosts: master
  become: yes
  roles:
    - role: k3s/master

- hosts: node
  become: yes
  roles:
    - role: k3s/node


================================================
FILE: api/.dockerignore
================================================
node_modules
package-lock.json

================================================
FILE: api/.eslintignore
================================================
src/database/
tests

================================================
FILE: api/.gitignore
================================================
node_modules/
.vol/
.env
dev.js
private.pem
public.pem
.DS_Store
*/.DS_Store
k8s/secrets.yml
kubeconfig.yml

================================================
FILE: api/.gitlab-ci.yml
================================================
stages:
  - build

build-job:
  image: docker:dind
  stage: build
  services:
    - docker:dind
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
  script:
    - docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG

================================================
FILE: api/.sequelizerc
================================================
const path = require('path');

module.exports = {
    'config':           path.resolve('src/providers', 'connections.js'),
    'models-path':      path.resolve('src/',          'models'),
    'seeders-path':     path.resolve('src/database',  'seeders'),
    'migrations-path':  path.resolve('src/database',  'migrations')
}

================================================
FILE: api/Dockerfile
================================================
FROM node:20

RUN apt-get update && apt-get install -y curl nano
RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/arm64/kubectl"
RUN install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

RUN npm install -g nodemon mocha sequelize sequelize-cli mysql2 eslint

WORKDIR /app
COPY . /app
RUN npm install

ENTRYPOINT [ "node", "/app/src/index.js" ]


================================================
FILE: api/LICENSE
================================================
The MIT License

Copyright Anthony C. Budd

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

================================================
FILE: api/ReadMe.md
================================================
# S3 API

This API simulates the back-end of the AWS Console. A user can sign-up, login, create a bucket then delete the bucket.

This API was built using my project [anthonybudd/express-api-boilerplate.](https://github.com/anthonybudd/express-api-boilerplate)

### Main Files
- Auth Controller: [./src/routes/Auth.js](./src/routes/auth.js)
- Bucket Controller: [./src/routes/Buckets.js](./src/routes/Buckets.js)
- Model: [./src/models/Bucket.js](./src/models/Bucket.js)


### Set-up
```
cp .env.example .env
npm install

# Private RSA key for JWT signing
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem

# Start the app
docker compose up
npm run _db:refresh
npm run _test
```


### Routes
| Method      | Route                            | Description                           | Payload                               | Response          | 
| ----------- | -------------------------------- | ------------------------------------- | ------------------------------------- | ----------------- |  
| **Buckets**  |                                  |                                       |                                       |                   |  
| GET         | `/api/v1/buckets`                | Get all buckets for the current user  | --                                    | [Bucket, Bucket]  |  
| POST        | `/api/v1/buckets`                | Create new bucket                     | { name: "test-bucket" }               | {Bucket}          |  
| GET         | `/api/v1/buckets/:bucketID`      | Get a single bucket                   | --                                    | {Bucket}          |  
| DELETE      | `/api/v1/buckets/:bucketID`      | Returns HTTP 202 {id}                 | --                                    | {bucketID}    |  
| **Auth**    |                                  |                                       |                                       |                   |  
| POST        | `/api/v1/auth/login`             | Login                                 | {email, password}                     | {accessToken}     |  
| POST        | `/api/v1/auth/sign-up`           | Sign-up                               | {email, password, firstName, tos}     | {accessToken}     |  
| GET         | `/api/v1/_authcheck`             | Returns {auth: true} if has auth      | --                                    | {auth: true}      |  
| **User**    |                                  |                                       |                                       |                   |  
| GET         | `/api/v1/user`                   | Get the current user                  |                                       | {User}            |  
| POST        | `/api/v1/user`                   | Update the current user               | {firstName, lastName}                 | {User}            |  




================================================
FILE: api/docker-compose.yml
================================================
version: "3"

services:
  s3-api:
    build: .
    entrypoint: "nodemon /app/src/index.js --watch /app --legacy-watch"
    container_name: s3-api
    volumes:
      - ./:/app
      - ./.vol/tmp:/tmp
    links:
      - s3-api-db
      - s3-api-db-test
    ports:
      - "8888:80"
    environment:
      PORT: 80

  s3-api-db:
    image: mysql:oracle
    container_name: s3-api-db
    ports:
      - "3306:3306"
    volumes:
      - ./.vol/s3-api:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: supersecret
      MYSQL_DATABASE: $DB_DATABASE
      MYSQL_USER: $DB_USERNAME
      MYSQL_PASSWORD: $DB_PASSWORD

  s3-api-db-test:
    image: mysql:oracle
    container_name: s3-api-db-test
    ports:
      - "3307:3306"
    volumes:
      - ./.vol/s3-api-test:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: supersecret
      MYSQL_DATABASE: $DB_DATABASE
      MYSQL_USER: $DB_USERNAME
      MYSQL_PASSWORD: $DB_PASSWORD


================================================
FILE: api/k8s/Deploy.md
================================================
# Deploy




kubectl --kubeconfig=.kube/config create namespace s3-api
namespace/s3-api created
[Console]:~> kubectl --kubeconfig=.kube/config apply -f db.yml        
service/s3-db created
deployment.apps/s3-db created




----

Find & Replace (case-sensaive, whole repo): "s3-api" => "your-api-name" 

Save kubeconfig.yml to root of repo


### Namespace
Create a namespace
`kubectl --kubeconfig=./kubeconfig.yml create namespace s3-api`


### JWT
```
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
kubectl --kubeconfig=.kube/config --namespace=s3-api create secret generic s3-api-jwt-secret \
    --from-file=./private.pem \
    --from-file=./public.pem
rm ./private.pem ./public.pem 
```


### Secrets
Make a new secrets config file
`cp secrets.example.yml secrets.yml`

__Add Secrets in Base64__
Hint: `echo -n 'my-secret-string' | base64`

Create the secrets
`kubectl --kubeconfig=./kubeconfig.yml apply -f ./k8s/secrets.yml`


### Build & Push Container Image
```
docker buildx build --platform linux/amd64 --push -t registry.digitalocean.com/s3-api/app:latest
```

### Create Deployment
```
kubectl --kubeconfig=./kubeconfig.yml apply -f ./k8s/api.deployment.yml
kubectl --kubeconfig=./kubeconfig.yml apply -f ./k8s/api.service.yml
```

### Deploy
```
docker buildx build --platform linux/amd64 --push -t registry.digitalocean.com/s3-api/app:latest . && 
kubectl --kubeconfig=./kubeconfig.yml rollout restart deployment s3-api && \
kubectl --kubeconfig=./kubeconfig.yml get pods -w
```


### Migrate
Migrate the DB
```
export POD="$(kubectl --kubeconfig=kubeconfig.yml --namespace=s3-api get pods --field-selector=status.phase==Running --no-headers -o custom-columns=":metadata.name")"
kubectl --kubeconfig=./kubeconfig.yml --namespace=s3-api exec -ti $POD -- /bin/bash -c 'sequelize db:migrate:undo:all && sequelize db:migrate && sequelize db:seed:all'
```

### SSL
ReadMore: https://www.digitalocean.com/community/tutorials/how-to-set-up-an-nginx-ingress-with-cert-manager-on-digitalocean-kubernetes

```
kubectl --kubeconfig=./kubeconfig.yml apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.1/deploy/static/provider/do/deploy.yaml

kubectl --kubeconfig=./kubeconfig.yml get pods -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx --watch

kubectl --kubeconfig=./kubeconfig.yml apply -f ./k8s/api.ingress.yml

kubectl --kubeconfig=./kubeconfig.yml apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.1/cert-manager.yaml

kubectl --kubeconfig=./kubeconfig.yml get pods --namespace cert-manager

kubectl --kubeconfig=./kubeconfig.yml create -f k8s/prod-issuer.yml
```

### Useful K8S commands
##### Set $POD as the name of the pod in K8s
`export POD="$(kubectl --kubeconfig=kubeconfig.yml --namespace=s3-api get pods --field-selector=status.phase==Running --no-headers -o custom-columns=":metadata.name")"`

##### Execute bash script inside running container
`kubectl --kubeconfig=kubeconfig.yml exec -ti $POD -- /bin/bash -c "sequelize db:migrate"`

##### Get logs for $POD
`kubectl --kubeconfig=kubeconfig.yml logs $POD`

##### Create a cron job
`kubectl --kubeconfig=kubeconfig.yml create job --from=cronjob/s3-api-cron-job s3-api-cron-job`

##### Delete all faild cron jobs
`kubectl --kubeconfig=kubeconfig.yml delete jobs --field-selector status.successful=0`


================================================
FILE: api/k8s/api.deployment.yml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
  name: s3-api
  namespace: s3-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: s3-api
  template:
    metadata:
      labels:
        app: s3-api
    spec:
      volumes:
        - name: s3-api-jwt-secret
          secret:
            secretName: s3-api-jwt-secret
        - name: storage-cluster-config
          secret:
            secretName: storage-cluster-config   
      containers:
        - name: s3-api
          image: gitlab.local:5050/anthonybudd/api:master
          imagePullPolicy: Always
          lifecycle:
            postStart:
              exec:
                command: ["/bin/bash", "-c", "sequelize db:migrate"]
          ports:
          - containerPort: 80
          volumeMounts:
          - name: s3-api-jwt-secret
            mountPath: "/app/private.pem"
            subPath: private.pem
          - name: s3-api-jwt-secret
            mountPath: "/app/public.pem"
            subPath: public.pem
          - name: storage-k8s-config
            mountPath: "/app/config"
            subPath: storage-config
          env:
          - name: S3_ROOT
            value: "s3.anthonybudd.io"
          - name: K8S_CONFIG_PATH
            value: "/app/k8s-config"
          - name: NODE_ENV
            value: "production"
          - name: FRONTEND_URL
            value: "https://s3.anthonybudd.io"
          - name: BACKEND_URL
            value: "https://s3-api.anthonybudd.io/api/v1"
          - name: PORT
            value: "80"
          - name: PRIVATE_KEY_PATH
            value: "/app/private.pem"
          - name: PUBLIC_KEY_PATH
            value: "/app/public.pem"
          - name: DB_HOST
            value: "s3-db"
          - name: DB_PORT
            value: "3306"
          - name: DB_USERNAME
            value: "app"
          - name: DB_DATABASE
            value: "app"
          - name: DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: s3-api-secrets
                key: DB_PASSWORD
          - name: HCAPTCHA_SECRET
            valueFrom:
              secretKeyRef:
                name: s3-api-secrets
                key: HCAPTCHA_SECRET


================================================
FILE: api/k8s/api.ingress.yml
================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: s3-api
  name: s3-ingress
  annotations:
    kubernetes.io/ingress.class: "traefik"
spec:
  rules:
  - host: api.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: s3-api
            port:
              number: 80

================================================
FILE: api/k8s/api.service.yml
================================================
apiVersion: v1
kind: Service
metadata:
  name: s3-api
  namespace: s3-api
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: s3-api

================================================
FILE: api/k8s/api.ssl.ingress.yml
================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: s3-api
  name: s3-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    kubernetes.io/ingress.class: "traefik"
spec:
  tls:
  - hosts:
    - s3-api.anthonybudd.io
    secretName: s3-api-anthonybudd-io-cert
  rules:
  - host: s3-api.anthonybudd.io
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: s3-api
            port:
              number: 80

================================================
FILE: api/k8s/db.yml
================================================
apiVersion: v1
kind: Service
metadata:
  name: s3-db
  namespace: s3-api
spec:
  selector:    
    app: s3-db 
  ports:  
  - protocol: TCP  
    port: 80 
    targetPort: 3306
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: s3-db
  namespace: s3-api
spec:
  selector:
    matchLabels:
      app: s3-db
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: s3-db
    spec:
      containers:
      - image: mysql:8
        name: s3-db
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: password
        - name: MYSQL_DATABASE
          value: app
        - name: MYSQL_USER
          value: app
        - name: MYSQL_PASSWORD
          value: password
        ports:
        - containerPort: 3306
          name: s3-db
      #   volumeMounts:
      #   - name: mysql-persistent-storage
      #     mountPath: /var/lib/mysql
      # volumes:
      # - name: mysql-persistent-storage
      #   persistentVolumeClaim:
      #     claimName: mysql-pv-claim

================================================
FILE: api/k8s/prod.clusterissuer.yml
================================================
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
  namespace: cert-manager
spec:
  acme:
    email: YOUR_EMAIL_ADDRESS
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: traefik

================================================
FILE: api/k8s/secrets.example.yml
================================================
apiVersion: v1    
kind: Secret
metadata:
  name: s3-api-secrets
  namespace: default
type: Opaque
data:
  DB_PASSWORD: 
  

================================================
FILE: api/k8s/sync.job.yml
================================================
apiVersion: batch/v1
kind: CronJob
metadata:
  name: sync
  namespace: s3-api
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          volumes:
          - name: storage-k8s-config
            secret:
              secretName: storage-k8s-config 
          containers:
          - name: s3-api
            image: gitlab.local:5050/anthonybudd/api:main
            imagePullPolicy: Always
            command:
            - /bin/bash
            - -c
            - "node /app/src/scripts/sync.js"
            volumeMounts:
            - name: storage-k8s-config
              mountPath: "/app/storage-config"
              subPath: storage-config
            env:
            - name: S3_ROOT
              value: "s3.anthonybudd.io"
            - name: K8S_CONFIG_PATH
              value: "/app/storage-config"
            - name: NODE_ENV
              value: "production"
            - name: FRONTEND_URL
              value: "https://s3.anthonybudd.io"
            - name: BACKEND_URL
              value: "https://s3-api.anthonybudd.io/api/v1"
            - name: PORT
              value: "80"
            - name: DB_HOST
              value: "s3-db"
            - name: DB_PORT
              value: "3306"
            - name: DB_USERNAME
              value: "app"
            - name: DB_DATABASE
              value: "app"
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: s3-api-secrets
                  key: DB_PASSWORD
            - name: HCAPTCHA_SECRET
              valueFrom:
                secretKeyRef:
                  name: s3-api-secrets
                  key: HCAPTCHA_SECRET

================================================
FILE: api/package.json
================================================
{
    "name": "s3-api-boilerplate",
    "version": "1.0.0",
    "main": "./src/index.js",
    "author": "Anthony Budd",
    "scripts": {
        "start": "node ./src/",
        "lint": "eslint src",
        "_lint": "docker exec -ti s3-api npm run lint",
        "jwt": "node ./src/scripts/jwt.js",
        "_jwt": "docker exec -ti s3-api npm run jwt",
        "env": "./src/scripts/env",
        "db:migrate": "sequelize db:migrate",
        "db:seed": "./src/scripts/seed",
        "db:refresh": "./src/scripts/refresh",
        "_db:refresh": "docker exec -ti s3-api npm run db:refresh",
        "db:refresh-test": "node_modules/.bin/sequelize db:migrate:undo:all --env test && node_modules/.bin/sequelize db:migrate --env test && node_modules/.bin/sequelize db:seed:all --env test",
        "test": "npm run db:refresh-test && mocha --exit --timeout 10000 tests",
        "_test": "docker exec -ti s3-api npm run test"
    },
    "eslintConfig": {
        "extends": "eslint:recommended",
        "parserOptions": {
            "ecmaVersion": 8,
            "sourceType": "module"
        },
        "env": {
            "node": true,
            "es6": true
        },
        "rules": {
            "no-console": 0,
            "no-unused-vars": 1
        }
    },
    "dependencies": {
        "axios": "^0.24.0",
        "bcrypt-nodejs": "0.0.3",
        "cors": "^2.4.1",
        "dotenv": "^10.0.0",
        "express": "^4.8.5",
        "express-fileupload": "^1.4.0",
        "express-jwt": "^6.1.0",
        "express-validator": "^6.13.0",
        "faker": "^4.1.0",
        "i": "^0.3.6",
        "install": "^0.12.1",
        "jsonwebtoken": "^5.7.0",
        "jwt-decode": "^2.2.0",
        "lodash": "^4.17.21",
        "minimist": "^1.2.6",
        "moment": "^2.30.1",
        "morgan": "^1.9.1",
        "mustache": "^3.2.1",
        "mysql2": "^2.2.5",
        "npm": "^7.20.6",
        "passport": "^0.4.0",
        "passport-jwt": "^4.0.0",
        "passport-local": "^1.0.0",
        "sequelize": "^6.11.0",
        "sequelize-cli": "^6.3.0",
        "sha256": "^0.2.0",
        "uuid": "^3.4.0"
    },
    "devDependencies": {
        "chai": "^3.2.0",
        "chai-http": "^4.3.0",
        "eslint": "^5.8.0",
        "mocha": "^9.1.3",
        "nyc": "^14.1.1",
        "prettier": "^1.18.2"
    }
}


================================================
FILE: api/postman.json
================================================
{
	"info": {
		"_postman_id": "994858ef-e55e-425c-9aac-1cf12496a933",
		"name": "s3-api-Boilerplate",
		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
	},
	"item": [
		{
			"name": "Auth",
			"item": [
				{
					"name": "/auth",
					"event": [
						{
							"listen": "prerequest",
							"script": {
								"exec": [
									""
								],
								"type": "text/javascript"
							}
						}
					],
					"protocolProfileBehavior": {
						"disableBodyPruning": true
					},
					"request": {
						"method": "GET",
						"header": [
							{
								"key": "Authorization",
								"value": "Bearer {{accessToken}}",
								"type": "text"
							}
						],
						"body": {
							"mode": "urlencoded",
							"urlencoded": []
						},
						"url": {
							"raw": "{{hostname}}/_authcheck",
							"host": [
								"{{hostname}}"
							],
							"path": [
								"_authcheck"
							]
						},
						"description": "The body must have `username` and `password`. It returns `id_token` and `access_token` are signed with the secret located at the `config.json` file. The `id_token` will contain the `username` and the `extra` information sent, while the `access_token` will contain the `audience`, `jti`, `issuer` and `scope`."
					},
					"response": []
				},
				{
					"name": "/auth/login",
					"event": [
						{
							"listen": "test",
							"script": {
								"exec": [
									"var jsonData = pm.response.json()",
									"pm.collectionVariables.set(\"accessToken\", jsonData.data.accessToken);",
									""
								],
								"type": "text/javascript"
							}
						},
						{
							"listen": "prerequest",
							"script": {
								"exec": [
									""
								],
								"type": "text/javascript"
							}
						}
					],
					"request": {
						"method": "POST",
						"header": [
							{
								"key": "Content-Type",
								"value": "application/x-www-form-urlencoded"
							}
						],
						"body": {
							"mode": "urlencoded",
							"urlencoded": [
								{
									"key": "email",
									"value": "user@example.com",
									"type": "text"
								},
								{
									"key": "password",
									"value": "password",
									"type": "text"
								}
							]
						},
						"url": {
							"raw": "{{hostname}}/auth/login",
							"host": [
								"{{hostname}}"
							],
							"path": [
								"auth",
								"login"
							]
						},
						"description": "The body must have `username` and `password`. It returns `id_token` and `access_token` are signed with the secret located at the `config.json` file. The `id_token` will contain the `username` and the `extra` information sent, while the `access_token` will contain the `audience`, `jti`, `issuer` and `scope`."
					},
					"response": []
				},
				{
					"name": "/auth/sign-up",
					"event": [
						{
							"listen": "test",
							"script": {
								"exec": [
									"var jsonData = pm.response.json()",
									"pm.collectionVariables.set(\"accessToken\", jsonData.data.accessToken);",
									""
								],
								"type": "text/javascript"
							}
						}
					],
					"request": {
						"method": "POST",
						"header": [
							{
								"warning": "This is a duplicate header and will be overridden by the Content-Type header generated by Postman.",
								"key": "Content-Type",
								"value": "application/json"
							}
						],
						"body": {
							"mode": "urlencoded",
							"urlencoded": [
								{
									"key": "email",
									"value": "anthonybudd@example.com",
									"type": "text"
								},
								{
									"key": "password",
									"value": "password",
									"type": "text"
								},
								{
									"key": "firstName",
									"value": "Anthony",
									"type": "text"
								},
								{
									"key": "lastName",
									"value": "Budd",
									"type": "text"
								},
								{
									"key": "groupName",
									"value": "GitHub",
									"type": "text"
								},
								{
									"key": "tos",
									"value": "2021-21-19",
									"type": "text"
								}
							]
						},
						"url": {
							"raw": "{{hostname}}/auth/sign-up",
							"host": [
								"{{hostname}}"
							],
							"path": [
								"auth",
								"sign-up"
							]
						},
						"description": "The body must have `username` and `password`. It returns `id_token` and `access_token` are signed with the secret located at the `config.json` file. The `id_token` will contain the `username` and the `extra` information sent, while the `access_token` will contain the `audience`, `jti`, `issuer` and `scope`."
					},
					"response": []
				}
			]
		}
	],
	"event": [
		{
			"listen": "prerequest",
			"script": {
				"type": "text/javascript",
				"exec": [
					""
				]
			}
		},
		{
			"listen": "test",
			"script": {
				"type": "text/javascript",
				"exec": [
					""
				]
			}
		}
	],
	"variable": [
		{
			"key": "hostname",
			"value": "http://localhost:8888/api/v1"
		},
		{
			"key": "accessToken",
			"value": ""
		}
	]
}

================================================
FILE: api/requests.http
================================================
# Install VS Code extension rest-client 
# URL: https://marketplace.visualstudio.com/items?itemName=humao.rest-client

@host=http://localhost:8888/api/v1
# @host=http://api.local/api/v1
@AccessToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpZCI6ImM0NjQ0NzMzLWRlZWEtNDdkOC1iMzVhLTg2ZjMwZmY5NjE4ZSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImZpcnN0TmFtZSI6IlVzZXIiLCJsYXN0TmFtZSI6Ik9uZSIsImlhdCI6MTcxNTIxMzk0MiwiZXhwIjozNDMwNTE0MjgzfQ.khGH3zHxztsWmpwL9bWpwGr_VXcPFxGTCtgoCYJq9tz0H638kWKH_k_zLgjCQ1rD6N0fWh31pTE4l53RgUGz2iL8lAoYmq0ScwSgMmiWMKm6d1vxaN3UK0CivvZPku2Pn4MQ6p12xrfRxTUVCzxI_xP9hHEhG1VUbCA07JJnl-OJFQCwYVQWCmdK5daFe8wybddYLUCG0oAGpy7Kaf0_CBbJAeIccVCKI7fILgBxowVTwl7nqruzr3-k0biXuitkegNfHPyPwbs4AvIIYxdyLXZiT-Zz0JUazphQZncw4WBqB_PX4Eyoflf8xzQNRtgvdV3ANc6ZKeMG05jAp1IV3A

###########################################
# Auth

POST {{host}}/auth/login
content-type: application/json

{
    "email": "user@example.com",
    "password": "Password@1234"
}

### Check auth
GET {{host}}/_authcheck
Authorization: Bearer {{AccessToken}}


###########################################
# Buckets

GET {{host}}/buckets
Authorization: Bearer {{AccessToken}}


### Create Bucket
POST {{host}}/buckets
Authorization: Bearer {{AccessToken}}
content-type: application/json

{
    "namespace": "x--xxctest",
    "name": "x-0testx"
}

### Delete Bucket
DELETE {{host}}/buckets/fae8a1fb-bc90-4565-b567-1fe6846544de
Authorization: Bearer {{AccessToken}}


================================================
FILE: api/src/database/migrations/20180726090304-create-Users.js
================================================
module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.createTable('Users', {
        id: {
            type: Sequelize.UUID,
            defaultValue: Sequelize.UUIDV4,
            primaryKey: true,
            allowNull: false,
            unique: true
        },

        email: {
            type: Sequelize.STRING,
            allowNull: false,
            unique: true
        },
        password: Sequelize.STRING,

        firstName: Sequelize.STRING,
        lastName: Sequelize.STRING,
        bio: Sequelize.TEXT,

        tos: Sequelize.STRING,
        inviteKey: Sequelize.STRING,
        passwordResetKey: Sequelize.STRING,
        emailVerificationKey: Sequelize.STRING,
        emailVerified: {
            type: Sequelize.BOOLEAN,
            defaultValue: false,
            allowNull: false,
        },

        lastLoginAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
        createdAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
        updatedAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
    }),
    down: (queryInterface, Sequelize) => queryInterface.dropTable('Users'),
};


================================================
FILE: api/src/database/migrations/20180726090404-create-Groups.js
================================================
module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.createTable('Groups', {
        id: {
            type: Sequelize.UUID,
            defaultValue: Sequelize.UUIDV4,
            primaryKey: true,
            allowNull: false,
            unique: true
        },

        name: Sequelize.STRING,
        ownerID: Sequelize.UUID,

        createdAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
        updatedAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
        deletedAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
    }),
    down: (queryInterface, Sequelize) => queryInterface.dropTable('Groups')
};


================================================
FILE: api/src/database/migrations/20180726090405-create-GroupsUsers.js
================================================
module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.createTable('GroupsUsers', {
        id: {  // Not used. required by msq system var sql_require_primary_key
            type: Sequelize.UUID,
            defaultValue: Sequelize.UUIDV4,
            primaryKey: true,
            allowNull: false,
            unique: true
        },
        groupID: {
            type: Sequelize.UUID,
        },
        userID: {
            type: Sequelize.UUID,
        },

        createdAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
    }).then(() => queryInterface.addConstraint('GroupsUsers', {
        fields: ['groupID', 'userID'],
        type: 'unique',
        name: 'groupID_userID_index'
    })),
    down: (queryInterface, Sequelize) => queryInterface.dropTable('GroupsUsers'),
};

================================================
FILE: api/src/database/migrations/20240411041313-create-Buckets.js
================================================
module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.createTable('Buckets', {
        id: {
            type: Sequelize.UUID,
            defaultValue: Sequelize.UUIDV4,
            primaryKey: true,
            allowNull: false,
            unique: true
        },

        createdAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
        updatedAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
        deletedAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },

        userID: {
            type: Sequelize.UUID,
            allowNull: true,
        },

        namespace: {
            type: Sequelize.STRING,
            allowNull: false,
        },
        name: {
            type: Sequelize.STRING,
            allowNull: false,
        },

        status: {
            type: Sequelize.STRING,
            allowNull: false,
        },
        bucketCreated: {
            type: Sequelize.BOOLEAN,
            allowNull: false,
            defaultValue: false,
        },
        endpoint: {
            type: Sequelize.STRING,
            allowNull: false,
        },

        stdout: {
            type: Sequelize.TEXT,
            allowNull: true,
        },
        stderr: {
            type: Sequelize.TEXT,
            allowNull: true,
        },
    }),
    down: (queryInterface, Sequelize) => queryInterface.dropTable('Buckets'),
};


================================================
FILE: api/src/database/migrations/20240430101608-create-Blacklist.js
================================================
module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.createTable('Blacklist', {
        id: {
            type: Sequelize.UUID,
            defaultValue: Sequelize.UUIDV4,
            primaryKey: true,
            allowNull: false,
            unique: true
        },

        value: {
            type: Sequelize.STRING,
            allowNull: false,
        },

        createdAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
        updatedAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
    }),
    down: (queryInterface, Sequelize) => queryInterface.dropTable('Blacklist'),
};


================================================
FILE: api/src/database/seeders/20180726092449-Users.js
================================================
const bcrypt = require('bcrypt-nodejs');
const moment = require('moment');
const faker = require('faker');

const insert = [{
    id: 'c4644733-deea-47d8-b35a-86f30ff9618e',
    email: 'user@example.com',
    password: bcrypt.hashSync('Password@1234', bcrypt.genSaltSync(10)),
    firstName: 'User',
    lastName: 'One',
    tos: 'tos-version-2023-07-13',
    createdAt: moment().format('YYYY-MM-DD HH:mm:ss'),
    updatedAt: moment().format('YYYY-MM-DD HH:mm:ss'),
}, {
    id: 'd700932c-4a11-427f-9183-d6c4b69368f9',
    email: 'other.user@foobar.com',
    password: bcrypt.hashSync('Password@1234', bcrypt.genSaltSync(10)),
    firstName: faker.name.firstName(),
    lastName: faker.name.lastName(),
    tos: 'tos-version-2023-07-13',
    inviteKey: '86f30ff9618e',
    createdAt: moment().format('YYYY-MM-DD HH:mm:ss'),
    updatedAt: moment().format('YYYY-MM-DD HH:mm:ss'),
}];


module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.bulkInsert('Users', insert).catch(err => console.log(err)),
    down: (queryInterface, Sequelize) => { }
};


================================================
FILE: api/src/database/seeders/20180726093449-Group.js
================================================
const moment = require('moment');

const insert = [{
    id: 'fdab7a99-2c38-444b-bcb3-f7cef61c275b',
    ownerID: 'c4644733-deea-47d8-b35a-86f30ff9618e',
    name: 'Group A',
    createdAt: moment().format('YYYY-MM-DD HH:mm:ss'),
    updatedAt: moment().format('YYYY-MM-DD HH:mm:ss'),
}, {
    id: 'be1fcb4e-caf9-41c2-ac27-c06fa24da36a',
    ownerID: 'd700932c-4a11-427f-9183-d6c4b69368f9',
    name: 'Group B',
    createdAt: moment().format('YYYY-MM-DD HH:mm:ss'),
    updatedAt: moment().format('YYYY-MM-DD HH:mm:ss'),
}];


module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.bulkInsert('Groups', insert).catch(err => console.log(err)),
    down: (queryInterface, Sequelize) => { }
};


================================================
FILE: api/src/database/seeders/20180726093449-GroupsUsers.js
================================================
const moment = require('moment');

const insert = [
    {
        id: '1872dcde-b79d-4f28-a36b-a22af519ac23',
        userID: 'c4644733-deea-47d8-b35a-86f30ff9618e',
        groupID: 'fdab7a99-2c38-444b-bcb3-f7cef61c275b',
        createdAt: moment().format('YYYY-MM-DD HH:mm:ss'),
    },
    {
        id: 'f4444505-cec7-4f91-948f-cdf3d4471c9e',
        userID: 'c4644733-deea-47d8-b35a-86f30ff9618e',
        groupID: 'be1fcb4e-caf9-41c2-ac27-c06fa24da36a',
        createdAt: moment().add(1, 'min').format('YYYY-MM-DD HH:mm:ss'),
    },
    {
        id: 'ed748a2d-453b-4bc8-b80d-bf1056e2b920',
        userID: 'd700932c-4a11-427f-9183-d6c4b69368f9',
        groupID: 'be1fcb4e-caf9-41c2-ac27-c06fa24da36a',
        createdAt: moment().format('YYYY-MM-DD HH:mm:ss'),
    }
];


module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.bulkInsert('GroupsUsers', insert).catch(err => console.log(err)),
    down: (queryInterface, Sequelize) => { }
};


================================================
FILE: api/src/database/seeders/20240411041313-Buckets.js
================================================
const moment = require('moment');

const insert = [{
    id: 'fae8a1fb-bc90-4565-b567-1fe6846544de',
    createdAt: moment().format('YYYY-MM-DD HH:mm:ss'),
    updatedAt: moment().format('YYYY-MM-DD HH:mm:ss'),
    userID: 'c4644733-deea-47d8-b35a-86f30ff9618e',
    namespace: 'test-bucket',
    name: 'test-bucket',
    status: 'Provisioned',
    bucketCreated: 1,
    endpoint: `test-bucket.${process.env.S3_ROOT}`,
}];

module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.bulkInsert('Buckets', insert).catch(err => console.log(err)),
    down: (queryInterface, Sequelize) => { }
};


================================================
FILE: api/src/database/seeders/20240430101608-Blacklist.js
================================================
const { v4: uuidv4 } = require('uuid');

const blacklist = [
    'about',
    'aboutu',
    'abuse',
    'acme',
    'ad',
    'admanager',
    'admin',
    'admindashboard',
    'administrator',
    'ads',
    'adsense',
    'adult',
    'adword',
    'affiliate',
    'affiliatepage',
    'afp',
    'alpha',
    'anal',
    'analytic',
    'android',
    'answer',
    'anu',
    'anus',
    'ap',
    'api',
    'app',
    'appengine',
    'application',
    'appnew',
    'arse',
    'asdf',
    'a',
    'as',
    'ass',
    'asset',
    'asshole',
    'atf',
    'backup',
    'ball',
    'balls',
    'ballsack',
    'bank',
    'base',
    'bastard',
    'beginner',
    'beta',
    'biatch',
    'billing',
    'binarie',
    'binary',
    'bitch',
    'biz',
    'blackberry',
    'blog',
    'blogsearch',
    'bloody',
    'blowjob',
    'blowjobs',
    'bollock',
    'boner',
    'boob',
    'boobs',
    'book',
    'bugger',
    'bum',
    'butt',
    'buttplug',
    'buy',
    'buzz',
    'c',
    'cache',
    'calendar',
    'cart',
    'catalog',
    'ceo',
    'chart',
    'chat',
    'checkout',
    'ci',
    'cia',
    'client',
    'clitori',
    'clitoris',
    'cname',
    'cnarne',
    'cock',
    'code',
    'community',
    'confirm',
    'confirmation',
    'contact',
    'contact-u',
    'contactu',
    'content',
    'controlpanel',
    'coon',
    'core',
    'corp',
    'countrie',
    'country',
    'cp',
    'cpanel',
    'crap',
    'cs',
    'cunt',
    'cv',
    'damn',
    'dashboard',
    'data',
    'demo',
    'deploy',
    'deployment',
    'desktop',
    'dev',
    'devel',
    'developement',
    'developer',
    'development',
    'dick',
    'dike',
    'dildo',
    'dir',
    'directory',
    'discussion',
    'dl',
    'doc',
    'document',
    'donate',
    'download',
    'dyke',
    'e',
    'earth',
    'email',
    'enable',
    'encrypted',
    'engine',
    'error',
    'errorlog',
    'fag',
    'faggot',
    'fbi',
    'feature',
    'feck',
    'feed',
    'feedburner',
    'feedproxy',
    'felching',
    'fellate',
    'fellatio',
    'file',
    'finance',
    'flange',
    'folder',
    'forgotpassword',
    'forum',
    'friend',
    'ftp',
    'fuck',
    'fudgepacker',
    'fun',
    'fusion',
    'gadget',
    'gear',
    'geographic',
    'gettingstarted',
    'git',
    'gitlab',
    'gmail',
    'go',
    'goddamn',
    'goto',
    'gov',
    'graph',
    'group',
    'hell',
    'help',
    'home',
    'homo',
    'html',
    'htrnl',
    'http',
    'i',
    'image',
    'img',
    'investor',
    'invoice',
    'io',
    'ios',
    'ipad',
    'iphone',
    'irnage',
    'irng',
    'item',
    'j',
    'jenkin',
    'jerk',
    'jira',
    'jizz',
    'job',
    'join',
    'js',
    'knobend',
    'lab',
    'labia',
    'legal',
    'lesbo',
    'list',
    'lmao',
    'lmfao',
    'local',
    'locale',
    'location',
    'log',
    'login',
    'logout',
    'm',
    'mail',
    'manage',
    'manager',
    'map',
    'marketing',
    'me',
    'media',
    'message',
    'misc',
    'mm',
    'mms',
    'mobile',
    'model',
    'money',
    'movie',
    'muff',
    'my',
    'mystore',
    'n',
    'net',
    'network',
    'new',
    'newsite',
    'nigga',
    'nigger',
    'npm',
    'ns',
    'omg',
    'online',
    'order',
    'org',
    'other',
    'p0rn',
    'pack',
    'packagist',
    'page',
    'partner',
    'partnerpage',
    'password',
    'payment',
    'peni',
    'penis',
    'people',
    'person',
    'pi',
    'pis',
    'piss',
    'place',
    'podcast',
    'policy',
    'poop',
    'pop',
    'pop3',
    'popular',
    'porn',
    'pr0n',
    'pricing',
    'prick',
    'print',
    'privacy',
    'private',
    'prod',
    'product',
    'production',
    'profile',
    'promo',
    'promotion',
    'proxie',
    'proxies',
    'proxy',
    'pube',
    'public',
    'purchase',
    'pussy',
    'queer',
    'querie',
    'queries',
    'query',
    'r',
    'radio',
    'random',
    'reader',
    'recover',
    'redirect',
    'register',
    'registration',
    'release',
    'report',
    'research',
    'resolve',
    'resolver',
    'rnail',
    'rnicrosoft',
    'root',
    'rs',
    'rss',
    'sale',
    'sandbox',
    'scholar',
    'scrotum',
    'search',
    'secure',
    'seminar',
    'server',
    'service',
    'sex',
    'sftp',
    'sh1t',
    'shit',
    'shop',
    'shopping',
    'shortcut',
    'signin',
    'signup',
    'site',
    'sitemap',
    'sitenew',
    'sketchup',
    'sky',
    'slash',
    'slashinvoice',
    'slut',
    'sm',
    'smegma',
    'sms',
    'smtp',
    'soap',
    'software',
    'sorry',
    'spreadsheet',
    'spunk',
    'srntp',
    'ssh',
    'ssl',
    'stage',
    'staging',
    'stat',
    'static',
    'statistic',
    'statu',
    'store',
    'suggest',
    'suggestquerie',
    'suggestquery',
    'support',
    'survey',
    'surveytool',
    'svn',
    'sync',
    'sysadmin',
    'talk',
    'talkgadget',
    'test',
    'tester',
    'testing',
    'text',
    'tit',
    'tits',
    'tool',
    'toolbar',
    'tosser',
    'trac',
    'translate',
    'translation',
    'translator',
    'trend',
    'turd',
    'twat',
    'txt',
    'ul',
    'upload',
    'vagina',
    'validation',
    'vid',
    'video',
    'video-stat',
    'voice',
    'w',
    'wank',
    'wave',
    'webdisk',
    'webmail',
    'webmaster',
    'webrnail',
    'whm',
    'whoi',
    'whore',
    'wifi',
    'wiki',
    'wtf',
    'ww',
    'www',
    'wwww',
    'xhtml',
    'xhtrnl',
    'xml',
    'xxx',
];


module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.bulkInsert('Blacklist', blacklist.map((value) => ({
        id: uuidv4(),
        value,
    }))).catch(err => console.log(err)),
    down: (queryInterface, Sequelize) => { }
};


================================================
FILE: api/src/index.js
================================================
require('dotenv').config();
require('./providers/passport');
const fileUpload = require('express-fileupload');
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');


console.log('*************************************');
console.log('* Express API Boilerplate');
console.log('*');
console.log('* ENV');
console.log(`* NODE_ENV: ${process.env.NODE_ENV}`);
console.log(`* TEMP_FILE_DIR: ${process.env.TEMP_FILE_DIR}`);
if (!process.env.H_CAPTCHA_SECRET) console.log(`* H_CAPTCHA_SECRET: null ⚠️  Login/Sign-up requests will not require captcha validadation!`);
console.log('*');
console.log('*');


////////////////////////////////////////////////
// Express
const app = express();
app.disable('x-powered-by');
app.use(cors({
    origin: '*',
    credentials: true,
    allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(fileUpload({
    limits: { fileSize: 50 * 1024 * 1024 },
    tempFileDir: process.env.TEMP_FILE_DIR,
    useTempFiles: true,
    parseNested: true,
}));
app.get('/_readiness', (req, res) => res.send('healthy'));
app.get('/api/v1/_healthcheck', (req, res) => res.json({ status: 'ok' }));
if (typeof global.it !== 'function') app.use(morgan('[:date[iso]] HTTP/:http-version :status :method :url :response-time ms'));


////////////////////////////////////////////////
// HTTP
app.use('/api/v1/', require('./routes/auth'));
app.use('/api/v1/', require('./routes/user'));
app.use('/api/v1/', require('./routes/groups'));
app.use('/api/v1/', require('./routes/Buckets')); // AB: gen


////////////////////////////////////////////////
// Listen
let port = process.env.PORT || 80;
if (typeof global.it === 'function') port = 7777;
app.listen(port, () => console.log(`* Listening: http://127.0.0.1:${port}`));
module.exports = app;


================================================
FILE: api/src/models/Blacklist.js
================================================
const Sequelize = require('sequelize');
const db = require('./../providers/db');

const Blacklist = db.define('Blacklist', {
    id: {
        type: Sequelize.UUID,
        defaultValue: Sequelize.UUIDV4,
        primaryKey: true,
        allowNull: false,
        unique: true
    },

    value: {
        type: Sequelize.STRING,
        allowNull: false,
    },

    createdAt: {
        type: Sequelize.DATE,
        allowNull: true,
    },
    updatedAt: {
        type: Sequelize.DATE,
        allowNull: true,
    },

}, {
    tableName: 'Blacklist',
    defaultScope: {
        attributes: {
            exclude: [

            ]
        }
    },
});

module.exports = Blacklist;

================================================
FILE: api/src/models/Bucket.js
================================================
const { exec } = require('child_process');
const db = require('./../providers/db');
const Sequelize = require('sequelize');
const tmp = require('tmp');
const fs = require('fs');

const Bucket = db.define('Bucket', {
    id: {
        type: Sequelize.UUID,
        defaultValue: Sequelize.UUIDV4,
        primaryKey: true,
        allowNull: false,
        unique: true
    },

    createdAt: {
        type: Sequelize.DATE,
        allowNull: true,
    },
    updatedAt: {
        type: Sequelize.DATE,
        allowNull: true,
    },
    deletedAt: {
        type: Sequelize.DATE,
        allowNull: true,
    },

    userID: {
        type: Sequelize.UUID,
        allowNull: true,
    },

    namespace: {
        type: Sequelize.STRING,
        allowNull: false,
    },
    name: {
        type: Sequelize.STRING,
        allowNull: false,
    },

    status: {
        type: Sequelize.STRING,
        allowNull: false,
    },
    bucketCreated: {
        type: Sequelize.BOOLEAN,
        allowNull: false,
        defaultValue: false,
    },
    endpoint: {
        type: Sequelize.STRING,
        allowNull: false,
    },

    stdout: {
        type: Sequelize.TEXT,
        allowNull: true,
    },
    stderr: {
        type: Sequelize.TEXT,
        allowNull: true,
    },
}, {
    tableName: 'Buckets',
    paranoid: true,
    defaultScope: {
        attributes: {
            exclude: []
        }
    },
});

Bucket.prototype.createK3sAssets = async function () {
    const generateAccessKeyID = () => {
        const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz23456789';
        const length = 20;
        let randomString = '';
        for (let i = 0; i < length; i++) {
            const randomIndex = Math.floor(Math.random() * charSet.length);
            randomString += charSet.charAt(randomIndex);
        }
        return randomString;
    };

    const generateSecretAccessKey = () => {
        const length = 40;
        const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/';
        let randomString = '';
        for (let i = 0; i < length; i++) {
            const randomIndex = Math.floor(Math.random() * charset.length);
            randomString += charset.charAt(randomIndex);
        }
        return randomString;
    };

    const accessKeyID = generateAccessKeyID();
    const secretAccessKey = generateSecretAccessKey();

    tmp.file((err, path) => {
        if (err) throw err;
        fs.readFile('/app/src/providers/bucket.yml', 'utf8', (err, data) => {
            if (err) throw err;

            const result = data.replace(/NAMESPACE_HERE/g, this.namespace)
                .replace(/BUCKETNAME_HERE/g, this.name)
                .replace(/ROOTUSER/g, accessKeyID)
                .replace(/ROOTPASSWORD/g, secretAccessKey);

            fs.writeFile(path, result, 'utf8', (err) => {
                if (err) throw err;

                exec(`kubectl --kubeconfig=${process.env.K8S_CONFIG_PATH} apply -f ${path}`, (err, stdout, stderr) => {
                    if (err) console.error(err);

                    console.log(`stdout: ${stdout}`);
                    console.log(`stderr: ${stderr}`);

                    let status = 'Provisioning';
                    if (stderr) status = 'Error';

                    this.update({
                        status,
                        stdout,
                        stderr,
                    });
                });
            });
        });
    });

    return {
        accessKeyID,
        secretAccessKey
    };
};

Bucket.prototype.createBucket = async function () {
    const command = `kubectl --kubeconfig=${process.env.K8S_CONFIG_PATH} -n ${this.namespace} exec minio-pod -- ./s3-create-bucket-script/create-bucket.sh`;
    console.log(command);
    exec(command, (err, stdout, stderr) => {
        if (err) console.error(err);

        console.log(`stdout: ${stdout}`);
        console.log(`stderr: ${stderr}`);

        if (stderr) {
            this.update({
                status: 'Error',
                createStderr: `2: ${stderr}`,
            });
        } else {
            this.update({ bucketCreated: true });
        }
    });
};

Bucket.prototype.sync = async function () {
    if (this.status !== 'Error') {
        exec(`kubectl --kubeconfig=${process.env.K8S_CONFIG_PATH} -n ${this.namespace} get pod minio-pod --no-headers -o custom-columns=":status.phase"`, (err, stdout, stderr) => {
            if (err) console.error(err);

            console.log(`stdout: ${stdout}`);
            console.log(`stderr: ${stderr}`);

            switch (stdout.trim()) {
                case 'Running':
                    if (this.status !== 'Provisioned') this.update({ status: 'Provisioned' });
                    if (!this.bucketCreated) this.createBucket();
                    break;
            }
        });
    }
};

Bucket.prototype.deleteK3sAssets = async function () {
    exec(`kubectl --kubeconfig=${process.env.K8S_CONFIG_PATH} -n ${this.namespace} delete pod/minio-pod service/minio-svc ingress/minio-ing persistentvolumeclaim/minio-pvc namespace/${this.namespace}`, (err, stdout, stderr) => {
        if (err) console.error(err);
        if (stderr) console.log(`stderr: ${stderr}`);
        console.log(`stdout: ${stdout}`);

        this.update({
            stdout,
            stderr,
        });
    });
};

module.exports = Bucket;

================================================
FILE: api/src/models/Group.js
================================================
const Sequelize = require('sequelize');
const db = require('./../providers/db');

module.exports = db.define('Group', {
    id: {
        type: Sequelize.UUID,
        defaultValue: Sequelize.UUIDV4,
        primaryKey: true,
        allowNull: false,
        unique: true
    },

    name: Sequelize.STRING,
    ownerID: Sequelize.UUID,

    deletedAt: {
        type: Sequelize.DATE,
        allowNull: true,
    },
}, {
    tableName: 'Groups',
    paranoid: true,
});


================================================
FILE: api/src/models/GroupsUsers.js
================================================
const Sequelize = require('sequelize');
const db = require('./../providers/db');

module.exports = db.define('GroupsUsers', {
    id: {  // Not used. required by msq system var sql_require_primary_key
        type: Sequelize.UUID,
        defaultValue: Sequelize.UUIDV4,
        primaryKey: true,
        allowNull: false,
        unique: true
    },
    userID: Sequelize.UUID,
    groupID: Sequelize.UUID,
}, {
    tableName: 'GroupsUsers',
    updatedAt: false,
});


================================================
FILE: api/src/models/User.js
================================================
const Sequelize = require('sequelize');
const db = require('./../providers/db');

module.exports = db.define('User', {
    id: {
        type: Sequelize.UUID,
        defaultValue: Sequelize.UUIDV4,
        primaryKey: true,
        allowNull: false,
        unique: true
    },

    email: {
        type: Sequelize.STRING,
        allowNull: false,
        unique: true
    },
    password: Sequelize.STRING,

    firstName: Sequelize.STRING,
    lastName: Sequelize.STRING,
    bio: Sequelize.TEXT,

    tos: Sequelize.STRING,
    inviteKey: Sequelize.STRING,
    passwordResetKey: Sequelize.STRING,
    emailVerificationKey: Sequelize.STRING,
    emailVerified: {
        type: Sequelize.BOOLEAN,
        defaultValue: false,
        allowNull: false,
    },

    lastLoginAt: {
        type: Sequelize.DATE,
        allowNull: true,
    },
}, {
    tableName: 'Users',
    defaultScope: {
        attributes: {
            exclude: [
                'password',
                'passwordResetKey',
            ]
        }
    },
});


================================================
FILE: api/src/models/index.js
================================================
const User = require('./User');
const Group = require('./Group');
const GroupsUsers = require('./GroupsUsers');
const Bucket = require('./Bucket');
const Blacklist = require('./Blacklist');


User.belongsToMany(Group, {
    through: GroupsUsers,
    foreignKey: 'userID',
    otherKey: 'groupID',
});
Group.belongsToMany(User, {
    through: GroupsUsers,
    foreignKey: 'groupID',
    otherKey: 'userID',
});


module.exports = {
    User,
    Group,
    GroupsUsers,
    Bucket,
    Blacklist,
};


================================================
FILE: api/src/providers/bucket.yml
================================================
apiVersion: v1
kind: Namespace
metadata:
  name: NAMESPACE_HERE
  labels:
    name: NAMESPACE_HERE
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: minio-pod
  name: minio-pod
  namespace: NAMESPACE_HERE
spec:
  containers:
  - name: minio-pod
    image: quay.io/minio/minio:latest
    env:
    - name: MINIO_ROOT_USER
      value: ROOTUSER
    - name: MINIO_ROOT_PASSWORD
      value: ROOTPASSWORD
    - name: S3_NAMESPACE
      value: NAMESPACE_HERE
    - name: S3_BUCKET_NAME
      value: BUCKETNAME_HERE
    command:
    - /bin/bash
    - -c
    args: 
    - minio server /data --console-address :9001
    ports:
    - name: http
      containerPort: 80
    - name: https
      containerPort: 443
    - name: console
      containerPort: 9001
    - name: api
      containerPort: 9000
    volumeMounts:
    - name: longhornvolume
      mountPath: /data
    - name: s3-create-bucket-script
      mountPath: /s3-create-bucket-script
  volumes:
  - name: s3-create-bucket-script
    configMap:
      name: s3-create-bucket-script
      defaultMode: 0777
      items:
      - key: create-bucket.sh
        path: create-bucket.sh
  - name: longhornvolume
    persistentVolumeClaim:
      claimName: minio-pvc
---
apiVersion: v1
kind: Service  
metadata:
  name: minio-svc
  namespace: NAMESPACE_HERE 
spec:
  selector:
    app: minio-pod 
  ports:
  - name: http
    protocol: TCP  
    port: 80 
    targetPort: 9001
  - name: api
    port: 9000
    protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: NAMESPACE_HERE
  name: minio-ing
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    kubernetes.io/ingress.class: "traefik"
spec:
  tls:
  - hosts:
    - NAMESPACE_HERE.s3.anthonybudd.io
    secretName: NAMESPACE_HERE-s3-anthonybudd-io-cert
  - hosts:
    - BUCKETNAME_HERE.NAMESPACE_HERE.s3.anthonybudd.io
    secretName: BUCKETNAME_HERE-NAMESPACE_HERE-s3-anthonybudd-io-cert
  rules:
  - host: NAMESPACE_HERE.s3.anthonybudd.io
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: minio-svc
            port:
              number: 80
  - host: BUCKETNAME_HERE.NAMESPACE_HERE.s3.anthonybudd.io
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: minio-svc
            port:
              number: 9000
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: minio-pvc
  namespace: NAMESPACE_HERE
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 5Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: s3-create-bucket-script
  namespace: NAMESPACE_HERE
data:
  create-bucket.sh: |
    #!/bin/bash

    mc alias set local http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"
    mc mb local/"$S3_BUCKET_NAME"

================================================
FILE: api/src/providers/connections.js
================================================
require('dotenv').config();

module.exports = {
    development: {
        username: process.env.DB_USERNAME,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_DATABASE,
        host: process.env.DB_HOST,
        port: process.env.DB_PORT || '3306',
        dialect: 'mysql',
    },
    production: {
        username: process.env.DB_USERNAME,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_DATABASE,
        host: process.env.DB_HOST,
        port: process.env.DB_PORT || '3306',
        dialect: 'mysql',
    },
    test: {
        username: process.env.DB_USERNAME,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_DATABASE,
        host: 's3-api-db-test',
        port: process.env.DB_PORT || '3306',
        dialect: 'mysql',
    }
};


================================================
FILE: api/src/providers/db.js
================================================
const Sequelize = require('sequelize');
const connections = require('./connections');
const errorHandler = require('./errorHandler');


const connection = (typeof global.it === 'function') ? 'test' : (process.env.NODE_ENV || 'development');
const dbHost = connections[connection].host;
const dbPort = connections[connection].port;
const dbName = connections[connection].database;
const dbUser = connections[connection].username;
const dbPassword = connections[connection].password;
const dbDialect = connections[connection].dialect;


const sequelize = new Sequelize(dbName, dbUser, dbPassword, {
    port: dbPort,
    host: dbHost,
    dialect: dbDialect,
    logging: false,
    pool: {
        max: 5,
        min: 0,
        acquire: 30000,
        idle: 10000,
    },
});

sequelize.authenticate()
    .then(() => ((typeof global.it !== 'function') ? console.log('* Sequelize: Connected') : ''))
    .catch(err => errorHandler(err));

module.exports = sequelize;


================================================
FILE: api/src/providers/errorHandler.js
================================================
const crypto = require('crypto');

module.exports = (err, res) => {
    if (err.isAxiosError) {
        console.log(`Axios Error: ${err.request.path}`);
        if (err.response && err.response.data) {
            console.log(err.response.data);
        } else {
            console.error(err);
        }
    } else if (err.response && err.response.body) {
        console.error(err);
        console.error(err.response.body);
    } else {
        console.error(err);
    }

    if (res && !res.headersSent) res.status(500).json({
        msg: `Error`,
        code: crypto.randomBytes(32).toString('base64'),
    });
};


================================================
FILE: api/src/providers/generateJWT.js
================================================
const jwt = require('jsonwebtoken');
const moment = require('moment');
const fs = require('fs');

module.exports = (user, expires) => {
    const payload = {
        id: user.id,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
        displayName: user.displayName,
    };

    let expiresIn = moment(new Date()).add(1, 'day').unix();
    if (Array.isArray(expires) && expires.length === 2) {
        expiresIn = moment(new Date()).add(expires[0], expires[1]).unix();
    } else if (typeof expires === 'string') {
        expiresIn = moment(expires, 'YYYY-MM-DD HH:mmZ').unix();
    }

    return jwt.sign(payload, fs.readFileSync(process.env.PRIVATE_KEY_PATH, 'utf8'), {
        expiresIn,
        algorithm: 'RS512',
    });
};


================================================
FILE: api/src/providers/hCaptcha.js
================================================
const axios = require('axios');
const qs = require('qs');

const hCaptcha = axios.create({
    baseURL: 'https://hcaptcha.com',
});

module.exports = {
    verify: async (response) => await hCaptcha.post('/siteverify',
        qs.stringify({
            response,
            secret: process.env.H_CAPTCHA_SECRET,
        }),
        {
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        }
    ),

    axios: hCaptcha,
};


================================================
FILE: api/src/providers/passport.js
================================================
const LocalStrategy = require('passport-local').Strategy;
const { User, Group } = require('./../models');
const passportJWT = require('passport-jwt');
const ExtractJWT = passportJWT.ExtractJwt;
const JWTStrategy = passportJWT.Strategy;
const bcrypt = require('bcrypt-nodejs');
const passport = require('passport');
const fs = require('fs');


passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'password'
}, async (email, password, cb) => {

    const user = await User.unscoped().findOne({
        where: { email },
        include: [Group]
    });

    if (!user) return cb(null, false, { message: 'Incorrect email or password.' });

    return bcrypt.compare(password, user.password, (err, compare) => {
        if (compare) {
            return cb(null, user, { message: 'Logged in successfully' });
        } else {
            return cb(null, false, { message: 'Incorrect email or password.' });
        }
    });
}));


passport.use(new JWTStrategy({
    jwtFromRequest: ExtractJWT.fromExtractors([
        ExtractJWT.fromAuthHeaderAsBearerToken(),
        ExtractJWT.fromUrlQueryParameter('token'),
    ]),
    secretOrKey: fs.readFileSync(process.env.PUBLIC_KEY_PATH, 'utf8'),
}, (jwtPayload, cb) => cb(null, jwtPayload)));


module.exports = passport;


================================================
FILE: api/src/routes/Buckets.js
================================================
const { body, validationResult, matchedData } = require('express-validator');
const errorHandler = require('./../providers/errorHandler');
const { Bucket, Blacklist } = require('./../models');
const middleware = require('./middleware');
const passport = require('passport');
const express = require('express');


const app = (module.exports = express.Router());


/**
 * GET /api/v1/buckets
 * 
 */
app.get('/buckets', [
    passport.authenticate('jwt', { session: false })
], async (req, res) => {
    try {
        return res.json(await Bucket.findAll({
            where: {
                userID: req.user.id,
            }
        }));
    } catch (error) {
        errorHandler(error, res);
    }
});


/**
 * POST /api/v1/buckets
 * 
 * Create Bucket
 */
app.post('/buckets', [
    passport.authenticate('jwt', { session: false }),
    body('namespace')
        .exists()
        .notEmpty()
        .matches(/^[a-z0-9-_]+$/),

    body('namespace')
        .custom(async (value) => {
            const blacklist = await Blacklist.findOne({ where: { value } });
            if (blacklist) throw new Error('This namespace is not allowed');
        })
        .custom(async (namespace, { req }) => {
            const exists = await Bucket.findOne({
                where: {
                    namespace: req.body.namespace
                }
            });

            if (exists) throw new Error('Namespace already exists.');
        }),

    body('name')
        .exists()
        .notEmpty()
        .matches(/^[a-z0-9-_]+$/),
    body('name')
        .custom(async (value) => {
            const blacklist = await Blacklist.findOne({ where: { value } });
            if (blacklist) throw new Error('This bucket name is not allowed');
        })
        .custom(async (name, { req }) => {
            const exists = await Bucket.findOne({
                where: {
                    name: req.body.name
                }
            });

            if (exists) throw new Error('Bucket already exists.');
        }),
], async (req, res) => {
    try {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });
        const data = matchedData(req);

        const bucket = await Bucket.create({
            userID: req.user.id,
            namespace: data.namespace,
            name: data.name,
            status: 'Provisioning',
            endpoint: `${data.name}.${data.namespace}.${process.env.S3_ROOT}`,
        });

        const { accessKeyID, secretAccessKey } = await bucket.createK3sAssets();

        return res.json({
            ...bucket.get({ plain: true }),
            accessKeyID,
            secretAccessKey,
        });
    } catch (error) {
        return errorHandler(error, res);
    }
});


/**
 * DELETE /api/v1/buckets/:bucketID
 * 
 * Delete Bucket
 */
app.delete('/buckets/:bucketID', [
    passport.authenticate('jwt', { session: false }),
    middleware.canAccessBucket,
], async (req, res) => {
    try {
        const bucket = await Bucket.findByPk(req.params.bucketID);
        bucket.deleteK3sAssets();
        await bucket.destroy();
        return res.json({ id: req.params.bucketID });
    } catch (error) {
        return errorHandler(error, res);
    }
});


================================================
FILE: api/src/routes/auth.js
================================================
const { body, validationResult, matchedData } = require('express-validator');
const { User, Group, GroupsUsers } = require('./../models');
const errorHandler = require('./../providers/errorHandler');
const generateJWT = require('./../providers/generateJWT');
const middleware = require('./middleware');
const bcrypt = require('bcrypt-nodejs');
const passport = require('passport');
const express = require('express');
const uuidv4 = require('uuid/v4');
const moment = require('moment');
const crypto = require('crypto');

const app = (module.exports = express.Router());


/**
 * GET /api/v1/_authcheck
 * 
 * Helper route for testing auth status
 */
app.get('/_authcheck', [
    passport.authenticate('jwt', { session: false })
], (req, res) => res.json({
    auth: true,
    id: req.user.id,
}));


/**
 * POST api/v1/auth/login
 * 
 */
app.post('/auth/login', [
    body('email').notEmpty().toLowerCase(),
    body('password').notEmpty(),
    middleware.hCaptcha,
], async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });

    passport.authenticate('local', { session: false }, (err, user) => {
        if (err) return errorHandler(err, res);
        if (!user) return res.status(401).json('Incorrect email or password');

        req.login(user, { session: false }, (err) => {
            if (err) return errorHandler(err, res);

            res.json({
                accessToken: generateJWT(user)
            });

            User.update({
                lastLoginAt: moment(new Date()).format('YYYY-MM-DD HH:mm:ss'),
            }, {
                where: {
                    id: user.id
                }
            });
        });
    })(req, res);
});


/**
 * POST /api/v1/auth/sign-up
 * 
 */
app.post('/auth/sign-up', [
    body('email')
        .notEmpty()
        .isEmail()
        .trim()
        .toLowerCase()
        .custom(async (email) => {
            const user = await User.findOne({ where: { email } });
            if (user) throw new Error('This email address is taken');
        }),
    body('password', 'Your password must be atleast 7 characters long')
        .notEmpty()
        .isLength({ min: 7 }),
    body('firstName', 'You must provide your first name')
        .notEmpty()
        .exists(),
    body('lastName')
        .optional(),
    body('groupName')
        .optional(),
    body('tos', 'You must accept the Terms of Service to use this platform')
        .exists()
        .notEmpty(),
    middleware.hCaptcha,
], async (req, res) => {
    try {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });
        const data = matchedData(req);

        const userID = uuidv4();
        const groupID = uuidv4();
        const ucFirst = (string) => string.charAt(0).toUpperCase() + string.slice(1);
        const nameArr = data.firstName.split(' ');
        if (!data.lastName && nameArr.length >= 2) {
            data.firstName = nameArr[0];
            data.lastName = nameArr[1];
        }
        if (!data.lastName) data.lastName = '';
        if (!data.groupName) data.groupName = data.firstName.concat("'s Team");

        await Group.create({
            id: groupID,
            name: data.groupName,
            ownerID: userID,
        });

        await GroupsUsers.create({ userID, groupID });

        const user = await User.create({
            id: userID,
            email: data.email,
            password: bcrypt.hashSync(data.password, bcrypt.genSaltSync(10)),
            firstName: ucFirst(data.firstName),
            lastName: ucFirst(data.lastName),
            lastLoginAt: moment().format("YYYY-MM-DD HH:mm:ss"),
            tos: data.tos,
            emailVerificationKey: crypto.randomBytes(20).toString('hex'),
        });

        console.log(`\n\nEMAIL THIS TO THE USER\nEMAIL VERIFICATION LINK: ${process.env.FRONTEND_URL}/validate-email/${user.emailVerificationKey}\n\n`);

        return passport.authenticate('local', { session: false }, (err, user) => {
            if (err) return errorHandler(err, res);
            req.login(user, { session: false }, (err) => {
                if (err) return errorHandler(err, res);
                res.json({
                    accessToken: generateJWT(user)
                });
            });
        })(req, res);
    } catch (error) {
        return errorHandler(error, res);
    }
});


/**
 * GET /api/v1/auth/verify-email/:emailVerificationKey
 * 
 * Verify Email
 */
app.get('/auth/verify-email', async (req, res) => {
    const user = await User.findOne({
        where: {
            emailVerificationKey: req.params.emailVerificationKey
        }
    });

    if (!user) return res.status(404).json({
        msg: 'User not found',
        code: 40402
    });

    await user.update({
        emailVerified: true,
        emailVerificationKey: null,
    });

    return res.json({ success: true });
});


/**
 * POST /api/v1/auth/forgot
 * 
 * Forgot Password
 */
app.post('/auth/forgot', [
    body('email')
        .isEmail()
        .toLowerCase()
        .custom(async (email) => {
            const user = await User.findOne({ where: { email } });
            if (!user) throw new Error('This email address does not exist');
        }),
    middleware.hCaptcha,
], async (req, res) => {

    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });
    const { email } = matchedData(req);

    const user = await User.findOne({ where: { email } });
    if (!user) return res.status(404).json({
        msg: 'User not found',
        code: 40401
    });

    const passwordResetKey = crypto.randomBytes(32).toString('base64').replace(/[^a-zA-Z0-9]/g, '');

    await user.update({ passwordResetKey });

    console.log(`\n\nEMAIL THIS TO THE USER\nPASSWORD RESET LINK: ${process.env.FRONTEND_URL}/reset/${passwordResetKey}\n\n`);

    return res.json({ success: true });
});


/**
 * GET /api/v1/auth/get-user-by-reset-key/:passwordResetKey
 * 
 * Get users email
 */
app.get('/auth/get-user-by-reset-key/:passwordResetKey', async (req, res) => {
    const user = await User.findOne({
        where: {
            passwordResetKey: req.params.passwordResetKey
        },
    });
    if (!user) return res.status(404).send('Not found');

    return res.json({
        id: user.id,
        email: user.email
    });
});


/**
 * POST /api/v1/auth/reset
 * 
 * Update User's Password
 */
app.post('/auth/reset', [
    body('email')
        .isEmail()
        .toLowerCase()
        .custom(async (email) => {
            const user = await User.findOne({ where: { email } });
            if (!user) throw new Error('This email address does not exist');
        }),
    body('password').exists().isLength({ min: 7 }),
    body('passwordResetKey', 'This link has expired')
        .custom(async (passwordResetKey) => {
            if (!passwordResetKey) throw new Error('This link has expired');
            const user = await User.findOne({ where: { passwordResetKey } });
            if (!user) throw new Error('This link has expired');
        }),
    middleware.hCaptcha,
], async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });
    const { email, password, passwordResetKey } = matchedData(req);

    const user = await User.findOne({
        where: { email, passwordResetKey },
        include: [Group],
    });
    if (!user) return res.status(404).send('Not found');

    await user.update({
        password: bcrypt.hashSync(password, bcrypt.genSaltSync(10)),
        passwordResetKey: null,
    });

    return passport.authenticate('local', { session: false }, (err, user) => {
        if (err) return errorHandler(err, res);
        req.login(user, { session: false }, (err) => {
            if (err) return errorHandler(err, res);
            return res.json({ accessToken: generateJWT(user) });
        });
    })(req, res);
});


/**
 * GET /api/v1/auth/get-user-by-invite-key/:inviteKey
 * 
 * Get users email
 */
app.get('/auth/get-user-by-invite-key/:inviteKey', async (req, res) => {
    const user = await User.findOne({
        where: {
            inviteKey: req.params.inviteKey
        },
    });
    if (!user) return res.status(404).send('Not found');

    return res.json({
        id: user.id,
        email: user.email
    });
});


/**
 * POST /api/v1/auth/invite
 * 
 */
app.post('/auth/invite', [
    body('email', 'You must provide your email address')
        .exists({ checkFalsy: true })
        .isEmail()
        .toLowerCase(),
    body('password', 'Your password must be atleast 7 characters long')
        .isLength({ min: 7 }),
    body('firstName', 'You must provide your first name')
        .exists(),
    body('lastName'),
    body('tos', 'You must accept the Terms of Service to use this platform')
        .exists(),
    body('inviteKey').exists(),
    middleware.hCaptcha,
], async (req, res) => {
    try {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });
        const data = matchedData(req);

        const ucFirst = (string) => string.charAt(0).toUpperCase() + string.slice(1);
        const user = await User.findOne({ where: { inviteKey: data.inviteKey } });
        if (!user) return res.status(404).send('Not found');
        await user.update({
            password: bcrypt.hashSync(data.password, bcrypt.genSaltSync(10)),
            firstName: ucFirst(data.firstName),
            lastName: ucFirst(data.lastName),
            lastLoginAt: moment().format('YYYY-MM-DD HH:mm:ss'),
            tos: data.tos,
            inviteKey: null,
            emailVerified: true,
            emailVerificationKey: null,
        });

        return passport.authenticate('local', { session: false }, (err, user) => {
            if (err) return errorHandler(err, res);
            req.login(user, { session: false }, (err) => {
                if (err) return errorHandler(err, res);
                return res.json({ accessToken: generateJWT(user) });
            });
        })(req, res);
    } catch (error) {
        return errorHandler(error, res);
    }
});


================================================
FILE: api/src/routes/groups.js
================================================
const { body, validationResult, matchedData } = require('express-validator');
const { User, Group, GroupsUsers } = require('./../models');
const errorHandler = require('./../providers/errorHandler');
const middleware = require('./middleware');
const passport = require('passport');
const express = require('express');
const crypto = require('crypto');

const app = (module.exports = express.Router());


/**
 * GET /api/v1/groups/:groupID
 *
 */
app.get('/groups/:groupID', [
    passport.authenticate('jwt', { session: false }),
    middleware.isInGroup,
], async (req, res) => {
    try {
        const group = await Group.findByPk(req.params.groupID, {
            include: (req.query.with === 'users') ? [User] : [],
        });

        return res.json(group);
    } catch (error) {
        return errorHandler(error, res);
    }
});


/**
 * POST /api/v1/groups/:groupID
 *
 */
app.post('/groups/:groupID', [
    passport.authenticate('jwt', { session: false }),
    middleware.isInGroup,
    body('name')
], async (req, res) => {
    try {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });
        const data = matchedData(req);

        await Group.update(data, {
            where: {
                id: req.params.groupID
            }
        });

        return res.json(
            await Group.findByPk(req.params.groupID)
        );
    } catch (error) {
        return errorHandler(error, res);
    }
});


/**
 * POST /api/v1/groups/:groupID/users/invite
 *
 */
app.post('/groups/:groupID/users/invite', [
    passport.authenticate('jwt', { session: false }),
    middleware.isGroupOwner,
    body('email').isEmail().toLowerCase(),
], async (req, res) => {
    try {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });
        const { email } = matchedData(req);

        const groupID = req.params.groupID;

        let user = await User.findOne({
            where: { email }
        });

        if (user) {
            if (user.id === req.user.id) return res.status(401).json({
                msg: 'You cannot add yourself to a group',
                code: 98644,
            });

            // Check if relationship already exists
            const relationship = await GroupsUsers.findOne({
                where: {
                    groupID,
                    userID: user.id
                }
            });
            if (relationship) return res.json({
                groupID,
                userID: user.id
            });
        } else {
            try {
                user = await User.create({
                    email,
                    inviteKey: crypto.randomBytes(20).toString('hex'),
                    emailVerificationKey: crypto.randomBytes(20).toString('hex'),
                });

                console.log(`\n\nEMAIL THIS TO THE USER\nINVITE LINK: ${process.env.FRONTEND_URL}/invite/${user.inviteKey}\n\n`);
            } catch (error) {
                errorHandler(error);
            }
        }

        // Delete all first
        await GroupsUsers.destroy({
            where: {
                groupID,
                userID: user.id,
            }
        });

        await GroupsUsers.create({
            groupID,
            userID: user.id,
        });

        return res.json({
            groupID,
            userID: user.id,
        });
    } catch (error) {
        return errorHandler(error, res);
    }
});


/**
 * DELETE /api/v1/groups/:groupID/users/:userID
 *
 */
app.delete('/groups/:groupID/users/:userID', [
    passport.authenticate('jwt', { session: false }),
    middleware.isGroupOwner,
    middleware.isNotSelf,
], async (req, res) => {

    await GroupsUsers.destroy({
        where: {
            groupID: req.params.groupID,
            userID: req.params.userID,
        }
    });

    return res.json({
        userID: req.params.userID,
        groupID: req.params.groupID
    });
});


================================================
FILE: api/src/routes/middleware/canAccessBucket.js
================================================
const { Bucket } = require('./../../models');

module.exports = async (req, res, next) => {
    const bucketID = (req.params.bucketID || req.body.bucketID);
    const bucket = await Bucket.findByPk(bucketID);

    if (!bucket) return res.status(404).json({
        msg: `Bucket does not exists.`,
        code: 97924,
    });

    if (req.user.id === bucket.userID) {
        return next();
    } else {
        return res.status(401).json({
            msg: `You do not have access to bucket.id:${bucketID}`,
            code: 49390,
        });
    }
};


================================================
FILE: api/src/routes/middleware/checkPassword.js
================================================
const errorHandler = require('./../../providers/errorHandler');
const { User } = require('./../../models');
const bcrypt = require('bcrypt-nodejs');

module.exports = (req, res, next) => {
    if (!req.body.password) return res.status(422).json({
        errors: {
            components: {
                location: 'body',
                param: 'password',
                msg: 'Password must be provided'
            }
        }
    });

    User.unscoped().findOne({ where: { id: req.user.id } }).then(user => {
        if (!user) return res.status(401).json({
            msg: 'Incorrect password',
            code: 92294,
        });

        bcrypt.compare(req.body.password, user.password, (err, compare) => {
            if (err) return res.status(401).json({
                msg: 'Incorrect password',
                code: 96294,
            });

            if (compare) {
                return next();
            } else {
                return res.status(401).json({
                    msg: 'Incorrect password',
                    code: 92298,
                });
            }
        });
    }).catch(err => errorHandler(err, res));
};


================================================
FILE: api/src/routes/middleware/hCaptcha.js
================================================
const hCaptcha = require('./../../providers/hCaptcha');

module.exports = async (req, res, next) => {

    if (!process.env.H_CAPTCHA_SECRET) {
        console.log(`⚠️  Warning: H_CAPTCHA_SECRET not set, skipping captcha validadation`);
        return next();
    }

    if (!req.body.htoken) return res.status(422).json({
        errors: {
            htoken: {
                location: "body",
                param: "htoken",
                msg: "You must complete the captcha"
            }
        }
    });

    const { data } = await hCaptcha.verify(req.body.htoken);

    if (data.success) return next();

    return res.status(422).json({
        errors: {
            htoken: {
                location: "body",
                param: "htoken",
                msg: 'Captcha validation failed.'
            }
        }
    });
};


================================================
FILE: api/src/routes/middleware/index.js
================================================
const checkPassword = require('./checkPassword');
const isInGroup = require('./isInGroup');
const isNotSelf = require('./isNotSelf');
const isGroupOwner = require('./isGroupOwner');
const canAccessBucket = require('./canAccessBucket');
const hCaptcha = require('./hCaptcha');

module.exports = {
    checkPassword,
    isInGroup,
    isNotSelf,
    isGroupOwner,
    canAccessBucket,
    hCaptcha,
};


================================================
FILE: api/src/routes/middleware/isGroupOwner.js
================================================
const { Group } = require('./../../models');

module.exports = async (req, res, next) => {
    const groupID = (req.params.groupID || req.body.groupID);
    const group = await Group.findByPk(groupID);

    if (group.ownerID === req.user.id) {
        return next();
    } else {
        return res.status(401).json({
            msg: `You are not the owner of this group ${groupID}`,
            code: 55213,
        });
    }
};


================================================
FILE: api/src/routes/middleware/isInGroup.js
================================================
const { User, Group } = require('./../../models');

module.exports = async (req, res, next) => {
    const groupID = (req.params.groupID || req.body.groupID);
    const user = await User.findByPk(req.user.id, {
        include: [Group],
    });

    if (!user) return res.status(401).json({
        msg: `User not found`,
        code: 40120,
    });

    const groups = user.Groups.map(({ id }) => (id));

    if (Array.isArray(groups) && groups.includes(groupID)) {
        return next();
    } else {
        return res.status(401).json({
            msg: `You do not have access to group ${groupID} in [${groups.join(', ')}]`,
            code: 65196,
        });
    }
};


================================================
FILE: api/src/routes/middleware/isNotSelf.js
================================================
module.exports = (req, res, next) => {
    if (!req.user || !req.user.id) return res.status(401).json({
        msg: 'Access error',
        code: 18196,
    });

    if (req.user.id === req.body.userID) return res.status(401).json({
        msg: 'Access error',
        code: 18196,
    });

    return next();
};


================================================
FILE: api/src/routes/user.js
================================================
const { body, validationResult, matchedData } = require('express-validator');
const errorHandler = require('./../providers/errorHandler');
const { User, Group } = require('./../models');
const middleware = require('./middleware');
const bcrypt = require('bcrypt-nodejs');
const passport = require('passport');
const express = require('express');

const app = (module.exports = express.Router());


/**
 * GET /api/v1/user
 * 
 */
app.get('/user', [
    passport.authenticate('jwt', { session: false })
], async (req, res) => {
    try {
        const user = await User.findByPk(req.user.id, {
            include: [Group],
        });

        if (!user) return res.status(404).send('User not found');

        return res.json(user);
    } catch (error) {
        errorHandler(error, res);
    }
});


/**
 * POST /api/v1/user
 * 
 */
app.post('/user', [
    passport.authenticate('jwt', { session: false }),
    body('firstName').exists(),
    body('lastName').exists(),
    body('bio').exists(),
], async (req, res) => {
    try {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });
        const data = matchedData(req);

        await User.update(data, { where: { id: req.user.id } });

        return res.json(
            await User.findByPk(req.user.id)
        );
    } catch (error) {
        return errorHandler(error, res);
    }
});


/**
 * POST /api/v1/user/update-password
 * 
 * Update Password
 */
app.post('/user/update-password', [
    passport.authenticate('jwt', { session: false }),
    middleware.checkPassword,
    body('password').exists(),
    body('newPassword').exists(),
    body('newPassword', 'Your password must be atleast 7 characters long').isLength({ min: 7 }),
], async (req, res) => {
    try {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });
        const data = matchedData(req);

        await User.unscoped().update({
            password: bcrypt.hashSync(data.newPassword, bcrypt.genSaltSync(10)),
        }, {
            where: {
                id: req.user.id,
            }
        });

        return res.json({ success: true });
    } catch (error) {
        return errorHandler(error, res);
    }
});


================================================
FILE: api/src/scripts/blacklist.js
================================================

/**
 * node ./src/scripts/blacklist.js --value="bad_word"
 * docker exec -ti s3-api node ./src/scripts/blacklist.js --value="bad_word"
 *
 */
require('dotenv').config();
const argv = require('minimist')(process.argv.slice(2));
const { Blacklist } = require('./../models');
const db = require('./../providers/db');

if (!argv['value']) throw Error('You must provide --value argument');

(async function Main() {
    try {
        await Blacklist.create({
            value: argv['value']
        });
        console.log(`Word added to blacklist: ${argv['value']}`);
    } catch (err) {
        console.error(err);
    } finally {
        db.connectionManager.close();
    }
})();


================================================
FILE: api/src/scripts/buckets.js
================================================

/**
 * node ./src/scripts/buckets.js
 * docker exec -ti s3-api node ./src/scripts/buckets.js
 *
 */
require('dotenv').config();
const { Bucket } = require('./../models');
const db = require('./../providers/db');

(async function Main() {
    try {
        const buckets = await Bucket.findAll();
        for (let i = 0; i < buckets.length; i++) {
            const bucket = buckets[i];
            console.log(`${i} - ${bucket.id} [${bucket.status}] ${bucket.name}.${bucket.namespace}`);
        }
    } catch (err) {
        console.error(err);
    } finally {
        db.connectionManager.close();
    }
})();


================================================
FILE: api/src/scripts/deleteUser.js
================================================

/**
 * node ./src/scripts/deleteUser.js --userID="fdab7a99-2c38-444b-bcb3-f7cef61c275b"
 * docker exec -ti s3-api node ./src/scripts/deleteUser.js --userID="fdab7a99-2c38-444b-bcb3-f7cef61c275b"
 *
 */
require('dotenv').config();
const argv = require('minimist')(process.argv.slice(2));
const { User } = require('./../models');
const db = require('./../providers/db');

if (!argv['userID']) throw Error('You must provide --userID argument');

(async function Main() {
    try {
        await User.destroy({
            where: {
                id: argv['userID'],
            }
        });

        console.log(`User ${argv['userID']} deleted`);
    } catch (err) {
        console.error(err);
    } finally {
        db.connectionManager.close();
    }
})();


================================================
FILE: api/src/scripts/env
================================================
#!/bin/bash

if [[ $NODE_ENV == "production" ]]
  then
    echo "NODE_ENV=production ⚠️"
  else
    echo "NODE_ENV=$NODE_ENV"
  fi


================================================
FILE: api/src/scripts/forgotPassword.js
================================================

/**
 * node ./src/scripts/forgotPassword.js --userID="c4644733-deea-47d8-b35a-86f30ff9618e"
 * docker exec -ti s3-api node ./src/scripts/forgotPassword.js --userID="c4644733-deea-47d8-b35a-86f30ff9618e"
 *
 */
require('dotenv').config();
const generateJWT = require('./../providers/generateJWT');
const argv = require('minimist')(process.argv.slice(2));
const { User, Group } = require('./../models');
const db = require('./../providers/db');
const crypto = require('crypto');

if (!argv['userID']) throw Error('You must provide --userID argument');

(async function Main() {
    try {
        const user = await User.findByPk(argv['userID']);

        const passwordResetKey = crypto.randomBytes(32).toString('base64').replace(/[^a-zA-Z0-9]/g, '');

        await user.update({ passwordResetKey });

        console.log(`\n\nEMAIL THIS TO THE USER\nPASSWORD RESET LINK: ${process.env.FRONTEND_URL}/reset/${passwordResetKey}\n\n`);
    } catch (err) {
        console.error(err);
    } finally {
        db.connectionManager.close();
    }
})();


================================================
FILE: api/src/scripts/generate.js
================================================

/**
 * node ./src/scripts/generate.js --modelName="bucket"
 * docker exec -ti s3-api node ./src/scripts/generate.js --modelName="bucket"

 */
require('dotenv').config();
const argv = require('minimist')(process.argv.slice(2));
const { v4: uuidv4 } = require('uuid');
const Mustache = require('mustache');
const moment = require('moment');
const path = require('path');
const fs = require('fs');

if (!argv['modelName']) throw Error('You must provide --modelName argument');

(async function Main() {
    const ucFirst = (string) => (string.charAt(0).toUpperCase().concat(string.slice(1)));

    const params = {
        modelname: argv['modelName'].toLowerCase(),
        modelName: argv['modelName'],
        ModelName: ucFirst(argv['modelName']),

        modelnames: argv['modelName'].toLowerCase().concat('s'),
        modelNames: argv['modelName'].concat('s'),
        ModelNames: ucFirst(argv['modelName']).concat('s'),
        UUID: uuidv4(),
    };

    if (argv['v']) console.log(params);

    const pathModel = path.resolve(`./src/models/${params.ModelName}.js`);
    fs.writeFileSync(pathModel, Mustache.render(fs.readFileSync(path.resolve('./src/scripts/generator/Model.js'), 'utf8'), params));
    console.log(`Created: ${pathModel}`);

    const pathRoute = path.resolve(`./src/routes/${params.ModelNames}.js`);
    fs.writeFileSync(pathRoute, Mustache.render(fs.readFileSync(path.resolve('./src/scripts/generator/Route.js'), 'utf8'), params));
    console.log(`Created: ${pathRoute}`);

    const pathMigration = path.resolve(`./src/database/migrations/${moment().format('YYYYMMDDHHmmss')}-create-${params.ModelNames}.js`);
    fs.writeFileSync(pathMigration, Mustache.render(fs.readFileSync(path.resolve('./src/scripts/generator/Migration.js'), 'utf8'), params));
    console.log(`Created: ${pathMigration}`);

    const pathSeeder = path.resolve(`./src/database/seeders/${moment().format('YYYYMMDDHHmmss')}-${params.ModelNames}.js`);
    fs.writeFileSync(pathSeeder, Mustache.render(fs.readFileSync(path.resolve('./src/scripts/generator/Seeder.js'), 'utf8'), params));
    console.log(`Created: ${pathSeeder}`);
})();




================================================
FILE: api/src/scripts/generator/Migration.js
================================================
module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.createTable('{{ ModelNames }}', {
        id: {
            type: Sequelize.UUID,
            defaultValue: Sequelize.UUIDV4,
            primaryKey: true,
            allowNull: false,
            unique: true
        },

        createdAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
        updatedAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
        deletedAt: {
            type: Sequelize.DATE,
            allowNull: true,
        },
    }),
    down: (queryInterface, Sequelize) => queryInterface.dropTable('{{ ModelNames }}'),
};


================================================
FILE: api/src/scripts/generator/Model.js
================================================
const Sequelize = require('sequelize');
const db = require('./../providers/db');

module.exports = db.define('{{ ModelName }}', {
    id: {
        type: Sequelize.UUID,
        defaultValue: Sequelize.UUIDV4,
        primaryKey: true,
        allowNull: false,
        unique: true
    },

    createdAt: {
        type: Sequelize.DATE,
        allowNull: true,
    },
    updatedAt: {
        type: Sequelize.DATE,
        allowNull: true,
    },
    deletedAt: {
        type: Sequelize.DATE,
        allowNull: true,
    },

}, {
    tableName: '{{ ModelNames }}',
    paranoid: true,
    defaultScope: {
        attributes: {
            exclude: [

            ]
        }
    },
});


================================================
FILE: api/src/scripts/generator/Route.js
================================================
const { body, validationResult, matchedData } = require('express-validator');
const errorHandler = require('./../providers/errorHandler');
const { {{ ModelName }}, Group } = require('./../models');
const middleware = require('./middleware');
const bcrypt = require('bcrypt-nodejs');
const passport = require('passport');
const express = require('express');

const app = (module.exports = express.Router());


/**
 * GET /api/v1/{{ modelnames }}
 * 
 */
app.get('/{{ modelnames }}', [
    passport.authenticate('jwt', { session: false })
], async (req, res) => {
    try {
        const {{ modelnames }} = await {{ ModelName }}.findAll();

        return res.json({{ modelnames }});
    } catch (error) {
        errorHandler(error, res);
    }
});


/**
 * GET /api/v1/{{ modelnames }}/:{{ modelName }}ID
 * 
 */
app.get('/{{ modelnames }}/:{{ modelName }}ID', [
    passport.authenticate('jwt', { session: false }),
], async (req, res) => {
    try {
        return res.json(
            await {{ ModelName }}.findByPk(req.params.{{ modelName }}ID)
        );
    } catch (error) {
        return errorHandler(error, res);
    }
});


/**
 * POST /api/v1/{{ modelnames }}/:{{ modelName }}ID
 * 
 * Update {{ ModelName }}
 */
app.post('/{{ modelnames }}/:{{ modelName }}ID', [
    passport.authenticate('jwt', { session: false }),
    // body('field').exists(),
], async (req, res) => {
    try {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(422).json({ errors: errors.mapped() });
        const data = matchedData(req);

        await {{ ModelName }}.update(data, {
            where: {
                id: req.params.{{ modelName }}ID,
            }
        });

        return res.json({ success: true });
    } catch (error) {
        return errorHandler(error, res);
    }
});


================================================
FILE: api/src/scripts/generator/Seeder.js
================================================
const moment = require('moment');

const insert = [{
    id: '{{ UUID }}',
    createdAt: moment().format('YYYY-MM-DD HH:mm:ss'),
    updatedAt: moment().format('YYYY-MM-DD HH:mm:ss'),
}];

module.exports = {
    up: (queryInterface, Sequelize) => queryInterface.bulkInsert('{{ ModelNames }}', insert).catch(err => console.log(err)),
    down: (queryInterface, Sequelize) => { }
};


================================================
FILE: api/src/scripts/inviteUser.js
================================================

/**
 * node ./src/scripts/inviteUser.js --email="newuser@example.com" --groupID="fdab7a99-2c38-444b-bcb3-f7cef61c275b"
 * docker exec -ti s3-api node ./src/scripts/inviteUser.js --email="newuser@example.com" --groupID="fdab7a99-2c38-444b-bcb3-f7cef61c275b"
 *
 */
require('dotenv').config();
const generateJWT = require('./../providers/generateJWT');
const argv = require('minimist')(process.argv.slice(2));
const { User, Group, GroupsUsers } = require('./../models');
const db = require('./../providers/db');
const crypto = require('crypto');

if (!argv['email']) throw Error('You must provide --email argument');
if (!argv['groupID']) throw Error('You must provide --groupID argument');

(async function Main() {
    try {
        const email = argv['email'];
        const groupID = argv['groupID'];

        let user = await User.unscoped().findOne({
            where: { email }
        });

        if (user) {
            // Check if relationship already exists
            const relationship = await GroupsUsers.findOne({
                where: {
                    groupID,
                    userID: user.id
                }
            });
            if (relationship) return console.log(`User already in this group`);
        } else {
            try {
                user = await User.create({
                    email,
                    inviteKey: crypto.randomBytes(20).toString('hex')
                });

                console.log(user);
                console.log(user.get({ plain: true }));
                console.log(user.inviteKey);

                console.log(`\n\nEMAIL THIS TO THE USER\nINVITE LINK: ${process.env.FRONTEND_URL}/invite/${user.inviteKey}\n\n`);
            } catch (error) {
                errorHandler(error);
            }
        }

        // Delete all first
        await GroupsUsers.destroy({
            where: {
                groupID,
                userID: user.id,
            }
        });

        await GroupsUsers.create({
            groupID,
            userID: user.id,
        });

        console.log(`User ${user.email} invited`);
    } catch (err) {
        console.error(err);
    } finally {
        db.connectionManager.close();
    }
})();


================================================
FILE: api/src/scripts/jwt.js
================================================

/**
 * node ./src/scripts/jwt.js --userID="c4644733-deea-47d8-b35a-86f30ff9618e"
 * docker exec -ti s3-api node ./src/scripts/jwt.js --userID="c4644733-deea-47d8-b35a-86f30ff9618e"
 *
 */
require('dotenv').config();
const generateJWT = require('./../providers/generateJWT');
const argv = require('minimist')(process.argv.slice(2));
const { User, Group } = require('./../models');
const db = require('./../providers/db');

if (!argv['userID']) throw Error('You must provide --userID argument');

(async function Main() {
    try {
        const user = await User.findByPk(argv['userID'], {
            include: [Group]
        });
        console.log(`\n\nJWT:\n\n${generateJWT(user)}\n\n`);
    } catch (err) {
        console.error(err);
    } finally {
        db.connectionManager.close();
    }
})();


================================================
FILE: api/src/scripts/refresh
================================================
#!/bin/bash

if [[ $NODE_ENV == "production" ]]
  then
    echo "ERROR: Can not refresh while in production"
  else
    sequelize db:migrate:undo:all
    sequelize db:migrate
    sequelize db:seed:all
  fi

================================================
FILE: api/src/scripts/resetPassword.js
================================================

/**
 * node ./src/scripts/resetPassword.js --userID="c4644733-deea-47d8-b35a-86f30ff9618e" --password="password"
 * docker exec -ti s3-api node ./src/scripts/resetPassword.js --userID="c4644733-deea-47d8-b35a-86f30ff9618e" --password="password"
 *
 */
require('dotenv').config();
const generateJWT = require('./../providers/generateJWT');
const argv = require('minimist')(process.argv.slice(2));
const { User, Group } = require('./../models');
const db = require('./../providers/db');
const bcrypt = require('bcrypt-nodejs');

if (!argv['userID']) throw Error('You must provide --userID argument');
if (!argv['password']) throw Error('You must provide --password argument');

(async function Main() {
    try {
        const user = await User.findByPk(argv['userID']);

        await user.update({
            password: bcrypt.hashSync(argv['password'], bcrypt.genSaltSync(10)),
            passwordResetKey: null
        });

        console.log(`Password updated`);
    } catch (err) {
        console.error(err);
    } finally {
        db.connectionManager.close();
    }
})();


================================================
FILE: api/src/scripts/seed
================================================
#!/bin/bash

if [[ $NODE_ENV == "production" ]]
  then
    echo "ERROR: Can not seed while in production"
  else
    sequelize db:seed:all
  fi

================================================
FILE: api/src/scripts/sync.js
================================================
/**
 * node ./src/scripts/sync.js
 * docker exec -ti s3-api node ./src/scripts/sync.js
 *
 */

require('dotenv').config();
const argv = require('minimist')(process.argv.slice(2));
const { Bucket } = require('./../models');
const db = require('./../providers/db');

(async function Main() {
    try {
        const buckets = await Bucket.findAll();
        for (const bucket of buckets) {
            await bucket.sync();
        }
    } catch (err) {
        console.error(err);
    }
})();


================================================
FILE: api/src/scripts/users.js
================================================

/**
 * node ./src/scripts/users.js
 * docker exec -ti s3-api node ./src/scripts/users.js
 *
 */
require('dotenv').config();
const { User } = require('./../models');
const db = require('./../providers/db');

(async function Main() {
    try {
        const users = await User.findAll();
        for (let i = 0; i < users.length; i++) {
            const user = users[i];
            console.log(`${i} - ${user.id}: ${user.email}`);
        }
    } catch (err) {
        console.error(err);
    } finally {
        db.connectionManager.close();
    }
})();


================================================
FILE: api/tests/Auth.js
================================================
const chai = require('chai');
const chaiHttp = require('chai-http');
const server = require('../src');
const should = chai.should();
const faker = require('faker');

chai.use(chaiHttp);


describe('Auth', () => {

    /**
     * GET  api/v1/_authcheck
     * 
     */
    describe('GET /api/v1/_authcheck', () => {
        it('Should check auth status', (done) => {
            chai.request(server)
                .post('/api/v1/auth/login')
                .send({
                    email: 'user@example.com',
                    password: 'Password@1234'
                })
                .end((err, res) => {

                    chai.request(server)
                        .get('/api/v1/_authcheck')
                        .set({
                            'Authorization': `Bearer ${res.body.accessToken}`,
                        })
                        .end((err, res) => {
                            res.should.have.status(200);
                            res.body.should.have.property('id');
                            done(err);
                        });
                });
        });

        it('Should check bad headers', (done) => {
            chai.request(server)
                .get('/api/v1/_authcheck')
                .set({
                    'Authorization': 'Bearer xx.xx.xx',
                })
                .end((err, res) => {
                    res.should.have.status(401);
                    done(err);
                });
        });
    });


    /**
     * POST  api/v1/auth/login
     * 
     */
    describe('POST /api/v1/auth/login', () => {
        it('Should return auth access token', (done) => {
            chai.request(server)
                .post('/api/v1/auth/login')
                .send({
                    email: 'user@example.com',
                    password: 'Password@1234'
                })
                .end((err, res) => {
                    res.should.have.status(200);
                    res.should.be.json;
                    res.body.should.be.a('object');
                    res.body.should.have.property('accessToken');
                    done(err);
                });
        });

        it('Should reject absent password', (done) => {
            chai.request(server)
                .post('/api/v1/auth/login')
                .send({
                    email: 'user@example.com',
                })
                .end((err, res) => {
                    res.should.have.status(422);
                    done(err);
                });
        });

        it('Should reject wrong password', (done) => {
            chai.request(server)
                .post('/api/v1/auth/login')
                .send({
                    email: 'user@example.com',
                    password: 'BAD_PASSWORD'
                })
                .end((err, res) => {
                    res.should.have.status(401);
                    done(err);
                });
        });
    });


    /**
     * POST  api/v1/auth/sign-up
     * 
     */
    describe('POST /api/v1/auth/sign-up', () => {

        it('Should create a new user', (done) => {
            chai.request(server)
                .post('/api/v1/auth/sign-up')
                .send({
                    email: faker.internet.email(),
                    password: 'Password@1234',
                    firstName: faker.name.firstName(),
                    lastName: faker.name.lastName(),
                    groupName: faker.company.bsBuzz(),
                    tos: '2020-03-20'
                })
                .end((err, res) => {
                    res.should.have.status(200);
                    res.should.be.json;
                    res.body.should.be.a('object');
                    res.body.should.have.property('accessToken');
                    done(err);
                });
        });

        it('Should reject bad data', (done) => {
            chai.request(server)
                .post('/api/v1/auth/sign-up')
                .send({
                    email: faker.internet.email(),
                    firstName: faker.name.firstName(),
                })
                .end((err, res) => {
                    res.should.have.status(422);
                    done(err);
                });
        });

        it('Should reject bad email', (done) => {
            chai.request(server)
                .post('/api/v1/auth/sign-up')
                .send({
                    email: 'anthonybudd@',
                    password: 'password',
                    firstName: faker.name.firstName(),
                    lastName: faker.name.lastName(),
                    groupName: faker.company.bsBuzz()
                })
                .end((err, res) => {
                    res.should.have.status(422);
                    done(err);
                });
        });

        it('Should reject taken email', (done) => {
            chai.request(server)
                .post('/api/v1/auth/sign-up')
                .send({
                    email: 'user@example.com',
                    password: 'also_bad_password',
                    firstName: faker.name.firstName(),
                    lastName: faker.name.lastName(),
                    groupName: faker.company.bsBuzz()
                })
                .end((err, res) => {
                    res.should.have.status(422);
                    done(err);
                });
        });

        it('Should reject bad password', (done) => {
            chai.request(server)
                .post('/api/v1/auth/sign-up')
                .send({
                    email: 'user@example.com',
                    password: '12345',
                    firstName: faker.name.firstName(),
                    lastName: faker.name.lastName(),
                    groupName: faker.company.bsBuzz()
                })
                .end((err, res) => {
                    res.should.have.status(422);
                    done(err);
                });
        });
    });
});


================================================
FILE: api/tests/Group.js
================================================
require('dotenv').config();
const chai = require('chai');
const chaiHttp = require('chai-http');
const server = require('../src');
const should = chai.should();

chai.use(chaiHttp);

const GROUP_ID = 'fdab7a99-2c38-444b-bcb3-f7cef61c275b';
const OTHER_GROUP_ID = '190c8a70-34d1-4281-a775-850058453704';

describe('Groups', () => {

    /**
     * GET  /api/v1/groups/:groupID
     * 
     */
    describe('GET  /api/v1/groups/:groupID', () => {

        it('Should return the group', (done) => {
            chai.request(server)
                .get(`/api/v1/groups/${GROUP_ID}`)
                .set({
                    'Authorization': `Bearer ${process.env.TEST_JWT}`,
                })
                .end((err, res) => {
                    res.should.have.status(200);
                    res.should.be.json;
                    res.body.should.be.a('object');
                    res.body.should.have.property('id');
                    res.body.should.have.property('name');
                    done();
                });
        });

        it('Should reject bad group', (done) => {
            chai.request(server)
                .get(`/api/v1/groups/${OTHER_GROUP_ID}`)
                .set({
                    'Authorization': `Bearer ${process.env.TEST_JWT}`,
                })
                .end((err, res) => {
                    res.should.have.status(401);
                    done();
                });
        });
    });


    /**
     * POST /api/v1/groups/:groupID
     * 
     */
    describe('POST /api/v1/groups/:groupID', () => {

        it('Should update the group name', done => {
            chai.request(server)
                .post(`/api/v1/groups/${GROUP_ID}`)
                .set({
                    'Authorization': `Bearer ${process.env.TEST_JWT}`,
                })
                .send({
                    name: 'Test Group'
                })
                .end((err, res) => {
                    res.should.have.status(200);
                    res.should.be.json;
                    res.body.should.be.a('object');
                    res.body.should.have.property('id');
                    res.body.should.have.property('name');
                    res.body.name.should.equal('Test Group');
                    done();
                });
        });

        it('Should reject bad group', done => {
            chai.request(server)
                .post(`/api/v1/groups/${OTHER_GROUP_ID}`)
                .set({
                    'Authorization': `Bearer ${process.env.TEST_JWT}`,
                })
                .send({
                    name: 'Test Group'
                })
                .end((err, res) => {
                    res.should.have.status(401);
                    done();
                });
        });
    });


    /**
     * POST  /api/v1/groups/:groupID/users/add
     * 
     */
    describe('POST  /api/v1/groups/:groupID/users/add', () => {
        it('Should add user to the group', done => {
            chai.request(server)
                .post(`/api/v1/groups/${GROUP_ID}/users/add`)
                .set({
                    'Authorization': `Bearer ${process.env.TEST_JWT}`,
                })
                .send({
                    userID: 'd700932c-4a11-427f-9183-d6c4b69368f9',
                })
                .end((err, res) => {
                    res.should.have.status(200);
                    res.should.be.json;
                    res.body.should.be.a('object');
                    res.body.should.have.property('userID');
                    res.body.should.have.property('groupID');
                    done();
                });
        });

        it('Should reject bad userID', done => {
            chai.request(server)
                .post(`/api/v1/groups/${GROUP_ID}/users/add`)
                .set({
                    'Authorization': `Bearer ${process.env.TEST_JWT}`,
                })
                .send({
                    userID: '00000000-0000-0000-0000-000000000000',
                })
                .end((err, res) => {
                    res.should.have.status(422);
                    done();
                });
        });
    });


    /**
     * DELETE  /api/v1/groups/:groupID/users/:userID
     * 
     */
    describe('DELETE  /api/v1/groups/:groupID/users/:userID', () => {
        it('Should remove user from the group', done => {
            chai.request(server)
                .delete(`/api/v1/groups/${GROUP_ID}/users/d700932c-4a11-427f-9183-d6c4b69368f9`)
                .set({
                    'Authorization': `Bearer ${process.env.TEST_JWT}`,
                })
                .end((err, res) => {
                    res.should.have.status(200);
                    res.should.be.json;
                    res.body.should.be.a('object');
                    res.body.should.have.property('userID');
                    res.body.should.have.property('groupID');
                    done();
                });
        });
    });
});


================================================
FILE: api/tests/HealthCheck.js
================================================
const chai = require('chai');
const chaiHttp = require('chai-http');
const server = require('../src');
const should = chai.should();

chai.use(chaiHttp);

describe('DevOps', () => {
    describe('GET  /api/v1/_healthcheck', () => {
        it('Should return system status', (done) => {
            chai.request(server)
                .get('/api/v1/_healthcheck')
                .end((err, res) => {
                    res.should.have.status(200);
                    res.should.be.json;
                    res.body.should.be.a('object');
                    res.body.status.should.equal('ok');
                    done();
                });
        });
    });
});


================================================
FILE: api/tests/User.js
================================================
require('dotenv').config();
const chai = require('chai');
const chaiHttp = require('chai-http');
const server = require('../src');
const should = chai.should();

chai.use(chaiHttp);


describe('User', () => {

    /**
     * GET  /api/v1/user
     * 
     */
    describe('GET  /api/v1/user', () => {

        it('Should return the user model', done => {
            chai.request(server)
                .get('/api/v1/user')
                .set({
                    'Authorization': `Bearer ${process.env.TEST_JWT}`,
                })
                .end((err, res) => {
                    res.should.have.status(200);
                    res.should.be.json;
                    res.body.should.be.a('object');
                    res.body.should.have.property('id');
                    done();
                });
        });

        it('Should reject bad access token', done => {
            chai.request(server)
                .get('/api/v1/user')
                .set({
                    'Authorization': `Bearer BAD.TOKEN`,
                })
                .end((err, res) => {
                    res.should.have.status(401);
                    done();
                });
        });
    });


    /**
     * POST /api/v1/user
     * 
     */
    describe('POST /api/v1/user', () => {
        it('Should update the current user', done => {
            chai.request(server)
                .post('/api/v1/user')
                .set({
                    'Authorization': `Bearer ${process.env.TEST_JWT}`,
                })
                .send({
                    firstName: 'John',
                    lastName: 'Smith'
                })
                .end((err, res) => {
                    res.should.have.status(200);
                    res.should.be.json;
                    res.body.should.be.a('object');
                    done();
                });
        });
    });


    /**
     * POST /api/v1/user/update-password
     * 
     */
    describe('POST /api/v1/user/update-password', () => {
        it('Should update the current users password', (done) => {
            chai.request(server)
                .post('/api/v1/user/update-password')
                .set({
                    'Authorization': `Bearer ${process.env.TEST_JWT}`,
                })
                .send({
                    password: 'password',
                    newPassword: 'newpassword'
                })
                .end((err, res) => {
                    res.should.have.status(200);
                    res.should.be.json;
                    res.body.should.be.a('object');
                    done();
                });
        });
    });
});

================================================
FILE: automation-test/.gitlab-ci.yml
================================================
stages:
  - build

build-job:
  image: docker:dind
  stage: build
  services:
    - docker:dind
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
  script:
    - docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG

================================================
FILE: automation-test/Dockerfile
================================================
FROM ubuntu:noble

RUN apt-get update && apt-get install -y curl

RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/arm64/kubectl"

RUN install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

COPY bucket.yml /root/bucket.yml

================================================
FILE: automation-test/bucket.yml
================================================
apiVersion: v1
kind: Namespace
metadata:
  name: XXX
  labels:
    name: XXX
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: XXX-pod
  name: XXX-pod
  namespace: XXX
spec:
  containers:
  - name: XXX-pod
    image: quay.io/minio/minio:latest
    env:
    - name: MINIO_ROOT_USER
      value: root
    - name: MINIO_ROOT_PASSWORD
      value: password
    command:
    - /bin/bash
    - -c
    args: 
    - minio server /data --console-address :9001
    ports:
    - containerPort: 9001
    volumeMounts:
    - name: longhornvolume
      mountPath: /data
  volumes:
  - name: longhornvolume
    persistentVolumeClaim:
      claimName: XXX-pvc
---
apiVersion: v1
kind: Service  
metadata:
  name: XXX-svc
  namespace: XXX 
spec:
  selector:    
    app: XXX-pod 
  ports:  
  - protocol: TCP  
    port: 80 
    targetPort: 9001
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: XXX
  name: XXX-ing
  annotations:
    kubernetes.io/ingress.class: "traefik"
spec:
  rules:
  - host: XXX.minio.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: XXX-svc
            port:
              number: 80
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: XXX-pvc
  namespace: XXX
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 5Gi


================================================
FILE: automation-test/deployment.yml
================================================
kind: Deployment    
apiVersion: apps/v1   
metadata:            
  name: automation-test-deployment    
  labels:        
    app: automation-test    
spec:            
  replicas: 1    
  selector:       
    matchLabels: 
      app: automation-test
  template:        
    metadata:    
      labels:    
        app: automation-test
    spec:   
      volumes:
        - name: storage-cluster-config
          secret:
            secretName: storage-cluster-config     
      containers:    
      - name: automation-test
        image: gitlab.local:5050/anthonybudd/automation-test:master
        imagePullPolicy: Always    
        ports:
          - containerPort: 80
        command: [ "/bin/bash", "-c", "--" ]
        args: [ "while true; do sleep 30; done;" ]
        volumeMounts:
          - name: storage-cluster-config
            mountPath: "/root/config"
            subPath: config
        

================================================
FILE: aws-sdk-test/.gitignore
================================================
node_modules/

================================================
FILE: aws-sdk-test/index.js
================================================
const { S3Client, ListObjectsV2Command, PutObjectCommand } = require("@aws-sdk/client-s3");

const Bucket = 'kjdoewl';
const Namespace = 'gdiwk';
const accessKeyId = "kUoZRWyhUae7tFZNduTS";
const secretAccessKey = "qxYIqD/8WnvWYIkY7Rg7PSqSMrnxcVfBdpWgzz7z";

(async function () {
    const client = new S3Client({
        region: 'us-west-2',
        endpoint: `https://${Bucket}.${Namespace}.s3.anthonybudd.io`,
        forcePathStyle: true,
        sslEnabled: true,
        credentials: {
            accessKeyId,
            secretAccessKey
        },
    });

    const Key = `${Date.now().toString()}.txt`;
    await client.send(new PutObjectCommand({
        Bucket,
        Key,
        Body: `The time now is ${new Date().toLocaleString()}`,
        ACL: 'public-read',
        ContentType: 'text/plain',
    }));
    console.log(`New object successfully written to: ${Bucket}://${Key}\n`);

    const { Contents } = await client.send(new ListObjectsV2Command({ Bucket }));
    console.log("Bucket Contents:");
    console.log(Contents.map(({ Key }) => Key).join("\n"));
})();


================================================
FILE: aws-sdk-test/package.json
================================================
{
  "name": "aws-sdk-test",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@aws-sdk/client-s3": "^3.575.0",
    "aws-sdk": "^2.1620.0"
  }
}


================================================
FILE: deployment-test/.gitlab-ci.yml
================================================
stages:
  - build

build-job:
  image: docker:dind
  stage: build
  services:
    - docker:dind
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
  script:
    - docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG

================================================
FILE: deployment-test/Dockerfile
================================================
FROM nginx:alpine
COPY . /usr/share/nginx/html

================================================
FILE: deployment-test/index.html
================================================
<h1>Compiled on GitLab</h1>
<h1>Deployed on K3s</h1>

================================================
FILE: deployment-test/k8s.yml
================================================
kind: Deployment    
apiVersion: apps/v1   
metadata:            
  name: website-deployment    
  labels:        
    app: website    
spec:            
  replicas: 1    
  selector:       
    matchLabels: 
      app: website
  template:        
    metadata:    
      labels:    
        app: website
    spec:        
      containers:    
      - name: website
        image: gitlab.local:5050/anthonybudd/website:master
        imagePullPolicy: Always    
        ports:
          - containerPort: 80
---
apiVersion: v1
kind: Service  
metadata:
  name: website-service  
spec:
  selector:    
    app: website 
  ports:  
  - protocol: TCP  
    port: 80 
    targetPort: 80  
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: default
  name: website-ingress
  annotations:
    kubernetes.io/ingress.class: "traefik"
spec:
  rules:
  - host: website.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: website-service
            port:
              number: 80

================================================
FILE: frontend/.browserslistrc
================================================
> 1%
last 2 versions
not dead
not ie 11


================================================
FILE: frontend/.editorconfig
================================================
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: frontend/.eslintrc.js
================================================
module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
  ],
}


================================================
FILE: frontend/.gitignore
================================================
.DS_Store
node_modules
/dist

# local env files
.env
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: frontend/.gitlab-ci.yml
================================================
stages:
  - build

build-job:
  image: docker:dind
  stage: build
  services:
    - docker:dind
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
    VITE_API_URL: http://s3-api.anthonybudd.io/api/v1
    VITE_S3_ROOT: s3.anthonybudd.io
  before_script:
    - apk --update add nodejs npm
  script:
    - docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
    - npm install
    - npm run build
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG

================================================
FILE: frontend/Dockerfile
================================================
FROM nginx
COPY default.conf /etc/nginx/conf.d/default.conf
COPY ./dist /usr/share/nginx/html

================================================
FILE: frontend/ReadMe.md
================================================
# Frontend

This represents the AWS console found at [aws.amazon.com/console](https://aws.amazon.com/console/). This is a Vue.js static frontend SPA that makes HTTP requests to the [REST API](./api/ReadMe.md) for users to login, create and delete S3 buckets.


### Set-up
```
npm i
cp .env.example .env
npm run dev
```

<img src="https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/frontend.gif">

================================================
FILE: frontend/default.conf
================================================
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html =404;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}


================================================
FILE: frontend/index.html
================================================
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>S3</title>
</head>

<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>

</html>


================================================
FILE: frontend/jsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "baseUrl": "./",
    "moduleResolution": "node",
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  }
}


================================================
FILE: frontend/k8s/clusterissuer.yml
================================================
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          class: nginx 

================================================
FILE: frontend/k8s/frontend-ssl.ingress.yml
================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: frontend-ingress
  namespace: s3-api
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    kubernetes.io/ingress.class: "traefik"
spec:
  tls:
  - hosts:
    - s3.anthonybudd.io
    secretName: s3-anthonybudd-io-cert
  rules:
  - host: s3.anthonybudd.io
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend-service
            port:
              number: 80

================================================
FILE: frontend/k8s/frontend.deployment.yml
================================================
kind: Deployment    
apiVersion: apps/v1   
metadata:            
  name: frontend-deployment    
  namespace: s3-api
  labels:        
    app: frontend    
spec:            
  replicas: 1    
  selector:       
    matchLabels: 
      app: frontend
  template:        
    metadata:    
      labels:    
        app: frontend
    spec:        
      containers:    
      - name: frontend
        image: gitlab.local:5050/anthonybudd/frontend:master
        imagePullPolicy: Always    
        ports:
          - containerPort: 80

================================================
FILE: frontend/k8s/frontend.ingress.yml
================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: frontend-ingress
  namespace: s3-api
  annotations:
    kubernetes.io/ingress.class: "traefik"
spec:
  rules:
  - host: s3-app.anthonybudd.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend-service
            port:
              number: 80

================================================
FILE: frontend/k8s/frontend.service.yml
================================================
apiVersion: v1
kind: Service  
metadata:
  name: frontend-service  
  namespace: s3-api
spec:
  selector:    
    app: frontend
  ports:  
  - protocol: TCP  
    port: 80 
    targetPort: 80  

================================================
FILE: frontend/package.json
================================================
{
  "name": "frontend",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint . --fix --ignore-path .gitignore"
  },
  "dependencies": {
    "@hcaptcha/vue3-hcaptcha": "^1.3.0",
    "@kyvg/vue3-notification": "^3.2.1",
    "@mdi/font": "7.0.96",
    "axios": "^1.6.8",
    "core-js": "^3.29.0",
    "roboto-fontface": "*",
    "vue": "^3.2.0",
    "vue-router": "^4.0.0",
    "vuetify": "^3.0.0",
    "vuex": "^4.1.0",
    "webfontloader": "^1.0.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.0.0",
    "eslint": "^8.37.0",
    "eslint-plugin-vue": "^9.3.0",
    "sass": "^1.60.0",
    "vite": "^4.2.0",
    "vite-plugin-vuetify": "^1.0.0"
  }
}


================================================
FILE: frontend/src/App.vue
================================================
<template>
    <router-view v-if="isLoaded" />
    <notifications />
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { useStore } from 'vuex';
import api from './api';
import router from "@/plugins/router";

const store = useStore();
const isLoaded = ref(false);

onMounted(async () => {
    const skipAuthPages = ['/login', '/sign-up']; // Auth not needed on these pages
    const skipAuth = new RegExp(skipAuthPages.join('|')).test(window.location.href);

    if (skipAuth) {
        isLoaded.value = true;
        return;
    }

    const accessToken = localStorage.getItem('AccessToken');
    if (!accessToken) {
        isLoaded.value = true;
        router.push('/logout');
    } else {
        api.setJWT(accessToken);
        try {
            const { data: user } = await api.user.get();
            store.commit('setUser', user);
        } catch (error) {
            router.push('/login');
        }
        isLoaded.value = true;
    }
});
</script>


================================================
FILE: frontend/src/api/Auth.js
================================================
import Service from './Service';

class Auth extends Service {
    login(data) {
        return this.axios.post('/auth/login', data);
    }

    signUp(data) {
        return this.axios.post('/auth/sign-up', data);
    }
}

export default Auth;


================================================
FILE: frontend/src/api/Buckets.js
================================================
import Service from './Service';

class Bucket extends Service {
    index() {
        return this.axios.get(`/buckets`);
    }

    get(bucketID) {
        return this.axios.get(`/buckets/${bucketID}`);
    }

    create(bucket) {
        return this.axios.post(`/buckets`, bucket);
    }

    delete(bucketID) {
        return this.axios.delete(`/buckets/${bucketID}`);
    }
}

export default Bucket;


================================================
FILE: frontend/src/api/Service.js
================================================
import axios from 'axios';

class Service {
    constructor(JWT) {
        this.JWT = JWT;

        this.url = import.meta.env.VITE_API_URL || 'https://localhost:4431/api';

        this.axios = axios.create({
            baseURL: import.meta.env.VITE_API_URL || 'https://localhost:4431/api',
            headers: {
                Authorization: `Bearer ${JWT}`,
            }
        });
    }
}

export default Service;


================================================
FILE: frontend/src/api/User.js
================================================
import Service from './Service';

class User extends Service {
    get() {
        return this.axios.get('/user');
    }

    stats() {
        return this.axios.get(`/stats`);
    }
}

export default User;


================================================
FILE: frontend/src/api/index.js
================================================
import Auth from './Auth';
import User from './User';
import Buckets from './Buckets';

class API {
    constructor(JWT) {
        this.setJWT(JWT);
    }

    setJWT(JWT) {
        this.JWT = JWT;
        this.auth = new Auth(JWT);
        this.user = new User(JWT);
        this.buckets = new Buckets(JWT);
    }

    getJWT() {
        return this.JWT;
    }
}

let instance;
if (!instance) instance = new API();
export default instance;


================================================
FILE: frontend/src/components/CreateBucketForm.vue
================================================
<template>
    <v-card title="Create Bucket">
        <v-card-text>
            <v-text-field
                v-model="namespace"
                label="Namespace"
                variant="outlined"
                density="compact"
                required
                @keyup="onKeyUpNamespace"
                @keydown.enter.prevent="onClickCreateBucket"
                :error-messages="(errors.namespace) ? [errors.namespace.msg] : []"
            ></v-text-field>
            <v-text-field
                v-model="bucketName"
                label="Bucket Name"
                variant="outlined"
                density="compact"
                required
                @keyup="onKeyUpBucketName"
                @keydown.enter.prevent="onClickCreateBucket"
                :error-messages="(errors.name) ? [errors.name.msg] : []"
            ></v-text-field>
        </v-card-text>

        <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn
                variant="flat"
                color="primary"
                @click="onClickCreateBucket"
                :loading="loading"
                :disabled="loading || bucketName.length < 4 || namespace.length < 4"
            >Create</v-btn>
        </v-card-actions>
    </v-card>
</template>

<script setup>
import { useNotification } from "@kyvg/vue3-notification";
import { defineEmits, ref, inject } from 'vue';
import api from './../api';


const { notify } = useNotification();
const emit = defineEmits(['showsidebar']);
const errorHandler = inject('errorHandler');
const errors = ref({});
const loading = ref(false);
const namespace = ref('');
const bucketName = ref('');

const onClickCreateBucket = async () => {
    try {
        loading.value = true;
        const { data: bucket } = await api.buckets.create({
            namespace: namespace.value,
            name: bucketName.value,
        });
        emit('onCreateBucket', bucket);
        notify({
            title: 'Bucket Created'
        });
    } catch (error) {
        errorHandler(error, (data, code) => {
            if (code === 422) errors.value = data.errors;
        });
    } finally {
        loading.value = false;
    }
};

const onKeyUpBucketName = async () => {
    bucketName.value = bucketName.value.replace(/[^a-zA-Z0-9-]/g, '');
};

const onKeyUpNamespace = async () => {
    namespace.value = namespace.value.replace(/[^a-zA-Z0-9-]/g, '');
};
</script>


================================================
FILE: frontend/src/components/TermsOfService.vue
================================================
<template>
    <v-card>
        <v-card-text>
            <h1>Terms of Service for s3.anthonybudd.io</h1>

            <h3>1. Introduction</h3>
            <p>
                Welcome to S3.anthonybudd.io ("Service" or "API"). These Terms of Service ("Terms")
                govern your access to and use of the Service offered by Anthony Budd ("we," "us," or "our"). By
                accessing or using the Service, you ("you" or "your") agree to be bound by these Terms. If you do not
                agree
                to all of the Terms, you are not authorized to access or use the Service.
            </p>

            <h3>2. Definitions</h3>
            <p>
                Account: Your account with the Service.
                API Key: A unique identifier used to access the Service.
                API Request: A request sent to the Service to perform an operation, such as uploading or downloading an
                object.
                Content: Any data, information, or materials you upload, store, or transmit through the Service.
                Object: A unit of data stored in the Service, identified by a unique key.
                Service: The s3.anthonybudd.io service, including all features, functionalities,
                and
                documentation offered by us.
            </p>

            <h3>3. Your Account</h3>
            <p>
                3.1 You must be at least 18 years old to access or use the Service.
                3.2 You are responsible for maintaining the confidentiality of your Account information, including your
                login credentials, and for all activities that occur under your Account.
                3.3 You agree to notify us immediately of any unauthorized use of your Account or any other security
                breach.
                3.4 We reserve the right to terminate or suspend your access to the Service at any time and for any
                reason,
                with or without notice.
            </p>

            <h3>4. Content</h3>
            <p>
                4.1 You are solely responsible for all Content you upload, store, or transmit through the Service.
                4.2 You represent and warrant that you have all necessary rights, licenses, and permissions to use,
                upload,
                store, and transmit the Content.
                4.3 You agree not to upload, store, or transmit any Content that is:
                * Illegal, obscene, defamatory, threatening, harassing, or abusive.
                * Infringes on the intellectual property rights of any third party.
                * Contains viruses or other malicious code.
                * Violates any applicable laws or regulations.
            </p>

            <h3>5. Use of the Service</h3>
            <p>
                5.1 You may use the Service only for lawful purposes and in accordance with these Terms.
                5.2 You are responsible for ensuring that your use of the Service complies with all applicable laws and
                regulations.
                5.3 You agree not to:
                * Reverse engineer, decompile, disassemble, or modify the Service.
                * Access or use the Service in a way that disrupts, overloads, or impairs the performance of the Service
                or
                any other user's use of the Service.
                * Use the Service to store or transmit any Content that violates these Terms.
                * Attempt to gain unauthorized access to the Service or any other user's Account.
            </p>

            <h3>6. Fees and Payment</h3>
            <p>
                6.1 The Service may offer free and paid tiers. Pricing information will be available on our website or
                within the Service.
                6.2 For paid tiers, you agree to pay the applicable fees on time and in accordance with our payment
                terms.
                6.3 We reserve the right to change our pricing at any time, with or without notice.
            </p>


            <h3>7. Service Level Agreement (SLA)</h3>
            <p>
                There is no Service Level Agreement (SLA) for the Service. We do not guarantee any specific level of
                uptime.
            </p>

            <h3>8. Intellectual Property</h3>
            <p>
                8.1 The Service and all underlying technology are protected by intellectual property rights, including
                copyrights, trademarks, and patents. You agree not to remove or alter any proprietary notices on the
                Service.
                8.2 You grant us a non-exclusive, worldwide, royalty-free right to use, reproduce, modify, publish,
                distribute, and sublicense your Content solely for the purpose of providing the Service to you.
            </p>

            <h3>9. Disclaimer of Warranties</h3>
            <p>
                THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
                INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
                NON-INFRINGEMENT, OR COURSE OF DEALING. WE DO NOT WARRANT THAT THE SERVICE WILL BE UNINTERRUPTED,
                ERROR-FREE, OR VIRUS-FREE.
            </p>

            <h3>10. Limitation of Liability</h3>
            <p>
                IN NO EVENT SHALL WE, OUR AFFILIATES, OR OUR LICENSORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
                SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING OUT OF OR RELATING TO YOUR USE OF THE SERVICE,
                INCLUDING
                BUT NOT LIMITED TO DAMAGES FOR LOSS OF PROFITS, DATA, GOODWILL, OR USE, EVEN IF WE HAVE BEEN ADVISED
            </p>
        </v-card-text>

        <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn variant="text">Close</v-btn>
        </v-card-actions>
    </v-card>
</template>

<script setup>
</script>


================================================
FILE: frontend/src/layouts/default/AppBar.vue
================================================
<template>
    <div>
        <v-app-bar
            flat
            :height="(xs) ? 80 : 150"
        >
            <v-container
                fluid
                class="px-0 py-0"
            >
                <v-container class="d-flex align-center justify-center">
                    <v-app-bar-nav-icon
                        v-if="xs"
                        @click="drawer = !drawer"
                    ></v-app-bar-nav-icon>
                    <v-spacer v-if="xs"></v-spacer>

                    <div class="d-flex align-center justify-center">
                        <v-img
                            :width="(xs) ? 40 : 60"
                            src="./../../assets/logo.png"
                        ></v-img>
                        <h1
                            v-if="!xs"
                            class="ml-4"
                        >S3</h1>
                    </div>

                    <v-spacer></v-spacer>

                    <v-menu>
                        <template v-slot:activator="{ props }">
                            <v-btn
                                color="primary"
                                variant="outlined"
                                v-bind="props"
                                class="px-1"
                            >
                                <v-icon
                                    icon="mdi-account"
                                    size="large"
                                ></v-icon>
                            </v-btn>
                        </template>
                        <v-card>
                            <v-card-text class="d-flex align-center justify-center">
                                <v-btn
                                    color="primary"
                                    variant="tonal"
                                    class="px-1"
                                >
                                    {{ user.firstName.charAt(0) }}.{{ user.lastName.charAt(0) }}
                                </v-btn>
                                <p class="ml-4">
                                    <span class="font-weight-bold">{{ user.firstName }} {{ user.lastName }}</span><br>
                                    <span class="text-medium-emphasis">{{ user.id.split('-')[0].toUpperCase() }}</span>
                                </p>
                            </v-card-text>
                            <v-divider></v-divider>
                            <v-list class="pb-0 pt-0">
                                <v-list-item :to="'/logout'">
                                    <v-list-item-title>Logout</v-list-item-title>
                                </v-list-item>
                            </v-list>
                        </v-card>
                    </v-menu>
                </v-container>
                <v-divider class="d-none d-sm-block"></v-divider>
            </v-container>
        </v-app-bar>
        <v-navigation-drawer
            v-model="drawer"
            temporary
        >
            <v-list-item
                v-for="link in links"
                :to="link.href"
                :key="link.text"
                :title="link.text"
                link
            ></v-list-item>
        </v-navigation-drawer>
    </div>
</template>

<script setup>
import { ref } from 'vue';
import { useDisplay } from 'vuetify';
import { useStore } from 'vuex';

const store = useStore();
const { xs } = useDisplay();

const drawer = ref(false);
const user = ref(store.getters['user']);

const links = [
    { href: '/', text: 'Buckets' },
    { href: '/logout', text: 'Logout' },
];
</script>


================================================
FILE: frontend/src/layouts/default/Auth.vue
================================================
<template>
    <v-app class="bg-grey-lighten-3">
        <default-view />
    </v-app>
</template>

<script setup>
import DefaultBar from './AppBar.vue';
import DefaultView from './View.vue';
</script>


================================================
FILE: frontend/src/layouts/default/Default.vue
================================================
<template>
  <v-app>
    <default-bar />
    <notifications />
    <default-view />
  </v-app>
</template>

<script setup>
import DefaultBar from './AppBar.vue';
import DefaultView from './View.vue';
</script>


================================================
FILE: frontend/src/layouts/default/View.vue
================================================
<template>
  <v-main>
    <router-view />
  </v-main>
</template>

<script setup>
//
</script>


================================================
FILE: frontend/src/main.js
================================================
/**
 * main.js
 *
 * Bootstraps Vuetify and other plugins then mounts the App`
 */

// Components
import App from './App.vue';

// Composables
import { createApp } from 'vue';

// Plugins
import { registerPlugins } from '@/plugins';

const app = createApp(App);

registerPlugins(app);

app.mount('#app');


================================================
FILE: frontend/src/plugins/errorHandler.js
================================================
import { useNotification } from "@kyvg/vue3-notification";

const { notify } = useNotification();

export default function errorHandler(error, cb) {
    console.error(error);

    let code = false;
    let data = {};
    if (error.response) {
        code = error.response.status;
        data = error.response.data;
        if (code === 422) {
            // Do nothing
        } else {
            notify({
                title: error.response.data.msg || error.response.data,
            });
        }
    } else {
        notify({
            title: error.message,
        });
    }

    if (typeof cb === 'function') cb(data, code, error);
};


================================================
FILE: frontend/src/plugins/index.js
================================================
import { loadFonts } from './webfontloader';

import Notifications from '@kyvg/vue3-notification';
import vuetify from './vuetify';
import router from './router';
import store from './store';

import errorHandler from './errorHandler';
import api from './../api/index.js';

export function registerPlugins(app) {
  loadFonts();
  app
    .use(Notifications)
    .use(store)
    .use(vuetify)
    .use(router)
    .use({
      install: (app) => {
        app.provide('errorHandler', errorHandler);
        app.provide('api', api);
      },
    });
}


================================================
FILE: frontend/src/plugins/router.js
================================================
// Composables
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
    {
        path: '/',
        component: () => import('@/layouts/default/Default.vue'),
        children: [
            {
                path: '',
                name: 'Buckets',
                component: () => import(/* webpackChunkName: "home" */ '@/views/Buckets.vue'),
            },
        ],
    },

    {
        path: '/',
        component: () => import('@/layouts/default/Auth.vue'),
        children: [
            {
                path: '/login',
                name: 'Login',
                component: () => import(/* webpackChunkName: "home" */ '@/views/Login.vue'),
            },
            {
                path: '/sign-up',
                name: 'SignUp',
                component: () => import(/* webpackChunkName: "home" */ '@/views/SignUp.vue'),
            },
            {
                path: '/logout',
                name: 'Logout',
                beforeEnter: async (to, from, next) => {
                    console.warn('/logout');
                    localStorage.removeItem('AccessToken');
                    if (to.query.redirect) {
                        next(`/login?redirect=${to.query.redirect}`);
                    } else {
                        next('/login');
                    }
                },
            },
        ],
    },
];

const router = createRouter({
    history: createWebHistory(import.meta.env.VITE_BASE_URL),
    routes,
});

export default router;


================================================
FILE: frontend/src/plugins/store.js
================================================
import { createStore } from "vuex";

export default createStore({
    state: {
        user: null,
    },
    mutations: {
        setUser(state, payload) {
            state.user = payload;
        }
    },
    getters: {
        user(state) {
            return state.user;
        }
    },
});

================================================
FILE: frontend/src/plugins/vuetify.js
================================================
/**
 * plugins/vuetify.js
 *
 * Framework documentation: https://vuetifyjs.com`
 */

// Styles
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'

// Composables
import { createVuetify } from 'vuetify'

// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
  theme: {
    themes: {
      light: {
        colors: {
          primary: '#1867C0',
          secondary: '#5CBBF6',
        },
      },
    },
  },
})


================================================
FILE: frontend/src/plugins/webfontloader.js
================================================
/**
 * plugins/webfontloader.js
 *
 * webfontloader documentation: https://github.com/typekit/webfontloader
 */

export async function loadFonts () {
  const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader')

  webFontLoader.load({
    google: {
      families: ['Roboto:100,300,400,500,700,900&display=swap'],
    },
  })
}


================================================
FILE: frontend/src/styles/settings.scss
================================================
/**
 * src/styles/settings.scss
 *
 * Configures SASS variables and Vuetify overwrites
 */

// https://vuetifyjs.com/features/sass-variables/`
// @use 'vuetify/settings' with (
//   $color-pack: false
// );


.v-btn--size-default {
    min-width: 36px !important;
}

.btn-create-bucket {
    margin-top: -25px;
}

.m-auto {
    margin: auto;
}

.hide-items .v-data-table-footer__items-per-page {
    display: none;
}

.link {
    color: rgb(0, 0, 238);
    text-decoration-line: underline;
}

.credentials {
    font-size: 12px;
    text-wrap: wrap;
    border: 1px solid #cdcdcd;
    overflow: hidden;
    border-radius: 5px;
    margin: 5px 0px;
    width: 100%;
    background: #ececec;
    margin-bottom: 10px;
    max-width: 470px;

    p {
        padding: 5px 10px;
        background: #ffffff;
        border-bottom: 1px solid #cdcdcd;
    }
    
    code {
        background: #ececec;

        pre {
            padding: 5px 10px;
        }
    }
}

.v-table > .v-table__wrapper > table > tbody > tr > td,
.v-table > .v-table__wrapper > table > tbody > tr > th,
.v-table > .v-table__wrapper > table > thead > tr > td,
.v-table > .v-table__wrapper > table > thead > tr > th,
.v-table > .v-table__wrapper > table > tfoot > tr > td,
.v-table > .v-table__wrapper > table > tfoot > tr > th {
    padding: 0px 8px !important;
}

.v-table .v-table__wrapper > table > tbody > tr > td,
.v-table .v-table__wrapper > table > tbody > tr > th {
    border-bottom: none !important;
}

.v-table .v-table__wrapper > table > tbody > tr:not(:first-child):not(.expanded) > td,
.v-table .v-table__wrapper > table > tbody > tr:not(:first-child):not(.expanded) > th {
    border-top: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
    border-bottom: none;
}

.v-table .v-table__wrapper > table > tbody > tr:last-child > td,
.v-table .v-table__wrapper > table > tbody > tr:last-child > th {
    border-bottom: none  !important;
}

================================================
FILE: frontend/src/views/Buckets.vue
================================================
<template>
    <div>
        <v-container class="fill-height">
            <v-sheet
                width="100%"
                rounded
                border
            >
                <v-container class="px-4 py-4 d-flex align-center justify-center">
                    <v-text-field
                        v-model="search"
                        label="Search"
                        variant="outlined"
                        density="compact"
                    ></v-text-field>
                    <v-spacer></v-spacer>
                    <v-dialog
                        v-model="createBucketDialog"
                        max-width="350"
                    >
                        <template v-slot:activator="{ props }">
                            <v-btn
                                v-bind="props"
                                color="grey-darken-3 btn-create-bucket"
                                variant="flat"
                            >
                                Create Bucket
                            </v-btn>
                        </template>

                        <template v-slot:default="{ isActive }">
                            <CreateBucketForm @onCreateBucket="onCreateBucket" />
                        </template>
                    </v-dialog>
                </v-container>

                <v-data-table
                    :search="search"
                    :headers="headers"
                    :items="items"
                    :items-per-page-options="[]"
                    v-model:expanded="expanded"
                    class="hide-items"
                >
                    <template v-slot:no-data>
                        <div class="text-center my-4">
                            <h2>No Buckets</h2>
                            <p>Click <b>Create Bucket</b> to create a new bucket.</p>
                        </div>
                    </template>

                    <template v-slot:item.name="{ item }">
                        <p class="my-4">
                            <b>{{ item.name }}</b><br>
                            <small class="d-none d-sm-flex">
                                {{ item.endpoint }}
                            </small>
                        </p>
                    </template>

                    <template v-slot:item.status="{ item }">
                        <template
                            v-if="item.status === 'Provisioning'"
                            color="red"
                            height="6"
                            indeterminate
                            rounded
                        >
                            <p>{{ item.status }}</p>
                            <v-progress-linear
                                color="deep-purple-accent-4"
                                height="6"
                                indeterminate
                                rounded
                            ></v-progress-linear>
                        </template>
                        <v-chip
                            v-else-if="item.status === 'Provisioned'"
                            color="green"
                            size="small"
                            label
                        >
                            Provisioned
                            <v-icon
                                icon="mdi-check"
                                end
                            ></v-icon>
                        </v-chip>
                        <v-chip
                            v-else
                            size="small"
                            label
                        >
                            {{ item.status }}
                        </v-chip>
                    </template>

                    <template v-slot:item.actions="{ item }">
                        <v-dialog
                            v-model="item.deleteBucketDialog"
                            max-width="400"
                        >
                            <template v-slot:activator="{ props: activatorProps }">
                                <v-btn
                                    v-if="item.status !== 'Provisioning'"
                                    v-bind="activatorProps"
                                    size="small"
                                    variant="tonal"
                                    class="red--text"
                                >
                                    Delete
                                </v-btn>
                            </template>

                            <v-card title="Delete Bucket">
                                <v-card-text>
                                    <p>
                                        Are you sure you want to delete the bucket named <b>{{ item.name }}</b> and all
                                        of it's contents? This action cannot be undone.
                                    </p>

                                    <v-text-field
                                        v-model="item._name"
                                        label="Bucket Name"
                                        :placeholder="item.name"
                                        variant="outlined"
                                        density="compact"
                                        max-width="200"
                                        class="mt-2"
                                        @keydown.enter.prevent="deleteBucket(item)"
                                    ></v-text-field>
                                </v-card-text>
                                <template v-slot:actions>
                                    <v-spacer></v-spacer>
                                    <v-btn
                                        text
                                        @click="item.deleteBucketDialog = false"
                                    >
                                        Cancel
                                    </v-btn>
                                    <v-btn
                                        variant="flat"
                                        color="red"
                                        :loading="item.loading"
                                        :disabled="item.loading || item._name !== item.name"
                                        @click="deleteBucket(item)"
                                    >
                                        Delete
                                    </v-btn>
                                </template>
                            </v-card>
                        </v-dialog>
                    </template>

                    <template v-slot:expanded-row="{ columns, item }">
                        <tr class="expanded">
                            <td :colspan="columns.length">
                                <div class="credentials">
                                    <p class="mb-1"><v-icon
                                            size="small"
                                            icon="mdi-alert"
                                        ></v-icon> This will only be shown once.
                                    </p>
                                    <code>
<pre>
<strong>AccessKeyID:</strong><br>{{ item.accessKeyID }}
<strong>SecretAccessKey:</strong><br>{{ item.secretAccessKey }}
</pre>
</code>
                                </div>
                            </td>
                        </tr>

                    </template>
                </v-data-table>
            </v-sheet>
        </v-container>
    </div>
</template>

<script setup>
import { ref, onMounted, computed } from 'vue';
import api from './../api';
import CreateBucketForm from './../components/CreateBucketForm.vue';
import { useNotification } from "@kyvg/vue3-notification";


let updateLoop;
const { notify } = useNotification();
const items = ref([]);
const search = ref('');
const createBucketDialog = ref(false);
const headers = [
    { title: 'Name', key: 'name', width: '40%' },
    { title: 'Status', key: 'status', width: '30%' },
    { title: 'Actions', key: 'actions', width: '30%', align: 'right' },
];

const expanded = computed(() => (items.value.map(({ id, accessKeyID, secretAccessKey }) => ((accessKeyID && secretAccessKey) ? id : null))));

onMounted(async () => {
    const { data } = await api.buckets.index();
    items.value = data;
    updateLoop = setInterval(updateBuckets, (10 * 1000));
});

const updateBuckets = async () => {
    const { data: buckets } = await api.buckets.index();
    buckets.forEach((bucket) => {
        const index = items.value.findIndex((item) => item.id === bucket.id);
        if (index === -1) {
            items.value.push(bucket);
        } else {
            // This is required so you credentials are not lost
            items.value[index].status = bucket.status;
        }
    });
};

const onCreateBucket = async (bucket) => {
    createBucketDialog.value = false;
    items.value.push(bucket);
};

const deleteBucket = async (bucket) => {
    bucket.loading = true;
    await api.buckets.delete(bucket.id);
    items.value = items.value.filter((item) => item.id !== bucket.id);
    bucket.loading = true;
    bucket.deleteBucketDialog = false;
    notify({
        title: 'Bucket Deleted',
    });
};
</script>

================================================
FILE: frontend/src/views/Login.vue
================================================
<template>
    <v-container class="fill-height d-flex align-center justify-center">
        <v-sheet
            width="500"
            rounded
            border
        >
            <v-container class="px-4 py-4">
                <v-img
                    class="mb-4 d-block m-auto"
                    width="60"
                    min-width="60"
                    src="./../assets/logo.png"
                ></v-img>

                <h2 class="text-center mb-2">Login</h2>
                <v-text-field
                    v-model="email"
                    :disabled="isLoading"
                    variant="outlined"
                    label="Email"
                    required
                    density="compact"
                    class="mb-2"
                    @keydown.enter.prevent="onClickLogin"
                    :error-messages="(errors.email) ? [errors.email.msg] : []"
                ></v-text-field>
                <v-text-field
                    v-model="password"
                    :disabled="isLoading"
                    variant="outlined"
                    type="password"
                    label="Password"
                    required
                    density="compact"
                    class="mb-2"
                    @keydown.enter.prevent="onClickLogin"
                    :error-messages="(errors.password) ? [errors.password.msg] : []"
                ></v-text-field>

                <vue-hcaptcha
                    v-if="hCaptchaSiteKey"
                    @verify="onVerifyHcaptcha"
                    @expired="onExpiredHcaptcha"
                    :sitekey="hCaptchaSiteKey"
                    :key="failedAttempts"
                    class="mb-2"
                ></vue-hcaptcha>
                <p
                    v-if="errors.htoken"
                    class="text-caption text-red mb-4"
                >{{ errors.htoken.msg }}</p>

                <div>
                    <v-btn
                        :disabled="isLoading"
                        :loading="isLoading"
                        color="primary"
                        @click="onClickLogin"
                        class="mr-2"
                    >Login</v-btn>
                    <v-btn
                        :disabled="isLoading"
                        color="default"
                        :to="`/sign-up`"
                    >Sign-up</v-btn>
                </div>
            </v-container>
        </v-sheet>
    </v-container>
</template>

<script setup>
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
import { ref, inject } from 'vue';
import api from './../api';
import router from "@/plugins/router";
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';

const store = useStore();
const route = useRoute();
const errorHandler = inject('errorHandler');
const hCaptchaSiteKey = import.meta.env.VITE_H_CAPTCHA_SITE_KEY;

const isLoading = ref(false);
const errors = ref({});
const email = ref('');
const password = ref('');
const failedAttempts = ref(0);
const hCaptcha = ref(false);

const onVerifyHcaptcha = (token) => {
    hCaptcha.value = token;
};

const onExpiredHcaptcha = () => {
    hCaptcha.value = false;
};

const onClickLogin = async () => {
    try {
        isLoading.value = true;
        errors.value = {};
        const { data } = await api.auth.login({
            email: email.value,
            password: password.value,
            htoken: hCaptcha.value,
        });

        localStorage.setItem('AccessToken', data.accessToken);
        api.setJWT(data.accessToken);
        const { data: user } = await api.user.get();
        store.commit('setUser', user);

        if (route.query.redirect) {
            router.push(atob(route.query.redirect));
        } else {
            router.push('/');
        }
    } catch (error) {
        errorHandler(error, (data, code) => {
            if (code === 422) errors.value = data.errors;
            failedAttempts.value++;
        });
    } finally {
        isLoading.value = false;
    }
};
</script>

================================================
FILE: frontend/src/views/SignUp.vue
================================================
<template>
    <v-container class="fill-height d-flex align-center justify-center">
        <v-sheet
            width="500"
            rounded
            border
        >
            <v-container class="px-4 py-4">
                <v-img
                    class="mb-4 d-block m-auto"
                    width="60"
                    min-width="60"
                    src="./../assets/logo.png"
                ></v-img>

                <h2 class="text-center mb-2">Sign-up</h2>
                <v-text-field
                    v-model="firstName"
                    :disabled="isLoading"
                    variant="outlined"
                    label="Name"
                    required
                    density="compact"
                    class="mb-2"
                    :error-messages="(errors.firstName) ? [errors.firstName.msg] : []"
                ></v-text-field>
                <v-text-field
                    v-model="email"
                    :disabled="isLoading"
                    variant="outlined"
                    label="Email"
                    required
                    density="compact"
                    class="mb-2"
                    :error-messages="(errors.email) ? [errors.email.msg] : []"
                ></v-text-field>
                <v-text-field
                    v-model="password"
                    :disabled="isLoading"
                    variant="outlined"
                    type="password"
                    label="Password"
                    required
                    density="compact"
                    @keydown.enter.prevent="onClickSignUp"
                    :error-messages="(errors.password) ? [errors.password.msg] : []"
                ></v-text-field>

                <v-checkbox
                    label="Checkbox"
                    v-model="tos"
                    true-value="2023-04-22"
                    false-value=""
                    hide-details
                >
                    <template v-slot:label>
                        <div>
                            I agree to the


                            <v-dialog max-width="800">
                                <template v-slot:activator="{ props }">
                                    <span
                                        v-bind="props"
                                        class="link"
                                    >
                                        Terms of Service
                                    </span>
                                </template>

                                <template v-slot:default="{}">
                                    <TermsOfService></TermsOfService>
                                </template>
                            </v-dialog>
                        </div>
                    </template>
                </v-checkbox>


                <vue-hcaptcha
                    v-if="hCaptchaSiteKey"
                    @verify="onVerifyHcaptcha"
                    @expired="onExpiredHcaptcha"
                    :sitekey="hCaptchaSiteKey"
                    :key="failedAttempts"
                    class="mb-2"
                ></vue-hcaptcha>
                <p
                    v-if="errors.htoken"
                    class="text-caption text-red mb-4"
                >{{ errors.htoken.msg }}</p>

                <div>
                    <v-btn
                        :disabled="isLoading"
                        :loading="isLoading"
                        color="primary"
            
Download .txt
gitextract_fbxgv27m/

├── .gitignore
├── ReadMe.md
├── ansible/
│   ├── .gitignore
│   ├── .yamllint
│   ├── README.md
│   ├── ansible.cfg
│   ├── inventory/
│   │   ├── .gitignore
│   │   └── example/
│   │       ├── group_vars/
│   │       │   └── all.yml
│   │       └── hosts.ini
│   ├── reset.yml
│   ├── roles/
│   │   ├── download/
│   │   │   └── tasks/
│   │   │       └── main.yml
│   │   ├── k3s/
│   │   │   ├── master/
│   │   │   │   ├── tasks/
│   │   │   │   │   └── main.yml
│   │   │   │   └── templates/
│   │   │   │       └── k3s.service.j2
│   │   │   └── node/
│   │   │       ├── tasks/
│   │   │       │   └── main.yml
│   │   │       └── templates/
│   │   │           └── k3s.service.j2
│   │   ├── prereq/
│   │   │   └── tasks/
│   │   │       └── main.yml
│   │   ├── raspberrypi/
│   │   │   ├── handlers/
│   │   │   │   └── main.yml
│   │   │   └── tasks/
│   │   │       ├── main.yml
│   │   │       └── prereq/
│   │   │           ├── CentOS.yml
│   │   │           ├── Raspbian.yml
│   │   │           ├── Ubuntu.yml
│   │   │           └── default.yml
│   │   └── reset/
│   │       └── tasks/
│   │           ├── main.yml
│   │           └── umount_with_children.yml
│   └── site.yml
├── api/
│   ├── .dockerignore
│   ├── .eslintignore
│   ├── .gitignore
│   ├── .gitlab-ci.yml
│   ├── .sequelizerc
│   ├── Dockerfile
│   ├── LICENSE
│   ├── ReadMe.md
│   ├── docker-compose.yml
│   ├── k8s/
│   │   ├── Deploy.md
│   │   ├── api.deployment.yml
│   │   ├── api.ingress.yml
│   │   ├── api.service.yml
│   │   ├── api.ssl.ingress.yml
│   │   ├── db.yml
│   │   ├── prod.clusterissuer.yml
│   │   ├── secrets.example.yml
│   │   └── sync.job.yml
│   ├── package.json
│   ├── postman.json
│   ├── requests.http
│   ├── src/
│   │   ├── database/
│   │   │   ├── migrations/
│   │   │   │   ├── 20180726090304-create-Users.js
│   │   │   │   ├── 20180726090404-create-Groups.js
│   │   │   │   ├── 20180726090405-create-GroupsUsers.js
│   │   │   │   ├── 20240411041313-create-Buckets.js
│   │   │   │   └── 20240430101608-create-Blacklist.js
│   │   │   └── seeders/
│   │   │       ├── 20180726092449-Users.js
│   │   │       ├── 20180726093449-Group.js
│   │   │       ├── 20180726093449-GroupsUsers.js
│   │   │       ├── 20240411041313-Buckets.js
│   │   │       └── 20240430101608-Blacklist.js
│   │   ├── index.js
│   │   ├── models/
│   │   │   ├── Blacklist.js
│   │   │   ├── Bucket.js
│   │   │   ├── Group.js
│   │   │   ├── GroupsUsers.js
│   │   │   ├── User.js
│   │   │   └── index.js
│   │   ├── providers/
│   │   │   ├── bucket.yml
│   │   │   ├── connections.js
│   │   │   ├── db.js
│   │   │   ├── errorHandler.js
│   │   │   ├── generateJWT.js
│   │   │   ├── hCaptcha.js
│   │   │   └── passport.js
│   │   ├── routes/
│   │   │   ├── Buckets.js
│   │   │   ├── auth.js
│   │   │   ├── groups.js
│   │   │   ├── middleware/
│   │   │   │   ├── canAccessBucket.js
│   │   │   │   ├── checkPassword.js
│   │   │   │   ├── hCaptcha.js
│   │   │   │   ├── index.js
│   │   │   │   ├── isGroupOwner.js
│   │   │   │   ├── isInGroup.js
│   │   │   │   └── isNotSelf.js
│   │   │   └── user.js
│   │   └── scripts/
│   │       ├── blacklist.js
│   │       ├── buckets.js
│   │       ├── deleteUser.js
│   │       ├── env
│   │       ├── forgotPassword.js
│   │       ├── generate.js
│   │       ├── generator/
│   │       │   ├── Migration.js
│   │       │   ├── Model.js
│   │       │   ├── Route.js
│   │       │   └── Seeder.js
│   │       ├── inviteUser.js
│   │       ├── jwt.js
│   │       ├── refresh
│   │       ├── resetPassword.js
│   │       ├── seed
│   │       ├── sync.js
│   │       └── users.js
│   └── tests/
│       ├── Auth.js
│       ├── Group.js
│       ├── HealthCheck.js
│       └── User.js
├── automation-test/
│   ├── .gitlab-ci.yml
│   ├── Dockerfile
│   ├── bucket.yml
│   └── deployment.yml
├── aws-sdk-test/
│   ├── .gitignore
│   ├── index.js
│   └── package.json
├── deployment-test/
│   ├── .gitlab-ci.yml
│   ├── Dockerfile
│   ├── index.html
│   └── k8s.yml
├── frontend/
│   ├── .browserslistrc
│   ├── .editorconfig
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── .gitlab-ci.yml
│   ├── Dockerfile
│   ├── ReadMe.md
│   ├── default.conf
│   ├── index.html
│   ├── jsconfig.json
│   ├── k8s/
│   │   ├── clusterissuer.yml
│   │   ├── frontend-ssl.ingress.yml
│   │   ├── frontend.deployment.yml
│   │   ├── frontend.ingress.yml
│   │   └── frontend.service.yml
│   ├── package.json
│   ├── src/
│   │   ├── App.vue
│   │   ├── api/
│   │   │   ├── Auth.js
│   │   │   ├── Buckets.js
│   │   │   ├── Service.js
│   │   │   ├── User.js
│   │   │   └── index.js
│   │   ├── components/
│   │   │   ├── CreateBucketForm.vue
│   │   │   └── TermsOfService.vue
│   │   ├── layouts/
│   │   │   └── default/
│   │   │       ├── AppBar.vue
│   │   │       ├── Auth.vue
│   │   │       ├── Default.vue
│   │   │       └── View.vue
│   │   ├── main.js
│   │   ├── plugins/
│   │   │   ├── errorHandler.js
│   │   │   ├── index.js
│   │   │   ├── router.js
│   │   │   ├── store.js
│   │   │   ├── vuetify.js
│   │   │   └── webfontloader.js
│   │   ├── styles/
│   │   │   └── settings.scss
│   │   └── views/
│   │       ├── Buckets.vue
│   │       ├── Login.vue
│   │       └── SignUp.vue
│   └── vite.config.js
├── k3s/
│   ├── alpine.deployment.yml
│   ├── echo.s3.ssl.yml
│   ├── echo.ssl.yml
│   └── echo.yml
├── longhorn/
│   ├── longhorn.ingress.yml
│   ├── longhorn.lb.yml
│   └── longhorn.storageclass.yml
├── node/
│   └── node-config-script.sh
└── sections/
    ├── automated-bucket-deployment.md
    ├── console.md
    ├── deploying-from-gitlab-to-k3s.md
    ├── gitlab.md
    ├── internet.md
    ├── networking.md
    ├── node.md
    ├── production-cluster.md
    ├── ssl.md
    └── storage-cluster.md
Download .txt
SYMBOL INDEX (24 symbols across 10 files)

FILE: api/tests/Group.js
  constant GROUP_ID (line 9) | const GROUP_ID = 'fdab7a99-2c38-444b-bcb3-f7cef61c275b';
  constant OTHER_GROUP_ID (line 10) | const OTHER_GROUP_ID = '190c8a70-34d1-4281-a775-850058453704';

FILE: frontend/src/api/Auth.js
  class Auth (line 3) | class Auth extends Service {
    method login (line 4) | login(data) {
    method signUp (line 8) | signUp(data) {

FILE: frontend/src/api/Buckets.js
  class Bucket (line 3) | class Bucket extends Service {
    method index (line 4) | index() {
    method get (line 8) | get(bucketID) {
    method create (line 12) | create(bucket) {
    method delete (line 16) | delete(bucketID) {

FILE: frontend/src/api/Service.js
  class Service (line 3) | class Service {
    method constructor (line 4) | constructor(JWT) {

FILE: frontend/src/api/User.js
  class User (line 3) | class User extends Service {
    method get (line 4) | get() {
    method stats (line 8) | stats() {

FILE: frontend/src/api/index.js
  class API (line 5) | class API {
    method constructor (line 6) | constructor(JWT) {
    method setJWT (line 10) | setJWT(JWT) {
    method getJWT (line 17) | getJWT() {

FILE: frontend/src/plugins/errorHandler.js
  function errorHandler (line 5) | function errorHandler(error, cb) {

FILE: frontend/src/plugins/index.js
  function registerPlugins (line 11) | function registerPlugins(app) {

FILE: frontend/src/plugins/store.js
  method setUser (line 8) | setUser(state, payload) {
  method user (line 13) | user(state) {

FILE: frontend/src/plugins/webfontloader.js
  function loadFonts (line 7) | async function loadFonts () {
Condensed preview — 171 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (251K chars).
[
  {
    "path": ".gitignore",
    "chars": 55,
    "preview": ".DS_Store\n\nnotes/*\n*.crt\n*.k8s\n\napi_\nfrontend_\nwebsite_"
  },
  {
    "path": "ReadMe.md",
    "chars": 6710,
    "preview": "# S3 From Scratch\n\n<p align=\"center\">\n  <img width=\"300\" src=\"https://raw.githubusercontent.com/anthonybudd/s3-from-scra"
  },
  {
    "path": "ansible/.gitignore",
    "chars": 8,
    "preview": "Notes.md"
  },
  {
    "path": "ansible/.yamllint",
    "chars": 137,
    "preview": "---\nextends: default\n\nrules:\n  line-length:\n    max: 120\n    level: warning\n  truthy:\n    allowed-values: ['true', 'fals"
  },
  {
    "path": "ansible/README.md",
    "chars": 98,
    "preview": "# Ansible\n\nSource: [https://github.com/k3s-io/k3s-ansible](https://github.com/k3s-io/k3s-ansible)\n"
  },
  {
    "path": "ansible/ansible.cfg",
    "chars": 258,
    "preview": "[defaults]\nnocows = True\nroles_path = ./roles\ninventory  = ./hosts.ini\n\nremote_tmp = $HOME/.ansible/tmp\nlocal_tmp  = $HO"
  },
  {
    "path": "ansible/inventory/.gitignore",
    "chars": 41,
    "preview": "*-cluster/\n!exmaple/\n!.gitignore\n!sample/"
  },
  {
    "path": "ansible/inventory/example/group_vars/all.yml",
    "chars": 221,
    "preview": "---\nk3s_version: v1.26.9+k3s1\nansible_user: node\nsystemd_dir: /etc/systemd/system\nmaster_ip: \"{{ hostvars[groups['master"
  },
  {
    "path": "ansible/inventory/example/hosts.ini",
    "chars": 89,
    "preview": "[master]\n10.0.0.5\n\n[node]\n10.0.0.5\n10.0.0.6\n10.0.0.7\n\n[k3s_cluster:children]\nmaster\nnode\n"
  },
  {
    "path": "ansible/reset.yml",
    "chars": 87,
    "preview": "---\n\n- hosts: k3s_cluster\n  gather_facts: yes\n  become: yes\n  roles:\n    - role: reset\n"
  },
  {
    "path": "ansible/roles/download/tasks/main.yml",
    "chars": 1252,
    "preview": "---\n\n- name: Download k3s binary x64\n  get_url:\n    url: https://github.com/k3s-io/k3s/releases/download/{{ k3s_version "
  },
  {
    "path": "ansible/roles/k3s/master/tasks/main.yml",
    "chars": 1804,
    "preview": "---\n\n- name: Copy K3s service file\n  register: k3s_service\n  template:\n    src: \"k3s.service.j2\"\n    dest: \"{{ systemd_d"
  },
  {
    "path": "ansible/roles/k3s/master/templates/k3s.service.j2",
    "chars": 626,
    "preview": "[Unit]\nDescription=Lightweight Kubernetes\nDocumentation=https://k3s.io\nAfter=network-online.target\n\n[Service]\nType=notif"
  },
  {
    "path": "ansible/roles/k3s/node/tasks/main.yml",
    "chars": 296,
    "preview": "---\n\n- name: Copy K3s service file\n  template:\n    src: \"k3s.service.j2\"\n    dest: \"{{ systemd_dir }}/k3s-node.service\"\n"
  },
  {
    "path": "ansible/roles/k3s/node/templates/k3s.service.j2",
    "chars": 715,
    "preview": "[Unit]\nDescription=Lightweight Kubernetes\nDocumentation=https://k3s.io\nAfter=network-online.target\n\n[Service]\nType=notif"
  },
  {
    "path": "ansible/roles/prereq/tasks/main.yml",
    "chars": 1433,
    "preview": "---\n- name: Set SELinux to disabled state\n  selinux:\n    state: disabled\n  when: ansible_distribution in ['CentOS', 'Red"
  },
  {
    "path": "ansible/roles/raspberrypi/handlers/main.yml",
    "chars": 29,
    "preview": "---\n- name: reboot\n  reboot:\n"
  },
  {
    "path": "ansible/roles/raspberrypi/tasks/main.yml",
    "chars": 1788,
    "preview": "---\n- name: Test for raspberry pi /proc/cpuinfo\n  command: grep -E \"Raspberry Pi|BCM2708|BCM2709|BCM2835|BCM2836\" /proc/"
  },
  {
    "path": "ansible/roles/raspberrypi/tasks/prereq/CentOS.yml",
    "chars": 319,
    "preview": "---\n- name: Enable cgroup via boot commandline if not already enabled for Centos\n  lineinfile:\n    path: /boot/cmdline.t"
  },
  {
    "path": "ansible/roles/raspberrypi/tasks/prereq/Raspbian.yml",
    "chars": 693,
    "preview": "---\n- name: Activating cgroup support\n  lineinfile:\n    path: /boot/cmdline.txt\n    regexp: '^((?!.*\\bcgroup_enable=cpus"
  },
  {
    "path": "ansible/roles/raspberrypi/tasks/prereq/Ubuntu.yml",
    "chars": 346,
    "preview": "---\n- name: Enable cgroup via boot commandline if not already enabled for Ubuntu on a Raspberry Pi\n  lineinfile:\n    pat"
  },
  {
    "path": "ansible/roles/raspberrypi/tasks/prereq/default.yml",
    "chars": 4,
    "preview": "---\n"
  },
  {
    "path": "ansible/roles/reset/tasks/main.yml",
    "chars": 958,
    "preview": "---\n- name: Disable services\n  systemd:\n    name: \"{{ item }}\"\n    state: stopped\n    enabled: no\n  failed_when: false\n "
  },
  {
    "path": "ansible/roles/reset/tasks/umount_with_children.yml",
    "chars": 474,
    "preview": "---\n- name: Get the list of mounted filesystems\n  shell: set -o pipefail && cat /proc/mounts | awk '{ print $2}' | grep "
  },
  {
    "path": "ansible/site.yml",
    "chars": 255,
    "preview": "---\n\n- hosts: k3s_cluster\n  gather_facts: yes\n  become: yes\n  roles:\n    - role: prereq\n    - role: download\n    - role:"
  },
  {
    "path": "api/.dockerignore",
    "chars": 30,
    "preview": "node_modules\npackage-lock.json"
  },
  {
    "path": "api/.eslintignore",
    "chars": 19,
    "preview": "src/database/\ntests"
  },
  {
    "path": "api/.gitignore",
    "chars": 107,
    "preview": "node_modules/\n.vol/\n.env\ndev.js\nprivate.pem\npublic.pem\n.DS_Store\n*/.DS_Store\nk8s/secrets.yml\nkubeconfig.yml"
  },
  {
    "path": "api/.gitlab-ci.yml",
    "chars": 314,
    "preview": "stages:\n  - build\n\nbuild-job:\n  image: docker:dind\n  stage: build\n  services:\n    - docker:dind\n  variables:\n    IMAGE_T"
  },
  {
    "path": "api/.sequelizerc",
    "chars": 323,
    "preview": "const path = require('path');\n\nmodule.exports = {\n    'config':           path.resolve('src/providers', 'connections.js'"
  },
  {
    "path": "api/Dockerfile",
    "chars": 406,
    "preview": "FROM node:20\n\nRUN apt-get update && apt-get install -y curl nano\nRUN curl -LO \"https://dl.k8s.io/release/$(curl -L -s ht"
  },
  {
    "path": "api/LICENSE",
    "chars": 1066,
    "preview": "The MIT License\n\nCopyright Anthony C. Budd\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "api/ReadMe.md",
    "chars": 2884,
    "preview": "# S3 API\n\nThis API simulates the back-end of the AWS Console. A user can sign-up, login, create a bucket then delete the"
  },
  {
    "path": "api/docker-compose.yml",
    "chars": 939,
    "preview": "version: \"3\"\n\nservices:\n  s3-api:\n    build: .\n    entrypoint: \"nodemon /app/src/index.js --watch /app --legacy-watch\"\n "
  },
  {
    "path": "api/k8s/Deploy.md",
    "chars": 3388,
    "preview": "# Deploy\n\n\n\n\nkubectl --kubeconfig=.kube/config create namespace s3-api\nnamespace/s3-api created\n[Console]:~> kubectl --k"
  },
  {
    "path": "api/k8s/api.deployment.yml",
    "chars": 2194,
    "preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: s3-api\n  namespace: s3-api\nspec:\n  replicas: 1\n  selector:\n    ma"
  },
  {
    "path": "api/k8s/api.ingress.yml",
    "chars": 356,
    "preview": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  namespace: s3-api\n  name: s3-ingress\n  annotations:\n    kuber"
  },
  {
    "path": "api/k8s/api.service.yml",
    "chars": 148,
    "preview": "apiVersion: v1\nkind: Service\nmetadata:\n  name: s3-api\n  namespace: s3-api\nspec:\n  ports:\n  - port: 80\n    targetPort: 80"
  },
  {
    "path": "api/k8s/api.ssl.ingress.yml",
    "chars": 512,
    "preview": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  namespace: s3-api\n  name: s3-ingress\n  annotations:\n    cert-"
  },
  {
    "path": "api/k8s/db.yml",
    "chars": 1007,
    "preview": "apiVersion: v1\nkind: Service\nmetadata:\n  name: s3-db\n  namespace: s3-api\nspec:\n  selector:    \n    app: s3-db \n  ports: "
  },
  {
    "path": "api/k8s/prod.clusterissuer.yml",
    "chars": 337,
    "preview": "apiVersion: cert-manager.io/v1\nkind: ClusterIssuer\nmetadata:\n  name: letsencrypt-prod\n  namespace: cert-manager\nspec:\n  "
  },
  {
    "path": "api/k8s/secrets.example.yml",
    "chars": 123,
    "preview": "apiVersion: v1    \nkind: Secret\nmetadata:\n  name: s3-api-secrets\n  namespace: default\ntype: Opaque\ndata:\n  DB_PASSWORD: "
  },
  {
    "path": "api/k8s/sync.job.yml",
    "chars": 1732,
    "preview": "apiVersion: batch/v1\nkind: CronJob\nmetadata:\n  name: sync\n  namespace: s3-api\nspec:\n  schedule: \"* * * * *\"\n  jobTemplat"
  },
  {
    "path": "api/package.json",
    "chars": 2327,
    "preview": "{\n    \"name\": \"s3-api-boilerplate\",\n    \"version\": \"1.0.0\",\n    \"main\": \"./src/index.js\",\n    \"author\": \"Anthony Budd\",\n"
  },
  {
    "path": "api/postman.json",
    "chars": 5046,
    "preview": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"994858ef-e55e-425c-9aac-1cf12496a933\",\n\t\t\"name\": \"s3-api-Boilerplate\",\n\t\t\"schema\": \"https"
  },
  {
    "path": "api/requests.http",
    "chars": 1431,
    "preview": "# Install VS Code extension rest-client \n# URL: https://marketplace.visualstudio.com/items?itemName=humao.rest-client\n\n@"
  },
  {
    "path": "api/src/database/migrations/20180726090304-create-Users.js",
    "chars": 1215,
    "preview": "module.exports = {\n    up: (queryInterface, Sequelize) => queryInterface.createTable('Users', {\n        id: {\n          "
  },
  {
    "path": "api/src/database/migrations/20180726090404-create-Groups.js",
    "chars": 724,
    "preview": "module.exports = {\n    up: (queryInterface, Sequelize) => queryInterface.createTable('Groups', {\n        id: {\n         "
  },
  {
    "path": "api/src/database/migrations/20180726090405-create-GroupsUsers.js",
    "chars": 835,
    "preview": "module.exports = {\n    up: (queryInterface, Sequelize) => queryInterface.createTable('GroupsUsers', {\n        id: {  // "
  },
  {
    "path": "api/src/database/migrations/20240411041313-create-Buckets.js",
    "chars": 1460,
    "preview": "module.exports = {\n    up: (queryInterface, Sequelize) => queryInterface.createTable('Buckets', {\n        id: {\n        "
  },
  {
    "path": "api/src/database/migrations/20240430101608-create-Blacklist.js",
    "chars": 665,
    "preview": "module.exports = {\n    up: (queryInterface, Sequelize) => queryInterface.createTable('Blacklist', {\n        id: {\n      "
  },
  {
    "path": "api/src/database/seeders/20180726092449-Users.js",
    "chars": 1066,
    "preview": "const bcrypt = require('bcrypt-nodejs');\nconst moment = require('moment');\nconst faker = require('faker');\n\nconst insert"
  },
  {
    "path": "api/src/database/seeders/20180726093449-Group.js",
    "chars": 710,
    "preview": "const moment = require('moment');\n\nconst insert = [{\n    id: 'fdab7a99-2c38-444b-bcb3-f7cef61c275b',\n    ownerID: 'c4644"
  },
  {
    "path": "api/src/database/seeders/20180726093449-GroupsUsers.js",
    "chars": 968,
    "preview": "const moment = require('moment');\n\nconst insert = [\n    {\n        id: '1872dcde-b79d-4f28-a36b-a22af519ac23',\n        us"
  },
  {
    "path": "api/src/database/seeders/20240411041313-Buckets.js",
    "chars": 607,
    "preview": "const moment = require('moment');\n\nconst insert = [{\n    id: 'fae8a1fb-bc90-4565-b567-1fe6846544de',\n    createdAt: mome"
  },
  {
    "path": "api/src/database/seeders/20240430101608-Blacklist.js",
    "chars": 5907,
    "preview": "const { v4: uuidv4 } = require('uuid');\n\nconst blacklist = [\n    'about',\n    'aboutu',\n    'abuse',\n    'acme',\n    'ad"
  },
  {
    "path": "api/src/index.js",
    "chars": 1875,
    "preview": "require('dotenv').config();\nrequire('./providers/passport');\nconst fileUpload = require('express-fileupload');\nconst exp"
  },
  {
    "path": "api/src/models/Blacklist.js",
    "chars": 686,
    "preview": "const Sequelize = require('sequelize');\nconst db = require('./../providers/db');\n\nconst Blacklist = db.define('Blacklist"
  },
  {
    "path": "api/src/models/Bucket.js",
    "chars": 5421,
    "preview": "const { exec } = require('child_process');\nconst db = require('./../providers/db');\nconst Sequelize = require('sequelize"
  },
  {
    "path": "api/src/models/Group.js",
    "chars": 472,
    "preview": "const Sequelize = require('sequelize');\nconst db = require('./../providers/db');\n\nmodule.exports = db.define('Group', {\n"
  },
  {
    "path": "api/src/models/GroupsUsers.js",
    "chars": 469,
    "preview": "const Sequelize = require('sequelize');\nconst db = require('./../providers/db');\n\nmodule.exports = db.define('GroupsUser"
  },
  {
    "path": "api/src/models/User.js",
    "chars": 1038,
    "preview": "const Sequelize = require('sequelize');\nconst db = require('./../providers/db');\n\nmodule.exports = db.define('User', {\n "
  },
  {
    "path": "api/src/models/index.js",
    "chars": 499,
    "preview": "const User = require('./User');\nconst Group = require('./Group');\nconst GroupsUsers = require('./GroupsUsers');\nconst Bu"
  },
  {
    "path": "api/src/providers/bucket.yml",
    "chars": 2890,
    "preview": "apiVersion: v1\nkind: Namespace\nmetadata:\n  name: NAMESPACE_HERE\n  labels:\n    name: NAMESPACE_HERE\n---\napiVersion: v1\nki"
  },
  {
    "path": "api/src/providers/connections.js",
    "chars": 822,
    "preview": "require('dotenv').config();\n\nmodule.exports = {\n    development: {\n        username: process.env.DB_USERNAME,\n        pa"
  },
  {
    "path": "api/src/providers/db.js",
    "chars": 968,
    "preview": "const Sequelize = require('sequelize');\nconst connections = require('./connections');\nconst errorHandler = require('./er"
  },
  {
    "path": "api/src/providers/errorHandler.js",
    "chars": 621,
    "preview": "const crypto = require('crypto');\n\nmodule.exports = (err, res) => {\n    if (err.isAxiosError) {\n        console.log(`Axi"
  },
  {
    "path": "api/src/providers/generateJWT.js",
    "chars": 777,
    "preview": "const jwt = require('jsonwebtoken');\nconst moment = require('moment');\nconst fs = require('fs');\n\nmodule.exports = (user"
  },
  {
    "path": "api/src/providers/hCaptcha.js",
    "chars": 483,
    "preview": "const axios = require('axios');\nconst qs = require('qs');\n\nconst hCaptcha = axios.create({\n    baseURL: 'https://hcaptch"
  },
  {
    "path": "api/src/providers/passport.js",
    "chars": 1291,
    "preview": "const LocalStrategy = require('passport-local').Strategy;\nconst { User, Group } = require('./../models');\nconst passport"
  },
  {
    "path": "api/src/routes/Buckets.js",
    "chars": 3287,
    "preview": "const { body, validationResult, matchedData } = require('express-validator');\nconst errorHandler = require('./../provide"
  },
  {
    "path": "api/src/routes/auth.js",
    "chars": 10372,
    "preview": "const { body, validationResult, matchedData } = require('express-validator');\nconst { User, Group, GroupsUsers } = requi"
  },
  {
    "path": "api/src/routes/groups.js",
    "chars": 4056,
    "preview": "const { body, validationResult, matchedData } = require('express-validator');\nconst { User, Group, GroupsUsers } = requi"
  },
  {
    "path": "api/src/routes/middleware/canAccessBucket.js",
    "chars": 556,
    "preview": "const { Bucket } = require('./../../models');\n\nmodule.exports = async (req, res, next) => {\n    const bucketID = (req.pa"
  },
  {
    "path": "api/src/routes/middleware/checkPassword.js",
    "chars": 1159,
    "preview": "const errorHandler = require('./../../providers/errorHandler');\nconst { User } = require('./../../models');\nconst bcrypt"
  },
  {
    "path": "api/src/routes/middleware/hCaptcha.js",
    "chars": 842,
    "preview": "const hCaptcha = require('./../../providers/hCaptcha');\n\nmodule.exports = async (req, res, next) => {\n\n    if (!process."
  },
  {
    "path": "api/src/routes/middleware/index.js",
    "chars": 401,
    "preview": "const checkPassword = require('./checkPassword');\nconst isInGroup = require('./isInGroup');\nconst isNotSelf = require('."
  },
  {
    "path": "api/src/routes/middleware/isGroupOwner.js",
    "chars": 431,
    "preview": "const { Group } = require('./../../models');\n\nmodule.exports = async (req, res, next) => {\n    const groupID = (req.para"
  },
  {
    "path": "api/src/routes/middleware/isInGroup.js",
    "chars": 677,
    "preview": "const { User, Group } = require('./../../models');\n\nmodule.exports = async (req, res, next) => {\n    const groupID = (re"
  },
  {
    "path": "api/src/routes/middleware/isNotSelf.js",
    "chars": 315,
    "preview": "module.exports = (req, res, next) => {\n    if (!req.user || !req.user.id) return res.status(401).json({\n        msg: 'Ac"
  },
  {
    "path": "api/src/routes/user.js",
    "chars": 2321,
    "preview": "const { body, validationResult, matchedData } = require('express-validator');\nconst errorHandler = require('./../provide"
  },
  {
    "path": "api/src/scripts/blacklist.js",
    "chars": 680,
    "preview": "\n/**\n * node ./src/scripts/blacklist.js --value=\"bad_word\"\n * docker exec -ti s3-api node ./src/scripts/blacklist.js --v"
  },
  {
    "path": "api/src/scripts/buckets.js",
    "chars": 613,
    "preview": "\n/**\n * node ./src/scripts/buckets.js\n * docker exec -ti s3-api node ./src/scripts/buckets.js\n *\n */\nrequire('dotenv').c"
  },
  {
    "path": "api/src/scripts/deleteUser.js",
    "chars": 761,
    "preview": "\n/**\n * node ./src/scripts/deleteUser.js --userID=\"fdab7a99-2c38-444b-bcb3-f7cef61c275b\"\n * docker exec -ti s3-api node "
  },
  {
    "path": "api/src/scripts/env",
    "chars": 131,
    "preview": "#!/bin/bash\n\nif [[ $NODE_ENV == \"production\" ]]\n  then\n    echo \"NODE_ENV=production ⚠️\"\n  else\n    echo \"NODE_ENV=$NODE"
  },
  {
    "path": "api/src/scripts/forgotPassword.js",
    "chars": 1047,
    "preview": "\n/**\n * node ./src/scripts/forgotPassword.js --userID=\"c4644733-deea-47d8-b35a-86f30ff9618e\"\n * docker exec -ti s3-api n"
  },
  {
    "path": "api/src/scripts/generate.js",
    "chars": 2138,
    "preview": "\n/**\n * node ./src/scripts/generate.js --modelName=\"bucket\"\n * docker exec -ti s3-api node ./src/scripts/generate.js --m"
  },
  {
    "path": "api/src/scripts/generator/Migration.js",
    "chars": 679,
    "preview": "module.exports = {\n    up: (queryInterface, Sequelize) => queryInterface.createTable('{{ ModelNames }}', {\n        id: {"
  },
  {
    "path": "api/src/scripts/generator/Model.js",
    "chars": 690,
    "preview": "const Sequelize = require('sequelize');\nconst db = require('./../providers/db');\n\nmodule.exports = db.define('{{ ModelNa"
  },
  {
    "path": "api/src/scripts/generator/Route.js",
    "chars": 1832,
    "preview": "const { body, validationResult, matchedData } = require('express-validator');\nconst errorHandler = require('./../provide"
  },
  {
    "path": "api/src/scripts/generator/Seeder.js",
    "chars": 382,
    "preview": "const moment = require('moment');\n\nconst insert = [{\n    id: '{{ UUID }}',\n    createdAt: moment().format('YYYY-MM-DD HH"
  },
  {
    "path": "api/src/scripts/inviteUser.js",
    "chars": 2223,
    "preview": "\n/**\n * node ./src/scripts/inviteUser.js --email=\"newuser@example.com\" --groupID=\"fdab7a99-2c38-444b-bcb3-f7cef61c275b\"\n"
  },
  {
    "path": "api/src/scripts/jwt.js",
    "chars": 806,
    "preview": "\n/**\n * node ./src/scripts/jwt.js --userID=\"c4644733-deea-47d8-b35a-86f30ff9618e\"\n * docker exec -ti s3-api node ./src/s"
  },
  {
    "path": "api/src/scripts/refresh",
    "chars": 205,
    "preview": "#!/bin/bash\n\nif [[ $NODE_ENV == \"production\" ]]\n  then\n    echo \"ERROR: Can not refresh while in production\"\n  else\n    "
  },
  {
    "path": "api/src/scripts/resetPassword.js",
    "chars": 1083,
    "preview": "\n/**\n * node ./src/scripts/resetPassword.js --userID=\"c4644733-deea-47d8-b35a-86f30ff9618e\" --password=\"password\"\n * doc"
  },
  {
    "path": "api/src/scripts/seed",
    "chars": 143,
    "preview": "#!/bin/bash\n\nif [[ $NODE_ENV == \"production\" ]]\n  then\n    echo \"ERROR: Can not seed while in production\"\n  else\n    seq"
  },
  {
    "path": "api/src/scripts/sync.js",
    "chars": 491,
    "preview": "/**\n * node ./src/scripts/sync.js\n * docker exec -ti s3-api node ./src/scripts/sync.js\n *\n */\n\nrequire('dotenv').config("
  },
  {
    "path": "api/src/scripts/users.js",
    "chars": 556,
    "preview": "\n/**\n * node ./src/scripts/users.js\n * docker exec -ti s3-api node ./src/scripts/users.js\n *\n */\nrequire('dotenv').confi"
  },
  {
    "path": "api/tests/Auth.js",
    "chars": 6030,
    "preview": "const chai = require('chai');\nconst chaiHttp = require('chai-http');\nconst server = require('../src');\nconst should = ch"
  },
  {
    "path": "api/tests/Group.js",
    "chars": 5025,
    "preview": "require('dotenv').config();\nconst chai = require('chai');\nconst chaiHttp = require('chai-http');\nconst server = require("
  },
  {
    "path": "api/tests/HealthCheck.js",
    "chars": 670,
    "preview": "const chai = require('chai');\nconst chaiHttp = require('chai-http');\nconst server = require('../src');\nconst should = ch"
  },
  {
    "path": "api/tests/User.js",
    "chars": 2683,
    "preview": "require('dotenv').config();\nconst chai = require('chai');\nconst chaiHttp = require('chai-http');\nconst server = require("
  },
  {
    "path": "automation-test/.gitlab-ci.yml",
    "chars": 314,
    "preview": "stages:\n  - build\n\nbuild-job:\n  image: docker:dind\n  stage: build\n  services:\n    - docker:dind\n  variables:\n    IMAGE_T"
  },
  {
    "path": "automation-test/Dockerfile",
    "chars": 283,
    "preview": "FROM ubuntu:noble\n\nRUN apt-get update && apt-get install -y curl\n\nRUN curl -LO \"https://dl.k8s.io/release/$(curl -L -s h"
  },
  {
    "path": "automation-test/bucket.yml",
    "chars": 1405,
    "preview": "apiVersion: v1\nkind: Namespace\nmetadata:\n  name: XXX\n  labels:\n    name: XXX\n---\napiVersion: v1\nkind: Pod\nmetadata:\n  la"
  },
  {
    "path": "automation-test/deployment.yml",
    "chars": 908,
    "preview": "kind: Deployment    \napiVersion: apps/v1   \nmetadata:            \n  name: automation-test-deployment    \n  labels:      "
  },
  {
    "path": "aws-sdk-test/.gitignore",
    "chars": 13,
    "preview": "node_modules/"
  },
  {
    "path": "aws-sdk-test/index.js",
    "chars": 1086,
    "preview": "const { S3Client, ListObjectsV2Command, PutObjectCommand } = require(\"@aws-sdk/client-s3\");\n\nconst Bucket = 'kjdoewl';\nc"
  },
  {
    "path": "aws-sdk-test/package.json",
    "chars": 298,
    "preview": "{\n  \"name\": \"aws-sdk-test\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no tes"
  },
  {
    "path": "deployment-test/.gitlab-ci.yml",
    "chars": 314,
    "preview": "stages:\n  - build\n\nbuild-job:\n  image: docker:dind\n  stage: build\n  services:\n    - docker:dind\n  variables:\n    IMAGE_T"
  },
  {
    "path": "deployment-test/Dockerfile",
    "chars": 46,
    "preview": "FROM nginx:alpine\nCOPY . /usr/share/nginx/html"
  },
  {
    "path": "deployment-test/index.html",
    "chars": 52,
    "preview": "<h1>Compiled on GitLab</h1>\n<h1>Deployed on K3s</h1>"
  },
  {
    "path": "deployment-test/k8s.yml",
    "chars": 1064,
    "preview": "kind: Deployment    \napiVersion: apps/v1   \nmetadata:            \n  name: website-deployment    \n  labels:        \n    a"
  },
  {
    "path": "frontend/.browserslistrc",
    "chars": 40,
    "preview": "> 1%\nlast 2 versions\nnot dead\nnot ie 11\n"
  },
  {
    "path": "frontend/.editorconfig",
    "chars": 121,
    "preview": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true"
  },
  {
    "path": "frontend/.eslintrc.js",
    "chars": 142,
    "preview": "module.exports = {\n  root: true,\n  env: {\n    node: true,\n  },\n  extends: [\n    'plugin:vue/vue3-essential',\n    'eslint"
  },
  {
    "path": "frontend/.gitignore",
    "chars": 235,
    "preview": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*"
  },
  {
    "path": "frontend/.gitlab-ci.yml",
    "chars": 493,
    "preview": "stages:\n  - build\n\nbuild-job:\n  image: docker:dind\n  stage: build\n  services:\n    - docker:dind\n  variables:\n    IMAGE_T"
  },
  {
    "path": "frontend/Dockerfile",
    "chars": 93,
    "preview": "FROM nginx\nCOPY default.conf /etc/nginx/conf.d/default.conf\nCOPY ./dist /usr/share/nginx/html"
  },
  {
    "path": "frontend/ReadMe.md",
    "chars": 418,
    "preview": "# Frontend\n\nThis represents the AWS console found at [aws.amazon.com/console](https://aws.amazon.com/console/). This is "
  },
  {
    "path": "frontend/default.conf",
    "chars": 1139,
    "preview": "server {\n    listen       80;\n    listen  [::]:80;\n    server_name  localhost;\n\n    #access_log  /var/log/nginx/host.acc"
  },
  {
    "path": "frontend/index.html",
    "chars": 314,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <link rel=\"icon\" href=\"/favicon.ico\" />\n  <meta na"
  },
  {
    "path": "frontend/jsconfig.json",
    "chars": 279,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"module\": \"esnext\",\n    \"baseUrl\": \"./\",\n    \"moduleResolution\": \"node"
  },
  {
    "path": "frontend/k8s/clusterissuer.yml",
    "chars": 284,
    "preview": "apiVersion: cert-manager.io/v1\nkind: ClusterIssuer\nmetadata:\n  name: letsencrypt-prod\nspec:\n  acme:\n    server: https://"
  },
  {
    "path": "frontend/k8s/frontend-ssl.ingress.yml",
    "chars": 516,
    "preview": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: frontend-ingress\n  namespace: s3-api\n  annotations:\n   "
  },
  {
    "path": "frontend/k8s/frontend.deployment.yml",
    "chars": 533,
    "preview": "kind: Deployment    \napiVersion: apps/v1   \nmetadata:            \n  name: frontend-deployment    \n  namespace: s3-api\n  "
  },
  {
    "path": "frontend/k8s/frontend.ingress.yml",
    "chars": 387,
    "preview": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: frontend-ingress\n  namespace: s3-api\n  annotations:\n   "
  },
  {
    "path": "frontend/k8s/frontend.service.yml",
    "chars": 193,
    "preview": "apiVersion: v1\nkind: Service  \nmetadata:\n  name: frontend-service  \n  namespace: s3-api\nspec:\n  selector:    \n    app: f"
  },
  {
    "path": "frontend/package.json",
    "chars": 759,
    "preview": "{\n  \"name\": \"frontend\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite bu"
  },
  {
    "path": "frontend/src/App.vue",
    "chars": 987,
    "preview": "<template>\n    <router-view v-if=\"isLoaded\" />\n    <notifications />\n</template>\n\n<script setup>\nimport { ref, onMounted"
  },
  {
    "path": "frontend/src/api/Auth.js",
    "chars": 245,
    "preview": "import Service from './Service';\n\nclass Auth extends Service {\n    login(data) {\n        return this.axios.post('/auth/l"
  },
  {
    "path": "frontend/src/api/Buckets.js",
    "chars": 404,
    "preview": "import Service from './Service';\n\nclass Bucket extends Service {\n    index() {\n        return this.axios.get(`/buckets`)"
  },
  {
    "path": "frontend/src/api/Service.js",
    "chars": 423,
    "preview": "import axios from 'axios';\n\nclass Service {\n    constructor(JWT) {\n        this.JWT = JWT;\n\n        this.url = import.me"
  },
  {
    "path": "frontend/src/api/User.js",
    "chars": 207,
    "preview": "import Service from './Service';\n\nclass User extends Service {\n    get() {\n        return this.axios.get('/user');\n    }"
  },
  {
    "path": "frontend/src/api/index.js",
    "chars": 441,
    "preview": "import Auth from './Auth';\nimport User from './User';\nimport Buckets from './Buckets';\n\nclass API {\n    constructor(JWT)"
  },
  {
    "path": "frontend/src/components/CreateBucketForm.vue",
    "chars": 2435,
    "preview": "<template>\n    <v-card title=\"Create Bucket\">\n        <v-card-text>\n            <v-text-field\n                v-model=\"n"
  },
  {
    "path": "frontend/src/components/TermsOfService.vue",
    "chars": 6003,
    "preview": "<template>\n    <v-card>\n        <v-card-text>\n            <h1>Terms of Service for s3.anthonybudd.io</h1>\n\n            <"
  },
  {
    "path": "frontend/src/layouts/default/AppBar.vue",
    "chars": 3655,
    "preview": "<template>\n    <div>\n        <v-app-bar\n            flat\n            :height=\"(xs) ? 80 : 150\"\n        >\n            <v-"
  },
  {
    "path": "frontend/src/layouts/default/Auth.vue",
    "chars": 202,
    "preview": "<template>\n    <v-app class=\"bg-grey-lighten-3\">\n        <default-view />\n    </v-app>\n</template>\n\n<script setup>\nimpor"
  },
  {
    "path": "frontend/src/layouts/default/Default.vue",
    "chars": 210,
    "preview": "<template>\n  <v-app>\n    <default-bar />\n    <notifications />\n    <default-view />\n  </v-app>\n</template>\n\n<script setu"
  },
  {
    "path": "frontend/src/layouts/default/View.vue",
    "chars": 95,
    "preview": "<template>\n  <v-main>\n    <router-view />\n  </v-main>\n</template>\n\n<script setup>\n//\n</script>\n"
  },
  {
    "path": "frontend/src/main.js",
    "chars": 305,
    "preview": "/**\n * main.js\n *\n * Bootstraps Vuetify and other plugins then mounts the App`\n */\n\n// Components\nimport App from './App"
  },
  {
    "path": "frontend/src/plugins/errorHandler.js",
    "chars": 649,
    "preview": "import { useNotification } from \"@kyvg/vue3-notification\";\n\nconst { notify } = useNotification();\n\nexport default functi"
  },
  {
    "path": "frontend/src/plugins/index.js",
    "chars": 549,
    "preview": "import { loadFonts } from './webfontloader';\n\nimport Notifications from '@kyvg/vue3-notification';\nimport vuetify from '"
  },
  {
    "path": "frontend/src/plugins/router.js",
    "chars": 1524,
    "preview": "// Composables\nimport { createRouter, createWebHistory } from 'vue-router';\n\nconst routes = [\n    {\n        path: '/',\n "
  },
  {
    "path": "frontend/src/plugins/store.js",
    "chars": 296,
    "preview": "import { createStore } from \"vuex\";\n\nexport default createStore({\n    state: {\n        user: null,\n    },\n    mutations:"
  },
  {
    "path": "frontend/src/plugins/vuetify.js",
    "chars": 478,
    "preview": "/**\n * plugins/vuetify.js\n *\n * Framework documentation: https://vuetifyjs.com`\n */\n\n// Styles\nimport '@mdi/font/css/mat"
  },
  {
    "path": "frontend/src/plugins/webfontloader.js",
    "chars": 360,
    "preview": "/**\n * plugins/webfontloader.js\n *\n * webfontloader documentation: https://github.com/typekit/webfontloader\n */\n\nexport "
  },
  {
    "path": "frontend/src/styles/settings.scss",
    "chars": 1934,
    "preview": "/**\n * src/styles/settings.scss\n *\n * Configures SASS variables and Vuetify overwrites\n */\n\n// https://vuetifyjs.com/fea"
  },
  {
    "path": "frontend/src/views/Buckets.vue",
    "chars": 9367,
    "preview": "<template>\n    <div>\n        <v-container class=\"fill-height\">\n            <v-sheet\n                width=\"100%\"\n       "
  },
  {
    "path": "frontend/src/views/Login.vue",
    "chars": 4067,
    "preview": "<template>\n    <v-container class=\"fill-height d-flex align-center justify-center\">\n        <v-sheet\n            width=\""
  },
  {
    "path": "frontend/src/views/SignUp.vue",
    "chars": 5658,
    "preview": "<template>\n    <v-container class=\"fill-height d-flex align-center justify-center\">\n        <v-sheet\n            width=\""
  },
  {
    "path": "frontend/vite.config.js",
    "chars": 837,
    "preview": "// Plugins\nimport vue from '@vitejs/plugin-vue'\nimport vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'\n\n// Ut"
  },
  {
    "path": "k3s/alpine.deployment.yml",
    "chars": 446,
    "preview": "kind: Deployment    \napiVersion: apps/v1   \nmetadata:            \n  name: alpine-deployment    \n  labels:        \n    ap"
  },
  {
    "path": "k3s/echo.s3.ssl.yml",
    "chars": 1157,
    "preview": "kind: Deployment    \napiVersion: apps/v1   \nmetadata:            \n  name: echo-deployment    \n  labels:        \n    app:"
  },
  {
    "path": "k3s/echo.ssl.yml",
    "chars": 1148,
    "preview": "kind: Deployment    \napiVersion: apps/v1   \nmetadata:            \n  name: echo-deployment    \n  labels:        \n    app:"
  },
  {
    "path": "k3s/echo.yml",
    "chars": 1026,
    "preview": "kind: Deployment    \napiVersion: apps/v1   \nmetadata:            \n  name: echo-deployment    \n  labels:        \n    app:"
  },
  {
    "path": "longhorn/longhorn.ingress.yml",
    "chars": 387,
    "preview": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  namespace: longhorn-system\n  name: longhorn-ingress\n  annotat"
  },
  {
    "path": "longhorn/longhorn.lb.yml",
    "chars": 263,
    "preview": "apiVersion: v1\nkind: Service\nmetadata:\n  name: longhorn-lb\n  namespace: longhorn-system\nspec:\n  selector:\n    app: longh"
  },
  {
    "path": "longhorn/longhorn.storageclass.yml",
    "chars": 316,
    "preview": "---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n  name: longhorn\nprovisioner: driver.longhorn.io\nallowVol"
  },
  {
    "path": "node/node-config-script.sh",
    "chars": 1091,
    "preview": "#!/bin/bash\n\n# Update\nsudo apt update -y\nsudo apt full-upgrade -y\n\n\n# Fail2Ban - https://pimylifeup.com/raspberry-pi-fai"
  },
  {
    "path": "sections/automated-bucket-deployment.md",
    "chars": 3906,
    "preview": "# Automated Bucket Deployment\n\nWhen a user creates a bucket we will want the bucket to be created automatically with out"
  },
  {
    "path": "sections/console.md",
    "chars": 2020,
    "preview": "# Console\n\n<img height=\"400\" src=\"https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/console-clos"
  },
  {
    "path": "sections/deploying-from-gitlab-to-k3s.md",
    "chars": 5969,
    "preview": "# Deploying From GitLab Registry To Local K3s\n\nWe need to be able to deploy the SaaS front-end and REST API for S3 from "
  },
  {
    "path": "sections/gitlab.md",
    "chars": 9091,
    "preview": "# Installing GitLab on a Raspberry Pi 4\n\nFirst you will need to build a single node. Flash the SD card with a copy of 32"
  },
  {
    "path": "sections/internet.md",
    "chars": 3977,
    "preview": "# Connecting to the Internet\n\nIn a true datacenter we would request a static IP address from our ISP and point our DNS s"
  },
  {
    "path": "sections/networking.md",
    "chars": 32,
    "preview": "# Networking\n\n_AB: In Progress_\n"
  },
  {
    "path": "sections/node.md",
    "chars": 1376,
    "preview": "# Node\n\n<img height=\"300\" src=\"https://raw.githubusercontent.com/anthonybudd/s3-from-scratch/master/_img/node.png\">\n\n###"
  },
  {
    "path": "sections/production-cluster.md",
    "chars": 4001,
    "preview": "# K3S: Production cluster\n\nThis guide will cover how to set-up the production kubernetes cluster for hosting our public "
  },
  {
    "path": "sections/ssl.md",
    "chars": 2397,
    "preview": "# SSL\n\nkubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.1/deploy/static/prov"
  },
  {
    "path": "sections/storage-cluster.md",
    "chars": 9037,
    "preview": "# K3S: Storage Cluster\n\nThis section will cover how to set-up the storage kubernetes cluster which will store of the dat"
  }
]

About this extraction

This page contains the full source code of the anthonybudd/s3-from-scratch GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 171 files (224.5 KB), approximately 62.9k tokens, and a symbol index with 24 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!