Full Code of jc21/docker-registry-ui for AI

master 2643028aeda5 cached
65 files
120.3 KB
30.8k tokens
8 symbols
1 requests
Download .txt
Repository: jc21/docker-registry-ui
Branch: master
Commit: 2643028aeda5
Files: 65
Total size: 120.3 KB

Directory structure:
gitextract_kgk3g63p/

├── .babelrc
├── .gitignore
├── Dockerfile
├── Jenkinsfile
├── LICENCE
├── README.md
├── bin/
│   ├── build
│   ├── build-dev
│   ├── npm
│   ├── watch
│   └── yarn
├── doc/
│   └── full-stack/
│       └── docker-compose.yml
├── docker-compose.yml
├── nodemon.json
├── package.json
├── src/
│   ├── backend/
│   │   ├── app.js
│   │   ├── index.js
│   │   ├── internal/
│   │   │   └── repo.js
│   │   ├── lib/
│   │   │   ├── docker-registry.js
│   │   │   ├── error.js
│   │   │   ├── express/
│   │   │   │   ├── cors.js
│   │   │   │   └── pagination.js
│   │   │   ├── helpers.js
│   │   │   └── validator/
│   │   │       ├── api.js
│   │   │       └── index.js
│   │   ├── logger.js
│   │   ├── routes/
│   │   │   ├── api/
│   │   │   │   ├── main.js
│   │   │   │   └── repos.js
│   │   │   └── main.js
│   │   └── schema/
│   │       ├── definitions.json
│   │       ├── endpoints/
│   │       │   ├── rules.json
│   │       │   ├── services.json
│   │       │   ├── templates.json
│   │       │   ├── tokens.json
│   │       │   └── users.json
│   │       ├── examples.json
│   │       └── index.json
│   └── frontend/
│       ├── app-images/
│       │   └── favicons/
│       │       ├── browserconfig.xml
│       │       └── site.webmanifest
│       ├── html/
│       │   └── index.html
│       ├── js/
│       │   ├── actions.js
│       │   ├── components/
│       │   │   ├── app/
│       │   │   │   ├── image-tag.js
│       │   │   │   └── insecure-registries.js
│       │   │   └── tabler/
│       │   │       ├── big-error.js
│       │   │       ├── icon-stat-card.js
│       │   │       ├── modal.js
│       │   │       ├── nav.js
│       │   │       ├── stat-card.js
│       │   │       ├── table-body.js
│       │   │       ├── table-card.js
│       │   │       ├── table-head.js
│       │   │       └── table-row.js
│       │   ├── index.js
│       │   ├── lib/
│       │   │   ├── api.js
│       │   │   ├── manipulators.js
│       │   │   └── utils.js
│       │   ├── router.js
│       │   ├── routes/
│       │   │   ├── image.js
│       │   │   ├── images.js
│       │   │   └── instructions/
│       │   │       ├── deleting.js
│       │   │       ├── pulling.js
│       │   │       └── pushing.js
│       │   └── state.js
│       └── scss/
│           └── styles.scss
└── webpack.config.js

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

================================================
FILE: .babelrc
================================================
{
  "presets": [
    ["env", {
      "targets": {
        "browsers": ["Chrome >= 65"]
      },
      "debug": false,
      "modules": false,
      "useBuiltIns": "usage"
    }]
  ]
}


================================================
FILE: .gitignore
================================================
.idea
._*
.DS_Store
node_modules
dist/*
package-lock.json
yarn-error.log
yarn.lock
webpack_stats.html
tmp/*
.env
.yarnrc



================================================
FILE: Dockerfile
================================================
FROM jc21/node:latest

MAINTAINER Jamie Curnow <jc@jc21.com>
LABEL maintainer="Jamie Curnow <jc@jc21.com>"

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

ENV NODE_ENV=production

ADD dist                /app/dist
ADD node_modules        /app/node_modules
ADD LICENCE             /app/LICENCE
ADD package.json        /app/package.json
ADD src/backend         /app/src/backend

WORKDIR /app

CMD node --max_old_space_size=250 --abort_on_uncaught_exception src/backend/index.js

HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://localhost/ || exit 1



================================================
FILE: Jenkinsfile
================================================
pipeline {
	agent any
	options {
		buildDiscarder(logRotator(numToKeepStr: '10'))
		disableConcurrentBuilds()
	}
	environment {
		IMAGE           = "registry-ui"
		TAG_VERSION     = getPackageVersion()
		BRANCH_LOWER    = "${BRANCH_NAME.toLowerCase().replaceAll('/', '-')}"
		BUILDX_NAME     = "${COMPOSE_PROJECT_NAME}"
		BASE_IMAGE_NAME = "jc21/node:latest"
		TEMP_IMAGE_NAME = "${IMAGE}-build_${BUILD_NUMBER}"
	}
	stages {
		stage('Prepare') {
			steps {
				sh 'docker pull "${BASE_IMAGE_NAME}"'
				sh 'docker pull "${DOCKER_CI_TOOLS}"'
			}
		}
		stage('Build') {
			steps {
				// Codebase
				sh 'docker run --rm -v $(pwd):/app -w /app "${BASE_IMAGE_NAME}" yarn install'
				sh 'docker run --rm -v $(pwd):/app -w /app "${BASE_IMAGE_NAME}" yarn build'
				sh 'docker run --rm -v $(pwd):/app -w /app "${BASE_IMAGE_NAME}" chown -R "$(id -u):$(id -g)" *'
				sh 'rm -rf node_modules'
				sh 'docker run --rm -v $(pwd):/app -w /app "${BASE_IMAGE_NAME}" yarn install --prod'
				sh 'docker run --rm -v $(pwd):/data "${DOCKER_CI_TOOLS}" node-prune'

				// Docker Build
				sh 'docker build --pull --no-cache --squash --compress -t "${TEMP_IMAGE_NAME}" .'

				// Zip it
				sh 'rm -rf zips'
				sh 'mkdir -p zips'
				sh '''docker run --rm -v "$(pwd):/data/docker-registry-ui" -w /data "${DOCKER_CI_TOOLS}" zip -qr "/data/docker-registry-ui/zips/docker-registry-ui_${TAG_VERSION}.zip" docker-registry-ui -x \\
						\\*.gitkeep \\
						docker-registry-ui/zips\\* \\
						docker-registry-ui/bin\\* \\
						docker-registry-ui/src/frontend\\* \\
						docker-registry-ui/tmp\\* \\
						docker-registry-ui/node_modules\\* \\
						docker-registry-ui/.git\\* \\
						docker-registry-ui/.env \\
						docker-registry-ui/.babelrc \\
						docker-registry-ui/yarn\\* \\
						docker-registry-ui/.gitignore \\
						docker-registry-ui/Dockerfile \\
						docker-registry-ui/nodemon.json \\
						docker-registry-ui/webpack.config.js \\
						docker-registry-ui/webpack_stats.html
				'''
			}
			post {
				always {
					sh 'docker run --rm -v $(pwd):/app -w /app "${BASE_IMAGE_NAME}" chown -R "$(id -u):$(id -g)" *'
				}
			}
		}
		stage('Publish Develop') {
			when {
				branch 'develop'
			}
			steps {
				sh 'docker tag "${TEMP_IMAGE_NAME}" "jc21/${IMAGE}:develop"'
				withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
					sh "docker login -u '${duser}' -p '$dpass'"
					sh 'docker push "jc21/${IMAGE}:develop"'
				}

				// Artifacts
				dir(path: 'zips') {
					archiveArtifacts(artifacts: '**/*.zip', caseSensitive: true, onlyIfSuccessful: true)
				}
			}
		}
		stage('Publish Master') {
			when {
				branch 'master'
			}
			steps {
				// Public Registry
				sh 'docker tag "${TEMP_IMAGE_NAME}" "jc21/${IMAGE}:latest"'
				sh 'docker tag "${TEMP_IMAGE_NAME}" "jc21/${IMAGE}:${TAG_VERSION}"'
				withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
					sh "docker login -u '${duser}' -p '$dpass'"
					sh 'docker push "jc21/${IMAGE}:latest"'
					sh 'docker push "jc21/${IMAGE}:${TAG_VERSION}"'
				}

				// Artifacts
				dir(path: 'zips') {
					archiveArtifacts(artifacts: '**/*.zip', caseSensitive: true, onlyIfSuccessful: true)
				}
			}
		}
	}
	triggers {
		bitbucketPush()
	}
	post {
		success {
			juxtapose event: 'success'
			sh 'figlet "SUCCESS"'
		}
		failure {
			juxtapose event: 'failure'
			sh 'figlet "FAILURE"'
		}
		always {
			sh 'docker rmi "${TEMP_IMAGE_NAME}"'
		}
	}
}

def getPackageVersion() {
	ver = sh(script: 'docker run --rm -v $(pwd):/data "${DOCKER_CI_TOOLS}" bash -c "cat /data/package.json|jq -r \'.version\'"', returnStdout: true)
	return ver.trim()
}


================================================
FILE: LICENCE
================================================
The MIT License (MIT)

Copyright (c) 2017 Jamie Curnow, Brisbane Australia (https://jc21.com)

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: README.md
================================================
![Docker Registry UI](https://public.jc21.com/docker-registry-ui/github.png "Docker Registry UI")

# Docker Registry UI

![Version](https://img.shields.io/badge/version-2.0.2-green.svg)
![Stars](https://img.shields.io/docker/stars/jc21/registry-ui.svg)
![Pulls](https://img.shields.io/docker/pulls/jc21/registry-ui.svg)

Have you ever wanted a visual website to show you the contents of your Docker Registry? Look no further. Now you can list your Images, Tags and info in style.

This project comes as a [pre-built docker image](https://hub.docker.com/r/jc21/registry-ui/) capable of connecting to another registry.

Note: This project only works with Docker Registry v2.


## Getting started

### Creating a full Docker Registry Stack with this UI

By far the easiest way to get up and running. Refer to the example [docker-compose.yml](https://github.com/jc21/docker-registry-ui/blob/master/doc/full-stack/docker-compose.yml)
example file, put it on your Docker host and run:

```bash
docker-compose up -d
```

Then hit your server on http://127.0.0.1


### If you have your own Docker Registry to connect to

Here's a `docker-compose.yml` for you:

```bash
version: "2"
services:
  app:
    image: jc21/registry-ui
    ports:
      - 80:80
    environment:
      - REGISTRY_HOST=your-registry-server.com:5000
      - REGISTRY_SSL=true
      - REGISTRY_DOMAIN=your-registry-server.com:5000
      - REGISTRY_STORAGE_DELETE_ENABLED=
      - REGISTRY_USER=
      - REGISTRY_PASS=
    restart: on-failure
```

If you are like most people and want your docker registry and your docker ui to co-exist on the same domain on the same port, please
refer to the Nginx configuration used by the [docker-registry-ui-proxy image](https://github.com/jc21/docker-registry-ui-proxy/blob/master/conf.d/proxy.conf)
as an example. Note that there are some tweaks in there that you will need to be able to push successfully.


## Environment Variables

- **`REGISTRY_HOST`** - *Required:* The registry hostname and optional port to connect to for API calls
- **`REGISTRY_SSL`** - *Optional:* Specify `true` for this if the registry is accessed via HTTPS
- **`REGISTRY_DOMAIN`** - *Optional:* This is the registry domain to display in the UI for example push/pull code
- **`REGISTRY_STORAGE_DELETE_ENABLED`** - *Optional:* Specify `true` or `1` to enable deletion features, but see below first!
- **`REGISTRY_USER`** - *Optional:* If your docker registry is behind basic auth, specify the username
- **`REGISTRY_PASS`** - *Optional:* If your docker registry is behind basic auth, specify the password

Refer to the docker documentation for setting up [native basic auth](https://docs.docker.com/registry/deploying/#restricting-access).


## Deletion Support

Registry deletion support sux. It is disabled by default in this project on purpose
because you need to accomplish extra steps to get it up and running, sort of.

#### Permit deleting on the Registry

This step is pretty simple and involves adding an environment variable to your Docker Registry Container when you start it up:

```bash
docker run -d -p 5000:5000 -e REGISTRY_STORAGE_DELETE_ENABLED=true --name my-registry registry:2
```

#### Enabling Deletions in the UI

Same as the Registry, just add the **`REGISTRY_STORAGE_DELETE_ENABLED=true`** environment variable to the `registry-ui` container. Note that `true` is the only
acceptable value for this environment variable.


#### Cleaning up the Registry

When you delete an image from the registry this won't actually remove the blob layers as you might expect. For this reason you have to run this command on your docker registry host to perform garbage collection:

```bash
docker exec -it my-registry bin/registry garbage-collect /etc/docker/registry/config.yml
```

And if you wanted to make a cron job that runs every 30 mins:

```
0,30 * * * * /bin/docker exec -it my-registry bin/registry garbage-collect /etc/docker/registry/config.yml >> /dev/null 2>&1
```


## Screenshots

[![Dashboard](https://public.jc21.com/docker-registry-ui/screenshots/small/drui-1.jpg "Dashboard")](https://public.jc21.com/docker-registry-ui/screenshots/drui-1.jpg)
[![Image](https://public.jc21.com/docker-registry-ui/screenshots/small/drui-2.jpg "Image")](https://public.jc21.com/docker-registry-ui/screenshots/drui-2.jpg)
[![Pulling](https://public.jc21.com/docker-registry-ui/screenshots/small/drui-3.jpg "Pulling")](https://public.jc21.com/docker-registry-ui/screenshots/drui-3.jpg)
[![Pushing](https://public.jc21.com/docker-registry-ui/screenshots/small/drui-4.jpg "Pushing")](https://public.jc21.com/docker-registry-ui/screenshots/drui-4.jpg)


## TODO

- Add pagination to Repositories, currently only 300 images will be fetched
- Add support for token based registry authentication mechanisms


================================================
FILE: bin/build
================================================
#!/bin/bash

sudo /usr/local/bin/docker-compose run --no-deps --rm app npm run-script build
exit $?


================================================
FILE: bin/build-dev
================================================
#!/bin/bash

sudo /usr/local/bin/docker-compose run --no-deps --rm app npm run-script dev
exit $?


================================================
FILE: bin/npm
================================================
#!/bin/bash

sudo /usr/local/bin/docker-compose run --no-deps --rm app npm $@
exit $?


================================================
FILE: bin/watch
================================================
#!/bin/bash

sudo docker run --rm -it \
  -p 8124:8080 \
  -v $(pwd):/app \
  -w /app \
  jc21/node:latest npm run-script watch

exit $?


================================================
FILE: bin/yarn
================================================
#!/bin/bash

sudo /usr/local/bin/docker-compose run --no-deps --rm app yarn $@
exit $?


================================================
FILE: doc/full-stack/docker-compose.yml
================================================
version: "2"
services:
  registry:
    image: registry:2
    environment:
      - REGISTRY_HTTP_SECRET=o43g2kjgn2iuhv2k4jn2f23f290qfghsdg
      - REGISTRY_STORAGE_DELETE_ENABLED=
    volumes:
      - ./registry-data:/var/lib/registry
  ui:
    image: jc21/registry-ui
    environment:
      - NODE_ENV=production
      - REGISTRY_HOST=registry:5000
      - REGISTRY_SSL=
      - REGISTRY_DOMAIN=
      - REGISTRY_STORAGE_DELETE_ENABLED=
    links:
      - registry
    restart: on-failure
  proxy:
    image: jc21/registry-ui-proxy
    ports:
      - 80:80
    depends_on:
      - ui
      - registry
    links:
      - ui
      - registry
    restart: on-failure


================================================
FILE: docker-compose.yml
================================================
version: "2"
services:
  app:
    image: jc21/node:latest
    ports:
      - 4000:80
    environment:
      - DEBUG=
      - FORCE_COLOR=1
      - NODE_ENV=development
      - REGISTRY_HOST=${REGISTRY_HOST}
      - REGISTRY_DOMAIN=${REGISTRY_HOST}
      - REGISTRY_STORAGE_DELETE_ENABLED=true
      - REGISTRY_SSL=${REGISTRY_SSL}
      - REGISTRY_USER=${REGISTRY_USER}
      - REGISTRY_PASS=${REGISTRY_PASS}
    volumes:
      - .:/app
    working_dir: /app
    command: node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js



================================================
FILE: nodemon.json
================================================
{
    "verbose": false,
    "ignore": ["dist", "data", "src/frontend"],
    "ext": "js json ejs"
}


================================================
FILE: package.json
================================================
{
    "name": "docker-registry-ui",
    "version": "2.0.2",
    "description": "A nice web interface for managing your Docker Registry images",
    "main": "src/backend/index.js",
    "dependencies": {
        "ajv": "^6.5.4",
        "batchflow": "^0.4.0",
        "body-parser": "^1.18.3",
        "compression": "^1.7.3",
        "config": "^2.0.1",
        "ejs": "^2.6.1",
        "express": "^4.16.4",
        "express-winston": "^3.0.1",
        "html-entities": "^1.2.1",
        "json-schema-ref-parser": "^6.0.1",
        "lodash": "^4.17.11",
        "path": "^0.12.7",
        "restler": "^3.4.0",
        "signale": "^1.2.1"
    },
    "devDependencies": {
        "babel-core": "^6.26.3",
        "babel-loader": "^7.1.4",
        "babel-minify-webpack-plugin": "^0.3.1",
        "babel-preset-env": "^1.7.0",
        "@hyperapp/html": "git+https://github.com/maxholman/hyperapp-html.git#5bde674d42c87bb8191f8cc11a8a3c7d334e3dfb",
        "babel-plugin-transform-react-jsx": "^6.24.1",
        "copy-webpack-plugin": "^4.5.4",
        "css-loader": "^1.0.0",
        "file-loader": "^2.0.0",
        "html-loader": "^0.5.5",
        "html-webpack-plugin": "^3.2.0",
        "hyperapp": "^1.2.9",
        "hyperapp-hash-router": "^0.1.0",
        "imports-loader": "^0.8.0",
        "jquery": "^3.3.1",
        "jquery-serializejson": "^2.8.1",
        "mini-css-extract-plugin": "^0.4.4",
        "moment": "^2.22.2",
        "node-sass": "^4.9.4",
        "nodemon": "^1.18.4",
        "numeral": "^2.0.6",
        "sass-loader": "^7.1.0",
        "style-loader": "^0.23.1",
        "tabler-ui": "git+https://github.com/tabler/tabler.git#a09fd463309f2b395653e3615c98d1e8aca35b31",
        "uglifyjs-webpack-plugin": "^2.0.1",
        "webpack": "^4.12.0",
        "webpack-cli": "^3.0.8",
        "webpack-visualizer-plugin": "^0.1.11"
    },
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "dev": "webpack --mode development",
        "build": "webpack --mode production",
        "watch": "webpack-dev-server --mode development"
    },
    "signale": {
        "displayDate": true,
        "displayTimestamp": true
    },
    "author": "",
    "license": "MIT"
}


================================================
FILE: src/backend/app.js
================================================
'use strict';

const express     = require('express');
const bodyParser  = require('body-parser');
const compression = require('compression');
const log         = require('./logger').express;

/**
 * App
 */
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(compression());

/**
 * General Logging, BEFORE routes
 */
app.disable('x-powered-by');
app.enable('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
app.enable('strict routing');

// pretty print JSON when not live
if (process.env.NODE_ENV !== 'production') {
    app.set('json spaces', 2);
}

// set the view engine to ejs
app.set('view engine', 'ejs');

// CORS for everything
app.use(require('./lib/express/cors'));

// General security/cache related headers + server header
app.use(function (req, res, next) {
    res.set({
        'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload',
        'X-XSS-Protection':          '0',
        'X-Content-Type-Options':    'nosniff',
        'X-Frame-Options':           'DENY',
        'Cache-Control':             'no-cache, no-store, max-age=0, must-revalidate',
        Pragma:                      'no-cache',
        Expires:                     0
    });
    next();
});

/**
 * Routes
 */
app.use('/assets', express.static('dist/assets'));
app.use('/css', express.static('dist/css'));
app.use('/fonts', express.static('dist/fonts'));
app.use('/images', express.static('dist/images'));
app.use('/js', express.static('dist/js'));
app.use('/api', require('./routes/api/main'));
app.use('/', require('./routes/main'));

// production error handler
// no stacktraces leaked to user
app.use(function (err, req, res, next) {

    let payload = {
        error: {
            code:    err.status,
            message: err.public ? err.message : 'Internal Error'
        }
    };

    if (process.env.NODE_ENV === 'development') {
        payload.debug = {
            stack:    typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null,
            previous: err.previous
        };
    }

    // Not every error is worth logging - but this is good for now until it gets annoying.
    if (typeof err.stack !== 'undefined' && err.stack) {
        log.warn(err.stack);
    }

    res
        .status(err.status || 500)
        .send(payload);
});

module.exports = app;


================================================
FILE: src/backend/index.js
================================================
#!/usr/bin/env node

'use strict';

const logger = require('./logger').global;
const config = require('config');

let port = process.env.PORT || 80;

if (config.has('port')) {
    port = config.get('port');
}

if (!process.env.REGISTRY_HOST) {
    logger.error('Error: REGISTRY_HOST environment variable was not found!');
    process.exit(1);
}

function appStart () {

    const app          = require('./app');
    const apiValidator = require('./lib/validator/api');

    return apiValidator.loadSchemas
        .then(() => {
            const server = app.listen(port, () => {
                logger.info('PID ' + process.pid + ' listening on port ' + port + ' ...');
                logger.info('Registry Host: ' + process.env.REGISTRY_HOST);

                process.on('SIGTERM', () => {
                    logger.info('PID ' + process.pid + ' received SIGTERM');
                    server.close(() => {
                        logger.info('Stopping.');
                        process.exit(0);
                    });
                });
            });
        })
        .catch(err => {
            logger.error(err.message);
            setTimeout(appStart, 1000);
        });
}

try {
    appStart();
} catch (err) {
    logger.error(err.message, err);
    process.exit(1);
}


================================================
FILE: src/backend/internal/repo.js
================================================
'use strict';

const REGISTRY_HOST = process.env.REGISTRY_HOST;
const REGISTRY_SSL  = process.env.REGISTRY_SSL && process.env.REGISTRY_SSL.toLowerCase() === 'true' || parseInt(process.env.REGISTRY_SSL, 10) === 1;
const REGISTRY_USER = process.env.REGISTRY_USER;
const REGISTRY_PASS = process.env.REGISTRY_PASS;

const _         = require('lodash');
const Docker    = require('../lib/docker-registry');
const batchflow = require('batchflow');
const registry  = new Docker(REGISTRY_HOST, REGISTRY_SSL, REGISTRY_USER, REGISTRY_PASS);
const errors    = require('../lib/error');
const logger    = require('../logger').registry;

const internalRepo = {

    /**
     * @param  {String}   name
     * @param  {Boolean}  full
     * @return {Promise}
     */
    get: (name, full) => {
        return registry.getImageTags(name)
            .then(tags_data => {
                // detect errors
                if (typeof tags_data.errors !== 'undefined' && tags_data.errors.length) {
                    let top_err = tags_data.errors.shift();
                    if (top_err.code === 'NAME_UNKNOWN') {
                        throw new errors.ItemNotFoundError(name);
                    } else {
                        throw new errors.RegistryError(top_err.code, top_err.message);
                    }
                }

                if (full && tags_data.tags !== null) {
                    // Order the tags naturally, but put latest at the top if it exists
                    let latest_idx = tags_data.tags.indexOf('latest');
                    if (latest_idx !== -1) {
                        _.pullAt(tags_data.tags, [latest_idx]);
                    }

                    // sort
                    tags_data.tags = tags_data.tags.sort((a, b) => a.localeCompare(b));

                    if (latest_idx !== -1) {
                        tags_data.tags.unshift('latest');
                    }

                    return new Promise((resolve, reject) => {
                        batchflow(tags_data.tags).sequential()
                            .each((i, tag, next) => {
                                // for each tag, we want to get 2 manifests.
                                // Version 2 returns the layers and the correct image id
                                // Version 1 returns the history we want to pluck from
                                registry.getManifest(tags_data.name, tag, 2)
                                    .then(manifest2_result => {
                                        manifest2_result.name       = tag;
                                        manifest2_result.image_name = name;

                                        return registry.getManifest(tags_data.name, tag, 1)
                                            .then(manifest1_result => {
                                                manifest2_result.info = null;

                                                if (typeof manifest1_result.history !== 'undefined' && manifest1_result.history.length) {
                                                    let info = manifest1_result.history.shift();
                                                    if (typeof info.v1Compatibility !== undefined) {
                                                        info = JSON.parse(info.v1Compatibility);

                                                        // Remove cruft
                                                        if (typeof info.config !== 'undefined') {
                                                            delete info.config;
                                                        }

                                                        if (typeof info.container_config !== 'undefined') {
                                                            delete info.container_config;
                                                        }
                                                    }

                                                    manifest2_result.info = info;
                                                }

                                                next(manifest2_result);
                                            });
                                    })
                                    .catch(err => {
                                        logger.error(err);
                                        next(null);
                                    });
                            })
                            .error(err => {
                                reject(err);
                            })
                            .end(results => {
                                tags_data.tags = results || null;
                                resolve(tags_data);
                            });
                    });
                } else {
                    return tags_data;
                }
            });
    },

    /**
     * All repos
     *
     * @param   {Boolean}   [with_tags]
     * @returns {Promise}
     */
    getAll: with_tags => {
        return registry.getImages()
            .then(result => {
                if (typeof result.errors !== 'undefined' && result.errors.length) {
                    let first_err = result.errors.shift();
                    throw new errors.RegistryError(first_err.code, first_err.message);
                } else if (typeof result.repositories !== 'undefined') {
                    let repositories = [];

                    // sort images
                    result.repositories = result.repositories.sort((a, b) => a.localeCompare(b));

                    _.map(result.repositories, function (repo) {
                        repositories.push({
                            name: repo
                        });
                    });

                    return repositories;
                }

                return result;
            })
            .then(images => {
                if (with_tags) {
                    return new Promise((resolve, reject) => {
                        batchflow(images).sequential()
                            .each((i, image, next) => {
                                let image_result = image;
                                // for each image
                                registry.getImageTags(image.name)
                                    .then(tags_result => {
                                        if (typeof tags_result === 'string') {
                                            // usually some sort of error
                                            logger.error('Tags result was: ', tags_result);
                                            image_result.tags = null;
                                        } else if (typeof tags_result.tags !== 'undefined' && tags_result.tags !== null) {
                                            // Order the tags naturally, but put latest at the top if it exists
                                            let latest_idx = tags_result.tags.indexOf('latest');
                                            if (latest_idx !== -1) {
                                                _.pullAt(tags_result.tags, [latest_idx]);
                                            }

                                            // sort tags
                                            image_result.tags = tags_result.tags.sort((a, b) => a.localeCompare(b));

                                            if (latest_idx !== -1) {
                                                image_result.tags.unshift('latest');
                                            }
                                        }

                                        next(image_result);
                                    })
                                    .catch(err => {
                                        logger.error(err);
                                        image_result.tags = null;
                                        next(image_result);
                                    });
                            })
                            .error(err => {
                                reject(err);
                            })
                            .end(results => {
                                resolve(results);
                            });
                    });
                } else {
                    return images;
                }
            });
    },

    /**
     * Delete a image/tag
     *
     * @param   {String}   name
     * @param   {String}   digest
     * @returns {Promise}
     */
    delete: (name, digest) => {
        return registry.deleteImage(name, digest);
    }
};

module.exports = internalRepo;


================================================
FILE: src/backend/lib/docker-registry.js
================================================
'use strict';

const _    = require('lodash');
const rest = require('restler');

/**
 *
 * @param   {String}   domain
 * @param   {Boolean}  use_ssl
 * @param   {String}   [username]
 * @param   {String}   [password]
 * @returns {module}
 */
module.exports = function (domain, use_ssl, username, password) {

    this._baseurl = 'http' + (use_ssl ? 's' : '') + '://' + (username ? username + ':' + password + '@' : '') + domain + '/v2/';

    /**
     * @param   {Integer}  [version]
     * @returns {Object}
     */
    this.getUrlOptions = function (version) {
        let options = {
            headers: {
                'User-Agent': 'Docker Registry UI'
            }
        };

        if (version === 2) {
            options.headers.Accept = 'application/vnd.docker.distribution.manifest.v2+json';
        }

        return options;
    };

    /**
     * @param   {Integer}  [limit]
     * @returns {Promise}
     */
    this.getImages = function (limit) {
        limit = limit || 300;

        return new Promise((resolve, reject) => {
            rest.get(this._baseurl + '_catalog?n=' + limit, this.getUrlOptions())
                .on('timeout', function (ms) {
                    reject(new Error('Request timed out after ' + ms + 'ms'));
                })
                .on('complete', function (result) {
                    if (result instanceof Error) {
                        reject(result);
                    } else {
                        resolve(result);
                    }
                });
        });
    };

    /**
     * @param   {String}   image
     * @param   {Integer}  [limit]
     * @returns {Promise}
     */
    this.getImageTags = function (image, limit) {
        limit = limit || 300;

        return new Promise((resolve, reject) => {
            rest.get(this._baseurl + image + '/tags/list?n=' + limit, this.getUrlOptions())
                .on('timeout', function (ms) {
                    reject(new Error('Request timed out after ' + ms + 'ms'));
                })
                .on('complete', function (result) {
                    if (result instanceof Error) {
                        reject(result);
                    } else {
                        resolve(result);
                    }
                });
        });
    };

    /**
     * @param   {String}  image
     * @param   {String}  digest
     * @returns {Promise}
     */
    this.deleteImage = function (image, digest) {
        return new Promise((resolve, reject) => {
            rest.del(this._baseurl + image + '/manifests/' + digest, this.getUrlOptions())
                .on('timeout', function (ms) {
                    reject(new Error('Request timed out after ' + ms + 'ms'));
                })
                .on('202', function () {
                    resolve(true);
                })
                .on('404', function () {
                    resolve(false);
                })
                .on('complete', function (result) {
                    if (result instanceof Error) {
                        reject(result);
                    } else {
                        if (typeof result.errors !== 'undefined' && result.errors.length) {
                            let err = result.errors.shift();
                            resolve(err);
                        }
                    }
                });
        });
    };

    /**
     * @param   {String}  image
     * @param   {String}  layer_digest
     * @returns {Promise}
     */
    this.deleteLayer = function (image, layer_digest) {
        return new Promise((resolve, reject) => {
            rest.del(this._baseurl + image + '/blobs/' + layer_digest, this.getUrlOptions())
                .on('timeout', function (ms) {
                    reject(new Error('Request timed out after ' + ms + 'ms'));
                })
                .on('202', function () {
                    resolve(true);
                })
                .on('404', function () {
                    resolve(false);
                })
                .on('complete', function (result) {
                    if (result instanceof Error) {
                        reject(result);
                    } else {
                        if (typeof result.errors !== 'undefined' && result.errors.length) {
                            let err = result.errors.shift();
                            resolve(err);
                        }
                    }
                });
        });
    };

    /**
     * @param   {String}   image
     * @param   {String}   reference     can be a tag or digest
     * @param   {Integer}  [version]     1 or 2, defaults to 1
     * @returns {Promise}
     */
    this.getManifest = function (image, reference, version) {
        version = version || 1;

        return new Promise((resolve, reject) => {
            rest.get(this._baseurl + image + '/manifests/' + reference, this.getUrlOptions(version))
                .on('timeout', function (ms) {
                    reject(new Error('Request timed out after ' + ms + 'ms'));
                })
                .on('complete', function (result, response) {
                    if (result instanceof Error) {
                        reject(result);
                    } else {
                        if (typeof result === 'string') {
                            result = JSON.parse(result);
                        }

                        result.digest = null;
                        if (typeof response.headers['docker-content-digest'] !== 'undefined') {
                            result.digest = response.headers['docker-content-digest'];
                        }

                        resolve(result);
                    }
                });
        });
    };

    return this;
};


================================================
FILE: src/backend/lib/error.js
================================================
'use strict';

const _    = require('lodash');
const util = require('util');

module.exports = {

    ItemNotFoundError: function (id, previous) {
        Error.captureStackTrace(this, this.constructor);
        this.name     = this.constructor.name;
        this.previous = previous;
        this.message  = 'Item Not Found - ' + id;
        this.public   = true;
        this.status   = 404;
    },

    RegistryError: function (code, message, previous) {
        Error.captureStackTrace(this, this.constructor);
        this.name     = this.constructor.name;
        this.previous = previous;
        this.message  = code + ': ' + message;
        this.public   = true;
        this.status   = 500;
    },

    InternalValidationError: function (message, previous) {
        Error.captureStackTrace(this, this.constructor);
        this.name     = this.constructor.name;
        this.previous = previous;
        this.message  = message;
        this.status   = 400;
        this.public   = false;
    },

    ValidationError: function (message, previous) {
        Error.captureStackTrace(this, this.constructor);
        this.name     = this.constructor.name;
        this.previous = previous;
        this.message  = message;
        this.public   = true;
        this.status   = 400;
    }
};

_.forEach(module.exports, function (error) {
    util.inherits(error, Error);
});


================================================
FILE: src/backend/lib/express/cors.js
================================================
'use strict';

const validator = require('../validator');

module.exports = function (req, res, next) {

    if (req.headers.origin) {

        // very relaxed validation....
        validator({
            type: 'string',
            pattern: '^[a-z\\-]+:\\/\\/(?:[\\w\\-\\.]+(:[0-9]+)?/?)?$'
        }, req.headers.origin)
            .then(function () {
                res.set({
                    'Access-Control-Allow-Origin':      req.headers.origin,
                    'Access-Control-Allow-Credentials': true,
                    'Access-Control-Allow-Methods':     'OPTIONS, GET, POST',
                    'Access-Control-Allow-Headers':     'Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit',
                    'Access-Control-Max-Age':           5 * 60,
                    'Access-Control-Expose-Headers':    'X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit'
                });
                next();
            })
            .catch(next);

    } else {
        // No origin
        next();
    }

};


================================================
FILE: src/backend/lib/express/pagination.js
================================================
'use strict';

let _ = require('lodash');

module.exports = function (default_sort, default_offset, default_limit, max_limit) {

    /**
     * This will setup the req query params with filtered data and defaults
     *
     * sort    will be an array of fields and their direction
     * offset  will be an int, defaulting to zero if no other default supplied
     * limit   will be an int, defaulting to 50 if no other default supplied, and limited to the max if that was supplied
     *
     */

    return function (req, res, next) {

        req.query.offset = typeof req.query.limit === 'undefined' ? default_offset || 0 : parseInt(req.query.offset, 10);
        req.query.limit  = typeof req.query.limit === 'undefined' ? default_limit || 50 : parseInt(req.query.limit, 10);

        if (max_limit && req.query.limit > max_limit) {
            req.query.limit = max_limit;
        }

        // Sorting
        let sort       = typeof req.query.sort === 'undefined' ? default_sort : req.query.sort;
        let myRegexp   = /.*\.(asc|desc)$/ig;
        let sort_array = [];

        sort = sort.split(',');
        _.map(sort, function (val) {
            let matches = myRegexp.exec(val);

            if (matches !== null) {
                let dir = matches[1];
                sort_array.push({
                    field: val.substr(0, val.length - (dir.length + 1)),
                    dir: dir.toLowerCase()
                });
            } else {
                sort_array.push({
                    field: val,
                    dir: 'asc'
                });
            }
        });

        // Sort will now be in this format:
        // [
        //    { field: 'field1', dir: 'asc' },
        //    { field: 'field2', dir: 'desc' }
        // ]

        req.query.sort = sort_array;
        next();
    };
};


================================================
FILE: src/backend/lib/helpers.js
================================================
'use strict';

const moment = require('moment');
const _      = require('lodash');

module.exports = {

    /**
     * Takes an expression such as 30d and returns a moment object of that date in future
     *
     * Key      Shorthand
     * ==================
     * years         y
     * quarters      Q
     * months        M
     * weeks         w
     * days          d
     * hours         h
     * minutes       m
     * seconds       s
     * milliseconds  ms
     *
     * @param {String}  expression
     * @returns {Object}
     */
    parseDatePeriod: function (expression) {
        let matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m);
        if (matches) {
            return moment().add(matches[1], matches[2]);
        }

        return null;
    },

    /**
     * This will return an object that has the defaults supplied applied to it
     * if they didn't exist already.
     *
     * @param  {Object} obj
     * @param  {Object} defaults
     * @return {Object}
     */
    applyObjectDefaults: function (obj, defaults) {
        return _.assign({}, defaults, obj);
    },

    /**
     * Returns a random integer between min (included) and max (excluded)
     * Using Math.round() will give you a non-uniform distribution!
     *
     * @param   {Integer} min
     * @param   {Integer} max
     * @returns {Integer}
     */
    getRandomInt: function (min, max) {
        min = Math.ceil(min);
        max = Math.floor(max);
        return Math.floor(Math.random() * (max - min)) + min;
    },

    /**
     * Removes any fields with . joins in them, to avoid table joining select exposure
     * Also makes sure an 'id' field exists
     *
     * @param   {Array} fields
     * @returns {Array}
     */
    sanitizeFields: function (fields) {
        if (fields.indexOf('id') === -1) {
            fields.unshift('id');
        }

        let sanitized = [];
        for (let x = 0; x < fields.length; x++) {
            if (fields[x].indexOf('.') === -1) {
                sanitized.push(fields[x]);
            }
        }

        return sanitized;
    },

    /**
     *
     * @param   {String} input
     * @param   {String} [allowed]
     * @returns {String}
     */
    stripHtml: function (input, allowed) {
        allowed                = (((allowed || '') + '').toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join('');

        let tags               = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
        let commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi;

        return input.replace(commentsAndPhpTags, '').replace(tags, function ($0, $1) {
            return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : '';
        });
    },

    /**
     *
     * @param   {String} text
     * @returns {String}
     */
    stripJiraMarkup: function (text) {
        return text.replace(/(?:^|[^{]{)[^}]+}/gi, "\n");
    },

    /**
     * @param   {String} content
     * @returns {String}
     */
    compactWhitespace: function (content) {
        return content
            .replace(/(\r|\n)+/gim, ' ')
            .replace(/ +/gim, ' ');
    },

    /**
     * @param   {String}  content
     * @param   {Integer} length
     * @returns {String}
     */
    trimString: function (content, length) {
        if (content.length > (length - 3)) {
            //trim the string to the maximum length
            let trimmed = content.substr(0, length - 3);

            //re-trim if we are in the middle of a word
            return trimmed.substr(0, Math.min(trimmed.length, trimmed.lastIndexOf(' '))) + '...';
        }

        return content;
    },

    /**
     * @param   {String}  str
     * @returns {String}
     */
    ucwords: function (str) {
        return (str + '')
            .replace(/^(.)|\s+(.)/g, function ($1) {
                return $1.toUpperCase()
            })
    },

    niceVarName: function (name) {
        return name.replace('_', ' ')
            .replace(/^(.)|\s+(.)/g, function ($1) {
                return $1.toUpperCase();
            });
    }

};


================================================
FILE: src/backend/lib/validator/api.js
================================================
'use strict';

const error  = require('../error');
const path   = require('path');
const parser = require('json-schema-ref-parser');

const ajv = require('ajv')({
    verbose:        true,
    validateSchema: true,
    allErrors:      false,
    format:         'full',  // strict regexes for format checks
    coerceTypes:    true
});

/**
 * @param {Object} schema
 * @param {Object} payload
 * @returns {Promise}
 */
function apiValidator(schema, payload/*, description*/) {
    return new Promise(function Promise_apiValidator(resolve, reject) {
        if (typeof payload === 'undefined') {
            reject(new error.ValidationError('Payload is undefined'));
        }

        let validate = ajv.compile(schema);
        let valid = validate(payload);

        if (valid && !validate.errors) {
            resolve(payload);
        } else {
            let message = ajv.errorsText(validate.errors);

            //console.log(schema);
            //console.log(payload);
            //console.log(validate.errors);

            //var first_error = validate.errors.slice(0, 1).pop();
            let err = new error.ValidationError(message);
            err.debug = [validate.errors, payload];
            reject(err);
        }
    });
}

apiValidator.loadSchemas = parser
    .dereference(path.resolve('src/backend/schema/index.json'))
    .then((schema) => {
        ajv.addSchema(schema);
        return schema;
    });

module.exports = apiValidator;


================================================
FILE: src/backend/lib/validator/index.js
================================================
'use strict';

const _           = require('lodash');
const error       = require('../error');
const definitions = require('../../schema/definitions.json');

RegExp.prototype.toJSON = RegExp.prototype.toString;

const ajv = require('ajv')({
    verbose:     true, //process.env.NODE_ENV === 'development',
    allErrors:   true,
    format:      'full',  // strict regexes for format checks
    coerceTypes: true,
    schemas:     [
        definitions
    ]
});

/**
 *
 * @param {Object} schema
 * @param {Object} payload
 * @returns {Promise}
 */
function validator (schema, payload) {
    return new Promise(function (resolve, reject) {
        if (!payload) {
            reject(new error.InternalValidationError('Payload is falsy'));
        } else {
            try {
                let validate = ajv.compile(schema);

                let valid = validate(payload);
                if (valid && !validate.errors) {
                    resolve(_.cloneDeep(payload));
                } else {
                    console.log('SCHEMA:', schema);
                    console.log('PAYLOAD:', payload);

                    let message = ajv.errorsText(validate.errors);
                    reject(new error.InternalValidationError(message));
                }

            } catch (err) {
                reject(err);
            }

        }

    });

}

module.exports = validator;


================================================
FILE: src/backend/logger.js
================================================
const {Signale} = require('signale');

module.exports = {
    global:   new Signale({scope: 'Global  '}),
    migrate:  new Signale({scope: 'Migrate '}),
    express:  new Signale({scope: 'Express '}),
    registry: new Signale({scope: 'Registry'}),
};


================================================
FILE: src/backend/routes/api/main.js
================================================
'use strict';

const express = require('express');
const pjson   = require('../../../../package.json');

let router = express.Router({
    caseSensitive: true,
    strict:        true,
    mergeParams:   true
});

/**
 * Health Check
 * GET /api
 */
router.get('/', (req, res/*, next*/) => {
    let version = pjson.version.split('-').shift().split('.');

    res.status(200).send({
        status:  'OK',
        version: {
            major:    parseInt(version.shift(), 10),
            minor:    parseInt(version.shift(), 10),
            revision: parseInt(version.shift(), 10)
        },
        config:  {
            REGISTRY_STORAGE_DELETE_ENABLED: process.env.REGISTRY_STORAGE_DELETE_ENABLED && process.env.REGISTRY_STORAGE_DELETE_ENABLED.toLowerCase() === 'true' || parseInt(process.env.REGISTRY_STORAGE_DELETE_ENABLED, 10) === 1,
            REGISTRY_DOMAIN:                 process.env.REGISTRY_DOMAIN || null
        }
    });
});

router.use('/repos', require('./repos'));

module.exports = router;


================================================
FILE: src/backend/routes/api/repos.js
================================================
'use strict';

const express      = require('express');
const validator    = require('../../lib/validator');
const pagination   = require('../../lib/express/pagination');
const internalRepo = require('../../internal/repo');

let router = express.Router({
    caseSensitive: true,
    strict:        true,
    mergeParams:   true
});

/**
 * /api/repos
 */
router
    .route('/')
    .options((req, res) => {
        res.sendStatus(204);
    })

    /**
     * GET /api/repos
     *
     * Retrieve all repos
     */
    .get(pagination('name', 0, 50, 300), (req, res, next) => {
        validator({
            additionalProperties: false,
            properties:           {
                tags: {
                    type: 'boolean'
                }
            }
        }, {
            tags: (typeof req.query.tags !== 'undefined' ? !!req.query.tags : false)
        })
            .then(data => {
                return internalRepo.getAll(data.tags);
            })
            .then(repos => {
                res.status(200)
                    .send(repos);
            })
            .catch(next);
    });

/**
 * Specific repo
 *
 * /api/repos/abc123
 */
router
    .route('/:name([-a-zA-Z0-9/.,_]+)')
    .options((req, res) => {
        res.sendStatus(204);
    })

    /**
     * GET /api/repos/abc123
     *
     * Retrieve a specific repo
     */
    .get((req, res, next) => {
        validator({
            required:             ['name'],
            additionalProperties: false,
            properties:           {
                name: {
                    type:      'string',
                    minLength: 1
                },
                full: {
                    type: 'boolean'
                }
            }
        }, {
            name: req.params.name,
            full: (typeof req.query.full !== 'undefined' ? !!req.query.full : false)
        })
            .then(data => {
                return internalRepo.get(data.name, data.full);
            })
            .then(repo => {
                res.status(200)
                    .send(repo);
            })
            .catch(next);
    })

    /**
     * DELETE /api/repos/abc123
     *
     * Delete a specific image/tag
     */
    .delete((req, res, next) => {
        validator({
            required:             ['name', 'digest'],
            additionalProperties: false,
            properties:           {
                name:   {
                    type:      'string',
                    minLength: 1
                },
                digest: {
                    type:      'string',
                    minLength: 1
                }
            }
        }, {
            name:   req.params.name,
            digest: (typeof req.query.digest !== 'undefined' ? req.query.digest : '')
        })
            .then(data => {
                return internalRepo.delete(data.name, data.digest);
            })
            .then(result => {
                res.status(200)
                    .send(result);
            })
            .catch(next);
    });
module.exports = router;


================================================
FILE: src/backend/routes/main.js
================================================
'use strict';

const express = require('express');
const fs      = require('fs');

const router = express.Router({
    caseSensitive: true,
    strict:        true,
    mergeParams:   true
});

/**
 * GET .*
 */
router.get(/(.*)/, function (req, res, next) {
    req.params.page = req.params['0'];
    if (req.params.page === '/') {
        req.params.page = '/index.html';
    }

    fs.readFile('dist' + req.params.page, 'utf8', function(err, data) {
        if (err) {
            if (req.params.page !== '/index.html') {
                fs.readFile('dist/index.html', 'utf8', function(err2, data) {
                    if (err2) {
                        next(err);
                    } else {
                        res.contentType('text/html').end(data);
                    }
                });
            } else {
                next(err);
            }
        } else {
            res.contentType('text/html').end(data);
        }
    });
});

module.exports = router;


================================================
FILE: src/backend/schema/definitions.json
================================================
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "definitions",
  "definitions": {
    "id": {
      "description": "Unique identifier",
      "example": 123456,
      "readOnly": true,
      "type": "integer",
      "minimum": 1
    },
    "token": {
      "type": "string",
      "minLength": 10
    },
    "expand": {
      "anyOf": [
        {
          "type": "null"
        },
        {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "string"
          }
        }
      ]
    },
    "sort": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": [
          "field",
          "dir"
        ],
        "additionalProperties": false,
        "properties": {
          "field": {
            "type": "string"
          },
          "dir": {
            "type": "string",
            "pattern": "^(asc|desc)$"
          }
        }
      }
    },
    "query": {
      "anyOf": [
        {
          "type": "null"
        },
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        }
      ]
    },
    "criteria": {
      "anyOf": [
        {
          "type": "null"
        },
        {
          "type": "object"
        }
      ]
    },
    "fields": {
      "anyOf": [
        {
          "type": "null"
        },
        {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "string"
          }
        }
      ]
    },
    "omit": {
      "anyOf": [
        {
          "type": "null"
        },
        {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "string"
          }
        }
      ]
    },
    "created_on": {
      "description": "Date and time of creation",
      "format": "date-time",
      "readOnly": true,
      "type": "string"
    },
    "modified_on": {
      "description": "Date and time of last update",
      "format": "date-time",
      "readOnly": true,
      "type": "string"
    },
    "user_id": {
      "description": "User ID",
      "example": 1234,
      "type": "integer",
      "minimum": 1
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 255
    },
    "email": {
      "description": "Email Address",
      "example": "john@example.com",
      "format": "email",
      "type": "string",
      "minLength": 8,
      "maxLength": 100
    },
    "password": {
      "description": "Password",
      "type": "string",
      "minLength": 8,
      "maxLength": 255
    },
    "jira_webhook_data": {
      "type": "object",
      "additionalProperties": true,
      "required": [
        "webhookEvent",
        "timestamp"
      ],
      "properties": {
        "webhookEvent": {
          "type": "string",
          "minLength": 2
        },
        "timestamp": {
          "type": "integer",
          "minimum": 1
        },
        "user": {
          "type": "object"
        },
        "issue": {
          "type": "object"
        }
      }
    },
    "bitbucket_webhook_data": {
      "type": "object",
      "additionalProperties": true,
      "required": [
        "eventKey",
        "date"
      ],
      "properties": {
        "eventKey": {
          "type": "string",
          "minLength": 2
        },
        "date": {
          "type": "string",
          "minimum": 19
        },
        "actor": {
          "type": "object"
        },
        "pullRequest": {
          "type": "object"
        }
      }
    },
    "dockerhub_webhook_data": {
      "type": "object",
      "additionalProperties": true,
      "required": [
        "push_data",
        "repository"
      ],
      "properties": {
        "push_data": {
          "type": "object"
        },
        "repository": {
          "type": "object"
        }
      }
    },
    "zendesk_webhook_data": {
      "type": "object",
      "additionalProperties": true,
      "required": [
        "ticket",
        "current_user"
      ],
      "properties": {
        "ticket": {
          "type": "object"
        },
        "current_user": {
          "type": "object"
        }
      }
    },
    "service_type": {
      "description": "Service Type",
      "example": "slack",
      "type": "string",
      "minLength": 2,
      "maxLength": 30,
      "pattern": "^(slack|jira-webhook|bitbucket-webhook|dockerhub-webhook|zendesk-webhook|jabber)$"
    }
  }
}


================================================
FILE: src/backend/schema/endpoints/rules.json
================================================
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "endpoints/rules",
  "title": "Rules",
  "description": "Endpoints relating to Rules",
  "stability": "stable",
  "type": "object",
  "definitions": {
    "id": {
      "$ref": "../definitions.json#/definitions/id"
    },
    "created_on": {
      "$ref": "../definitions.json#/definitions/created_on"
    },
    "modified_on": {
      "$ref": "../definitions.json#/definitions/modified_on"
    },
    "user_id": {
      "$ref": "../definitions.json#/definitions/user_id"
    },
    "priority_order": {
      "description": "Priority Order",
      "example": 1,
      "type": "integer",
      "minimum": 0
    },
    "in_service_id": {
      "description": "Incoming Service ID",
      "example": 1234,
      "type": "integer",
      "minimum": 1
    },
    "trigger": {
      "description": "Trigger Type",
      "example": "assigned",
      "type": "string",
      "minLength": 2,
      "maxLength": 50
    },
    "extra_conditions": {
      "description": "Extra Incoming Trigger Conditions",
      "example": {
        "project": "BB"
      },
      "type": "object"
    },
    "out_service_id": {
      "description": "Outgoing Service ID",
      "example": 1234,
      "type": "integer",
      "minimum": 1
    },
    "out_template_id": {
      "description": "Outgoing Template ID",
      "example": 1234,
      "type": "integer",
      "minimum": 1
    },
    "out_template_options": {
      "description": "Custom options for Outgoing Template",
      "example": {
        "panel_color": "#ff00aa"
      },
      "type": "object"
    },
    "fired_count": {
      "description": "Fired Count",
      "example": 854,
      "readOnly": true,
      "type": "integer",
      "minimum": 1
    }
  },
  "links": [
    {
      "title": "List",
      "description": "Returns a list of Rules",
      "href": "/rules",
      "access": "private",
      "method": "GET",
      "rel": "self",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "targetSchema": {
        "type": "array",
        "items": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Create",
      "description": "Creates a new Rule",
      "href": "/rules",
      "access": "private",
      "method": "POST",
      "rel": "create",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "required": [
          "in_service_id",
          "trigger",
          "out_service_id",
          "out_template_id"
        ],
        "properties": {
          "user_id": {
            "$ref": "#/definitions/user_id"
          },
          "priority_order": {
            "$ref": "#/definitions/priority_order"
          },
          "in_service_id": {
            "$ref": "#/definitions/in_service_id"
          },
          "trigger": {
            "$ref": "#/definitions/trigger"
          },
          "extra_conditions": {
            "$ref": "#/definitions/extra_conditions"
          },
          "out_service_id": {
            "$ref": "#/definitions/out_service_id"
          },
          "out_template_id": {
            "$ref": "#/definitions/out_template_id"
          },
          "out_template_options": {
            "$ref": "#/definitions/out_template_options"
          }
        }
      },
      "targetSchema": {
        "properties": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Update",
      "description": "Updates a existing Rule",
      "href": "/rules/{definitions.identity.example}",
      "access": "private",
      "method": "PUT",
      "rel": "update",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "properties": {
          "priority_order": {
            "$ref": "#/definitions/priority_order"
          },
          "in_service_id": {
            "$ref": "#/definitions/in_service_id"
          },
          "trigger": {
            "$ref": "#/definitions/trigger"
          },
          "extra_conditions": {
            "$ref": "#/definitions/extra_conditions"
          },
          "out_service_id": {
            "$ref": "#/definitions/out_service_id"
          },
          "out_template_id": {
            "$ref": "#/definitions/out_template_id"
          },
          "out_template_options": {
            "$ref": "#/definitions/out_template_options"
          }
        }
      },
      "targetSchema": {
        "properties": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Delete",
      "description": "Deletes a existing Rule",
      "href": "/rules/{definitions.identity.example}",
      "access": "private",
      "method": "DELETE",
      "rel": "delete",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "targetSchema": {
        "type": "boolean"
      }
    },
    {
      "title": "Order",
      "description": "Sets the order for the rules",
      "href": "/rules/order",
      "access": "private",
      "method": "POST",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "array",
        "items": {
          "type": "object",
          "required": [
            "order",
            "rule_id"
          ],
          "properties": {
            "order": {
              "type": "integer",
              "minimum": 0
            },
            "rule_id": {
              "$ref": "../definitions.json#/definitions/id"
            }
          }
        }
      },
      "targetSchema": {
        "type": "boolean"
      }
    },
    {
      "title": "Copy",
      "description": "Copies rules from one user to another",
      "href": "/rules/copy",
      "access": "private",
      "method": "POST",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "required": [
          "from",
          "to"
        ],
        "properties": {
          "from": {
            "type": "integer",
            "minimum": 1
          },
          "to": {
            "type": "integer",
            "minimum": 1
          },
          "service_type": {
            "$ref": "../definitions.json#/definitions/service_type"
          }
        }
      },
      "targetSchema": {
        "type": "boolean"
      }
    }
  ],
  "properties": {
    "id": {
      "$ref": "#/definitions/id"
    },
    "created_on": {
      "$ref": "#/definitions/created_on"
    },
    "modified_on": {
      "$ref": "#/definitions/modified_on"
    },
    "user_id": {
      "$ref": "#/definitions/user_id"
    },
    "priority_order": {
      "$ref": "#/definitions/priority_order"
    },
    "in_service_id": {
      "$ref": "#/definitions/in_service_id"
    },
    "trigger": {
      "$ref": "#/definitions/trigger"
    },
    "extra_conditions": {
      "$ref": "#/definitions/extra_conditions"
    },
    "out_service_id": {
      "$ref": "#/definitions/out_service_id"
    },
    "out_template_id": {
      "$ref": "#/definitions/out_template_id"
    },
    "out_template_options": {
      "$ref": "#/definitions/out_template_options"
    },
    "fired_count": {
      "$ref": "#/definitions/fired_count"
    }
  }
}


================================================
FILE: src/backend/schema/endpoints/services.json
================================================
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "endpoints/services",
  "title": "Services",
  "description": "Endpoints relating to Services",
  "stability": "stable",
  "type": "object",
  "definitions": {
    "id": {
      "$ref": "../definitions.json#/definitions/id"
    },
    "created_on": {
      "$ref": "../definitions.json#/definitions/created_on"
    },
    "modified_on": {
      "$ref": "../definitions.json#/definitions/modified_on"
    },
    "type": {
      "$ref": "../definitions.json#/definitions/service_type"
    },
    "name": {
      "description": "Name",
      "example": "JiraBot",
      "type": "string",
      "minLength": 2,
      "maxLength": 100
    },
    "data": {
      "description": "Data",
      "example": {"api_token": "xox-somethingrandom"},
      "type": "object"
    }
  },
  "links": [
    {
      "title": "List",
      "description": "Returns a list of Services",
      "href": "/services",
      "access": "private",
      "method": "GET",
      "rel": "self",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "targetSchema": {
        "type": "array",
        "items": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Create",
      "description": "Creates a new Service",
      "href": "/services",
      "access": "private",
      "method": "POST",
      "rel": "create",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "required": [
          "type",
          "name",
          "data"
        ],
        "properties": {
          "type": {
            "$ref": "#/definitions/type"
          },
          "name": {
            "$ref": "#/definitions/name"
          },
          "data": {
            "$ref": "#/definitions/data"
          }
        }
      },
      "targetSchema": {
        "properties": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Update",
      "description": "Updates a existing Service",
      "href": "/services/{definitions.identity.example}",
      "access": "private",
      "method": "PUT",
      "rel": "update",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "properties": {
          "type": {
            "$ref": "#/definitions/type"
          },
          "name": {
            "$ref": "#/definitions/name"
          },
          "data": {
            "$ref": "#/definitions/data"
          }
        }
      },
      "targetSchema": {
        "properties": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Delete",
      "description": "Deletes a existing Service",
      "href": "/services/{definitions.identity.example}",
      "access": "private",
      "method": "DELETE",
      "rel": "delete",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "targetSchema": {
        "type": "boolean"
      }
    },
    {
      "title": "Test",
      "description": "Tests a existing Service",
      "href": "/services/{definitions.identity.example}/test",
      "access": "private",
      "method": "POST",
      "rel": "test",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "required": [
          "username",
          "message"
        ],
        "properties": {
          "username": {
            "type": "string",
            "minLength": 1
          },
          "message": {
            "type": "string",
            "minLength": 1
          }
        }
      },
      "targetSchema": {
        "type": "boolean"
      }
    },
    {
      "title": "User List",
      "description": "Get User List of a Service",
      "href": "/services/{definitions.identity.example}/users",
      "access": "private",
      "method": "GET",
      "rel": "users",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "required": [
          "username",
          "message"
        ],
        "properties": {
          "username": {
            "type": "string",
            "minLength": 1
          },
          "message": {
            "type": "string",
            "minLength": 1
          }
        }
      },
      "targetSchema": {
        "type": "boolean"
      }
    }
  ],
  "properties": {
    "id": {
      "$ref": "#/definitions/id"
    },
    "created_on": {
      "$ref": "#/definitions/created_on"
    },
    "modified_on": {
      "$ref": "#/definitions/modified_on"
    },
    "type": {
      "$ref": "#/definitions/type"
    },
    "name": {
      "$ref": "#/definitions/name"
    },
    "data": {
      "$ref": "#/definitions/data"
    }
  }
}


================================================
FILE: src/backend/schema/endpoints/templates.json
================================================
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "endpoints/templates",
  "title": "Templates",
  "description": "Endpoints relating to Templates",
  "stability": "stable",
  "type": "object",
  "definitions": {
    "id": {
      "$ref": "../definitions.json#/definitions/id"
    },
    "created_on": {
      "$ref": "../definitions.json#/definitions/created_on"
    },
    "modified_on": {
      "$ref": "../definitions.json#/definitions/modified_on"
    },
    "service_type": {
      "$ref": "../definitions.json#/definitions/service_type"
    },
    "in_service_type": {
      "$ref": "../definitions.json#/definitions/service_type"
    },
    "name": {
      "description": "Name of Template",
      "example": "Assigned Task Compact",
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "content": {
      "description": "Content",
      "example": "{\"text\": \"Hello World\"}",
      "type": "string"
    },
    "default_options": {
      "description": "Default Options",
      "example": {
        "panel_color": "#ff0000"
      },
      "type": "object"
    },
    "example_data": {
      "description": "Example Data",
      "example": {
        "summary": "Example Jira Summary"
      },
      "type": "object"
    },
    "event_types": {
      "description": "Event Types",
      "example": {
        "summary": ["assigned", "resolved"]
      },
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "render_engine": {
      "description": "Render Engine",
      "example": "liquid",
      "type": "string",
      "pattern": "^(ejs|liquid)$"
    }
  },
  "links": [
    {
      "title": "List",
      "description": "Returns a list of Templates",
      "href": "/templates",
      "access": "private",
      "method": "GET",
      "rel": "self",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "targetSchema": {
        "type": "array",
        "items": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Create",
      "description": "Creates a new Templates",
      "href": "/templates",
      "access": "private",
      "method": "POST",
      "rel": "create",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "required": [
          "service_type",
          "in_service_type",
          "name",
          "content",
          "default_options",
          "example_data",
          "event_types"
        ],
        "properties": {
          "service_type": {
            "$ref": "#/definitions/service_type"
          },
          "in_service_type": {
            "$ref": "#/definitions/in_service_type"
          },
          "name": {
            "$ref": "#/definitions/name"
          },
          "content": {
            "$ref": "#/definitions/content"
          },
          "default_options": {
            "$ref": "#/definitions/default_options"
          },
          "example_data": {
            "$ref": "#/definitions/default_options"
          },
          "event_types": {
            "$ref": "#/definitions/event_types"
          }
        }
      },
      "targetSchema": {
        "properties": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Update",
      "description": "Updates a existing Template",
      "href": "/templates/{definitions.identity.example}",
      "access": "private",
      "method": "PUT",
      "rel": "update",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "properties": {
          "service_type": {
            "$ref": "#/definitions/service_type"
          },
          "in_service_type": {
            "$ref": "#/definitions/in_service_type"
          },
          "name": {
            "$ref": "#/definitions/name"
          },
          "content": {
            "$ref": "#/definitions/content"
          },
          "default_options": {
            "$ref": "#/definitions/default_options"
          },
          "example_data": {
            "$ref": "#/definitions/default_options"
          },
          "event_types": {
            "$ref": "#/definitions/event_types"
          }
        }
      },
      "targetSchema": {
        "properties": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Delete",
      "description": "Deletes a existing Template",
      "href": "/templates/{definitions.identity.example}",
      "access": "private",
      "method": "DELETE",
      "rel": "delete",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "targetSchema": {
        "type": "boolean"
      }
    }
  ],
  "properties": {
    "id": {
      "$ref": "#/definitions/id"
    },
    "created_on": {
      "$ref": "#/definitions/created_on"
    },
    "modified_on": {
      "$ref": "#/definitions/modified_on"
    },
    "service_type": {
      "$ref": "#/definitions/service_type"
    },
    "in_service_type": {
      "$ref": "#/definitions/in_service_type"
    },
    "name": {
      "$ref": "#/definitions/name"
    },
    "content": {
      "$ref": "#/definitions/content"
    },
    "default_options": {
      "$ref": "#/definitions/default_options"
    },
    "example_data": {
      "$ref": "#/definitions/example_data"
    },
    "event_types": {
      "$ref": "#/definitions/event_types"
    }
  }
}


================================================
FILE: src/backend/schema/endpoints/tokens.json
================================================
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "endpoints/tokens",
  "title": "Token",
  "description": "Tokens are required to authenticate against the JiraBot API",
  "stability": "stable",
  "type": "object",
  "definitions": {
    "identity": {
      "description": "Email Address or other 3rd party providers identifier",
      "example": "john@example.com",
      "type": "string"
    },
    "secret": {
      "description": "A password or key",
      "example": "correct horse battery staple",
      "type": "string"
    },
    "token": {
      "description": "JWT",
      "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk",
      "type": "string"
    },
    "expires": {
      "description": "Token expiry time",
      "format": "date-time",
      "type": "string"
    },
    "scope": {
      "description": "Scope of the Token, defaults to 'user'",
      "example": "user",
      "type": "string"
    }
  },
  "links": [
    {
      "title": "Create",
      "description": "Creates a new token.",
      "href": "/tokens",
      "access": "public",
      "method": "POST",
      "rel": "create",
      "schema": {
        "type": "object",
        "required": [
          "identity",
          "secret"
        ],
        "properties": {
          "identity": {
            "$ref": "#/definitions/identity"
          },
          "secret": {
            "$ref": "#/definitions/secret"
          },
          "scope": {
            "$ref": "#/definitions/scope"
          }
        }
      },
      "targetSchema": {
        "type": "object",
        "properties": {
          "token": {
            "$ref": "#/definitions/token"
          },
          "expires": {
            "$ref": "#/definitions/expires"
          }
        }
      }
    },
    {
      "title": "Refresh",
      "description": "Returns a new token.",
      "href": "/tokens",
      "access": "private",
      "method": "GET",
      "rel": "self",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {},
      "targetSchema": {
        "type": "object",
        "properties": {
          "token": {
            "$ref": "#/definitions/token"
          },
          "expires": {
            "$ref": "#/definitions/expires"
          },
          "scope": {
            "$ref": "#/definitions/scope"
          }
        }
      }
    }
  ]
}


================================================
FILE: src/backend/schema/endpoints/users.json
================================================
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "endpoints/users",
  "title": "Users",
  "description": "Endpoints relating to Users",
  "stability": "stable",
  "type": "object",
  "definitions": {
    "id": {
      "$ref": "../definitions.json#/definitions/id"
    },
    "created_on": {
      "$ref": "../definitions.json#/definitions/created_on"
    },
    "modified_on": {
      "$ref": "../definitions.json#/definitions/modified_on"
    },
    "name": {
      "description": "Name",
      "example": "Jamie Curnow",
      "type": "string",
      "minLength": 2,
      "maxLength": 100
    },
    "nickname": {
      "description": "Nickname",
      "example": "Jamie",
      "type": "string",
      "minLength": 2,
      "maxLength": 50
    },
    "email": {
      "$ref": "../definitions.json#/definitions/email"
    },
    "avatar": {
      "description": "Avatar",
      "example": "http://somewhere.jpg",
      "type": "string",
      "minLength": 2,
      "maxLength": 150,
      "readOnly": true
    },
    "roles": {
      "description": "Roles",
      "example": [
        "admin"
      ],
      "type": "array"
    },
    "is_disabled": {
      "description": "Is Disabled",
      "example": false,
      "type": "boolean"
    }
  },
  "links": [
    {
      "title": "List",
      "description": "Returns a list of Users",
      "href": "/users",
      "access": "private",
      "method": "GET",
      "rel": "self",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "targetSchema": {
        "type": "array",
        "items": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Create",
      "description": "Creates a new User",
      "href": "/users",
      "access": "private",
      "method": "POST",
      "rel": "create",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "required": [
          "name",
          "nickname",
          "email"
        ],
        "properties": {
          "name": {
            "$ref": "#/definitions/name"
          },
          "nickname": {
            "$ref": "#/definitions/nickname"
          },
          "email": {
            "$ref": "#/definitions/email"
          },
          "roles": {
            "$ref": "#/definitions/roles"
          },
          "is_disabled": {
            "$ref": "#/definitions/is_disabled"
          },
          "auth": {
            "type": "object",
            "description": "Auth Credentials",
            "example": {
              "type": "password",
              "secret": "bigredhorsebanana"
            }
          }
        }
      },
      "targetSchema": {
        "properties": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Update",
      "description": "Updates a existing User",
      "href": "/users/{definitions.identity.example}",
      "access": "private",
      "method": "PUT",
      "rel": "update",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "properties": {
          "name": {
            "$ref": "#/definitions/name"
          },
          "nickname": {
            "$ref": "#/definitions/nickname"
          },
          "email": {
            "$ref": "#/definitions/email"
          },
          "roles": {
            "$ref": "#/definitions/roles"
          },
          "is_disabled": {
            "$ref": "#/definitions/is_disabled"
          }
        }
      },
      "targetSchema": {
        "properties": {
          "$ref": "#/properties"
        }
      }
    },
    {
      "title": "Delete",
      "description": "Deletes a existing User",
      "href": "/users/{definitions.identity.example}",
      "access": "private",
      "method": "DELETE",
      "rel": "delete",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "targetSchema": {
        "type": "boolean"
      }
    },
    {
      "title": "Set Password",
      "description": "Sets a password for an existing User",
      "href": "/users/{definitions.identity.example}/auth",
      "access": "private",
      "method": "PUT",
      "rel": "update",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "required": [
          "type",
          "secret"
        ],
        "properties": {
          "type": {
            "type": "string",
            "pattern": "^password$"
          },
          "current": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "secret": {
            "type": "string",
            "minLength": 8,
            "maxLength": 64
          }
        }
      },
      "targetSchema": {
        "type": "boolean"
      }
    },
    {
      "title": "Set Service Settings",
      "description": "Sets service settings for an existing User",
      "href": "/users/{definitions.identity.example}/services",
      "access": "private",
      "method": "POST",
      "rel": "update",
      "http_header": {
        "$ref": "../examples.json#/definitions/auth_header"
      },
      "schema": {
        "type": "object",
        "required": [
          "settings"
        ],
        "properties": {
          "settings": {
            "type": "object"
          }
        }
      },
      "targetSchema": {
        "type": "boolean"
      }
    }
  ],
  "properties": {
    "id": {
      "$ref": "#/definitions/id"
    },
    "created_on": {
      "$ref": "#/definitions/created_on"
    },
    "modified_on": {
      "$ref": "#/definitions/modified_on"
    },
    "name": {
      "$ref": "#/definitions/name"
    },
    "nickname": {
      "$ref": "#/definitions/nickname"
    },
    "email": {
      "$ref": "#/definitions/email"
    },
    "avatar": {
      "$ref": "#/definitions/avatar"
    },
    "roles": {
      "$ref": "#/definitions/roles"
    },
    "is_disabled": {
      "$ref": "#/definitions/is_disabled"
    }
  }
}


================================================
FILE: src/backend/schema/examples.json
================================================
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "examples",
  "type": "object",
  "definitions": {
    "name": {
      "description": "Name",
      "example": "John Smith",
      "type": "string",
      "minLength": 1,
      "maxLength": 255
    },
    "auth_header": {
      "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk",
      "X-API-Version": "next"
    },
    "token": {
      "type": "string",
      "description": "JWT",
      "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk"
    }
  }
}


================================================
FILE: src/backend/schema/index.json
================================================
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Juxtapose REST API",
  "description": "This is the Juxtapose REST API",
  "$id": "root",
  "version": "1.0.0",
  "links": [
    {
      "href": "http://juxtapose/api",
      "rel": "self"
    }
  ],
  "properties": {
    "tokens": {
      "$ref": "endpoints/tokens.json"
    },
    "users": {
      "$ref": "endpoints/users.json"
    },
    "services": {
      "$ref": "endpoints/services.json"
    },
    "templates": {
      "$ref": "endpoints/templates.json"
    },
    "rules": {
      "$ref": "endpoints/rules.json"
    }
  }
}


================================================
FILE: src/frontend/app-images/favicons/browserconfig.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
    <msapplication>
        <tile>
            <square150x150logo src="/images/favicons/mstile-150x150.png"/>
            <TileColor>#f5f5f5</TileColor>
        </tile>
    </msapplication>
</browserconfig>


================================================
FILE: src/frontend/app-images/favicons/site.webmanifest
================================================
{
    "name": "",
    "short_name": "",
    "icons": [
        {
            "src": "/images/favicons/android-chrome-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "/images/favicons/android-chrome-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ],
    "theme_color": "#ffffff",
    "background_color": "#ffffff",
    "display": "standalone"
}


================================================
FILE: src/frontend/html/index.html
================================================
<!doctype html>
<html lang="en" dir="ltr">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <meta http-equiv="Content-Language" content="en">
        <meta name="msapplication-TileColor" content="#2d89ef">
        <meta name="theme-color" content="#4188c9">
        <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
        <meta name="apple-mobile-web-app-capable" content="yes">
        <meta name="mobile-web-app-capable" content="yes">
        <meta name="HandheldFriendly" content="True">
        <meta name="MobileOptimized" content="320">
        <title>Docker Registry UI</title>
        <link rel="apple-touch-icon" sizes="180x180" href="/images/favicons/apple-touch-icon.png">
        <link rel="icon" type="image/png" sizes="32x32" href="/images/favicons/favicon-32x32.png">
        <link rel="icon" type="image/png" sizes="16x16" href="/images/favicons/favicon-16x16.png">
        <link rel="manifest" href="/images/favicons/site.webmanifest">
        <link rel="mask-icon" href="/images/favicons/safari-pinned-tab.svg" color="#5bbad5">
        <link rel="shortcut icon" href="/images/favicons/favicon.ico">
        <meta name="msapplication-TileColor" content="#f5f5f5">
        <meta name="msapplication-config" content="/images/favicons/browserconfig.xml">
        <meta name="theme-color" content="#ffffff">
        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,500,500i,600,600i,700,700i&amp;subset=latin-ext">
    </head>
    <body>
        <div class="page">
            <div class="page-main">
                <div class="header">
                    <div class="container">
                        <div class="d-flex">
                            <a class="navbar-brand" href="/">
                                <img src="/images/favicons/favicon-32x32.png" border="0"> &nbsp; Docker Registry
                            </a>
                        </div>
                    </div>
                </div>
                <div id="app">
                    <!-- app -->
                    <span class="loader"></span>
                </div>
            </div>
            <footer class="footer">
                <div class="container">
                    <div class="row align-items-center flex-row-reverse">
                        <div class="col-auto ml-auto">
                            <div class="row align-items-center">
                                <div class="col-auto">
                                    <ul class="list-inline list-inline-dots mb-0">
                                        <li class="list-inline-item"><a href="https://github.com/jc21/docker-registry-ui?utm_source=docker-registry-ui">Fork me on Github</a></li>
                                    </ul>
                                </div>
                            </div>
                        </div>
                        <div class="col-12 col-lg-auto mt-3 mt-lg-0 text-center">
                            v<span id="version_number"></span> &copy; 2018 <a href="https://jc21.com?utm_source=docker-registry-ui" target="_blank">jc21.com</a>. Theme by <a href="https://github.com/tabler/tabler?utm_source=docker-registry-ui" target="_blank">Tabler</a>
                        </div>
                    </div>
                </div>
            </footer>
        </div>
    </body>
</html>


================================================
FILE: src/frontend/js/actions.js
================================================
import {location} from 'hyperapp-hash-router';
import Api from './lib/api';
import $ from 'jquery';
import moment from 'moment';

const fetching = {};

const actions = {
    location: location.actions,

    /**
     * @param state
     * @returns {*}
     */
    updateState: state => state,

    /**
     * @returns {Function}
     */
    bootstrap: () => async (state, actions) => {
        try {
            let status = await Api.status();
            $('#version_number').text([status.version.major, status.version.minor, status.version.revision].join('.'));
            let repos = await Api.Repos.getAll(true);

            // Hack to remove any image that has no tags
            let clean_repos = [];
            repos.map(repo => {
                if (typeof repo.tags !== 'undefined' && repo.tags !== null && repo.tags.length) {
                    clean_repos.push(repo);
                }
            });

            actions.updateState({isLoading: false, status: status, repos: clean_repos, globalError: null});
        } catch (err) {
            actions.updateState({isLoading: false, globalError: err});
        }
    },

    /**
     * @returns {Function}
     */
    fetchImage: image_id => async (state, actions) => {
        if (typeof fetching[image_id] === 'undefined' || !fetching[image_id]) {
            fetching[image_id] = true;

            let image_item = {
                err:       null,
                timestamp: parseInt(moment().format('X'), 10),
                data:      null
            };

            try {
                image_item.data = await Api.Repos.get(image_id, true);
            } catch (err) {
                image_item.err = err;
            }

            let new_state              = {images: state.images};
            new_state.images[image_id] = image_item;
            actions.updateState(new_state);
            fetching[image_id] = false;
        }
    },

    deleteImageClicked: e => async (state, actions) => {
        let $btn     = $(e.currentTarget).addClass('btn-loading disabled').prop('disabled', true);
        let $modal   = $btn.parents('.modal').first();
        let image_id = $btn.data('image_id');

        Api.Repos.delete(image_id, $btn.data('digest'))
            .then(result => {
                if (typeof result.code !== 'undefined' && result.code === 'UNSUPPORTED') {
                    throw new Error('Deleting is not enabled on the Registry');
                } else if (result === true) {
                    $modal.modal('hide');

                    let new_state = {
                        isLoaded: true,
                        images:   state.images
                    };

                    delete new_state.images[image_id];

                    setTimeout(function () {
                        actions.updateState(new_state);

                        actions.location.go('/');
                        actions.bootstrap();
                    }, 300);
                } else {
                    throw new Error('Unrecognized response: ' + JSON.stringify(result));
                }
            })
            .catch(err => {
                console.error(err);
                $modal.find('.modal-body').append($('<p>').addClass('text-danger').text(err.message));
                $btn.removeClass('btn-loading disabled').prop('disabled', false);
            });
    }
};

export default actions;


================================================
FILE: src/frontend/js/components/app/image-tag.js
================================================
import {div, h3, p, a} from '@hyperapp/html';
import Utils from '../../lib/utils';

export default (tag, config) => {
    let total_size = 0;
    if (typeof tag.layers !== 'undefined' && tag.layers) {
        tag.layers.map(layer => total_size += layer.size);
        total_size = total_size / 1024 / 1024;
        total_size = total_size.toFixed(0);
    }

    let domain = config.REGISTRY_DOMAIN || window.location.hostname;

    return div({class: 'card tag-card'}, [
        div({class: 'card-header'},
            h3({class: 'card-title'}, tag.name)
        ),
        div({class: 'card-alert alert alert-secondary mb-0 pull-command'},
            'docker pull ' + domain + '/' + tag.image_name + ':' + tag.name
        ),
        div({class: 'card-body'},
            div({class: 'row'}, [
                div({class: 'col-lg-3 col-sm-6'}, [
                    div({class: 'h6'}, 'Image ID'),
                    p(Utils.getShortDigestId(tag.config.digest))
                ]),
                div({class: 'col-lg-3 col-sm-6'}, [
                    div({class: 'h6'}, 'Author'),
                    p(tag.info.author)
                ]),
                div({class: 'col-lg-3 col-sm-6'}, [
                    div({class: 'h6'}, 'Docker Version'),
                    p(tag.info.docker_version)
                ]),
                div({class: 'col-lg-3 col-sm-6'}, [
                    div({class: 'h6'}, 'Size'),
                    p(total_size ? total_size + ' mb' : 'Unknown')
                ])
            ])
        )
    ]);
}


================================================
FILE: src/frontend/js/components/app/insecure-registries.js
================================================
import {div, h3, h4, p, pre, code} from '@hyperapp/html';

export default domain => div({class: 'card'},
    div({class: 'card-header'},
        h3({class: 'card-title'}, 'Insecure Registries')
    ),
    div({class: 'card-body'}, [
        p('If this registry is insecure and doesn\'t hide behind SSL certificates then you will need to configure your Docker client to allow pushing to this insecure registry.'),
        h4('Linux'),
        p('Edit or you may even need to create the following file on your Linux server:'),
        pre(
            code('/etc/docker/daemon.json')
        ),
        p('And save the following content:'),
        pre(
            code(JSON.stringify({'insecure-registries': [domain]}, null, 2))
        ),
        p('You will need to restart your Docker service before these changes will take effect.')
    ])
);


================================================
FILE: src/frontend/js/components/tabler/big-error.js
================================================
import {div, i, h1, p, a} from '@hyperapp/html';

/**
 * @param {Number}  code
 * @param {String}  message
 * @param {*}       [detail]
 * @para, {Boolean} [hide_back_button]
 */
export default (code, message, detail, hide_back_button) =>
    div({class: 'container text-center'}, [
        div({class: 'display-1 text-muted mb-5'}, [
            i({class: 'si si-exclamation'}),
            code
        ]),
        h1({class: 'h2 mb-3'}, message),
        p({class: 'h4 text-muted font-weight-normal mb-7'}, detail),
        hide_back_button ? null : a({class: 'btn btn-primary', href: 'javascript:history.back();'}, [
            i({class: 'fe fe-arrow-left mr-2'}),
            'Go back'
        ])
    ]);


================================================
FILE: src/frontend/js/components/tabler/icon-stat-card.js
================================================
import {div, i, span, h4, small} from '@hyperapp/html';
import Utils from '../../lib/utils';

/**
 * @param {String|Number}  stat_number
 * @param {String}  stat_text
 * @param {String}  icon        without 'fe-' prefix
 * @param {String}  color       ie: 'green' from tabler 'bg-' class names
 */
export default (stat_number, stat_text, icon, color) =>
    div({class: 'card p-3'},
        div({class: 'd-flex align-items-center'}, [
            span({class: 'stamp stamp-md bg-' + color + ' mr-3'},
                i({class: 'fe fe-' + icon})
            ),
            div({},
                h4({class: 'm-0'}, [
                    typeof stat_number === 'number' ? Utils.niceNumber(stat_number) : stat_number,
                    small(' ' + stat_text)
                ])
            )
        ])
    );


================================================
FILE: src/frontend/js/components/tabler/modal.js
================================================
import {div} from '@hyperapp/html';
import $ from 'jquery';

export default (content, onclose) => div({class: 'modal fade', tabindex: '-1', role: 'dialog', ariaHidden: 'true', oncreate: function (elm) {
    let modal = $(elm);
    modal.modal('show');

    if (typeof onclose === 'function') {
        modal.on('hidden.bs.modal', onclose);
    }
}}, content);


================================================
FILE: src/frontend/js/components/tabler/nav.js
================================================
import {div, i, ul, li, a} from '@hyperapp/html';
import {Link} from 'hyperapp-hash-router';

export default (show_delete) => {

    let selected = 'images';
    if (window.location.hash.substr(0, 14) === '#/instructions') {
        selected = 'instructions';
    }

    return div({class: 'header collapse d-lg-flex p-0', id: 'headerMenuCollapse'},
        div({class: 'container'},
            div({class: 'row align-items-center'},
                div({class: 'col-lg order-lg-first'}, [
                    ul({class: 'nav nav-tabs border-0 flex-column flex-lg-row'}, [
                        li({class: 'nav-item'},
                            Link({class: 'nav-link' + (selected === 'images' ? ' active' : ''), to: '/'}, [
                                i({class: 'fe fe-box'}),
                                'Images'
                            ])
                        ),
                        li({class: 'nav-item'}, [
                            a({class: 'nav-link' + (selected === 'instructions' ? ' active' : ''), href: 'javascript:void(0)', 'data-toggle': 'dropdown'}, [
                                i({class: 'fe fe-feather'}),
                                'Instructions'
                            ]),
                            div({class: 'dropdown-menu dropdown-menu-arrow'}, [
                                Link({class: 'dropdown-item', to: '/instructions/pulling'}, 'Pulling'),
                                Link({class: 'dropdown-item', to: '/instructions/pushing'}, 'Pushing'),
                                show_delete ? Link({class: 'dropdown-item', to: '/instructions/deleting'}, 'Deleting') : null
                            ])
                        ])
                    ])
                ])
            )
        )
    );
}


================================================
FILE: src/frontend/js/components/tabler/stat-card.js
================================================
import {div, i} from '@hyperapp/html';
import Utils from '../../lib/utils';

/**
 * @param {String|Number}  big_stat
 * @param {String}  stat_text
 * @param {String}  small_stat
 * @param {Boolean} negative       If truthy, shows as red. Otherwise, green.
 */
export default (big_stat, stat_text, small_stat, negative) =>
    div({class: 'card'},
        div({class: 'card-body p-3 text-center'}, [
            small_stat ? div({class: 'text-right ' + (negative ? 'text-red' : 'text-green')}, [
                small_stat,
                i({class: 'fe ' + (negative ? 'fe-chevron-down' : 'fe-chevron-up')})
            ]) : null,
            div({class: 'h1 m-0'}, typeof big_stat === 'number' ? Utils.niceNumber(big_stat) : big_stat),
            div({class: 'text-muted mb-4'}, stat_text)
        ])
    );


================================================
FILE: src/frontend/js/components/tabler/table-body.js
================================================
import {tbody} from '@hyperapp/html';
import Trow from './table-row';
import _ from 'lodash';

/**
 * @param   {Object}   fields
 * @param   {Array}    rows
 */
export default (fields, rows) => {
    let field_keys = [];

    _.map(fields, (val, key) => {
        field_keys.push(key);
    });

    return tbody(rows.map(row => {
        return Trow(_.pick(row, field_keys), fields);
    }));
}



================================================
FILE: src/frontend/js/components/tabler/table-card.js
================================================
import {div, table} from '@hyperapp/html';
import Thead from './table-head';
import Tbody from './table-body';

/**
 * @param   {Array}    header
 * @param   {Object}   fields
 * @param   {Array}    rows
 */
export default (header, fields, rows) =>
    div({class: 'card'},
        div({class: 'table-responsive'},
            table({class: 'table table-hover table-outline table-vcenter text-nowrap card-table'}, [
                Thead(header),
                Tbody(fields, rows)
            ])
        )
    );


================================================
FILE: src/frontend/js/components/tabler/table-head.js
================================================
import {thead, tr, th} from '@hyperapp/html';
import _ from 'lodash';

/**
 * @param {Array}   header
 */
export default function (header) {
    let cells = [];

    _.map(header, cell => {
        if (typeof cell === 'object' && typeof cell.class !== 'undefined' && cell.class) {
            cells.push(th({class: cell.class}, cell.value));
        } else {
            cells.push(th(cell));
        }
    });

    return thead({},
        tr({}, cells)
    );
};


================================================
FILE: src/frontend/js/components/tabler/table-row.js
================================================
import {tr, td} from '@hyperapp/html';
import _ from 'lodash';

/**
 * @param   {Object}  row
 * @param   {Object}  fields
 */
export default function (row, fields) {
    let cells = [];

    _.map(row, (cell, key) => {
        let manipulator = fields[key].manipulator || null;
        let value = cell;

        if (typeof cell === 'object' && cell !== null && typeof cell.value !== 'undefined') {
            value = cell.value;
        }

        if (typeof manipulator === 'function') {
            value = manipulator(value, cell);
        }

        if (typeof cell.attributes !== 'undefined' && cell.attributes) {
            cells.push(td(cell.attributes, value));
        } else {
            cells.push(td(value));
        }
    });

    return tr(cells);
};


================================================
FILE: src/frontend/js/index.js
================================================
// This has to exist here so that Webpack picks it up
import '../scss/styles.scss';

import $ from 'jquery';
import {app} from 'hyperapp';
import actions from './actions';
import state from './state';
import {location} from 'hyperapp-hash-router';
import router from './router';

global.jQuery = $;
global.$      = $;

window.tabler = {
    colors: {
        'blue':               '#467fcf',
        'blue-darkest':       '#0e1929',
        'blue-darker':        '#1c3353',
        'blue-dark':          '#3866a6',
        'blue-light':         '#7ea5dd',
        'blue-lighter':       '#c8d9f1',
        'blue-lightest':      '#edf2fa',
        'azure':              '#45aaf2',
        'azure-darkest':      '#0e2230',
        'azure-darker':       '#1c4461',
        'azure-dark':         '#3788c2',
        'azure-light':        '#7dc4f6',
        'azure-lighter':      '#c7e6fb',
        'azure-lightest':     '#ecf7fe',
        'indigo':             '#6574cd',
        'indigo-darkest':     '#141729',
        'indigo-darker':      '#282e52',
        'indigo-dark':        '#515da4',
        'indigo-light':       '#939edc',
        'indigo-lighter':     '#d1d5f0',
        'indigo-lightest':    '#f0f1fa',
        'purple':             '#a55eea',
        'purple-darkest':     '#21132f',
        'purple-darker':      '#42265e',
        'purple-dark':        '#844bbb',
        'purple-light':       '#c08ef0',
        'purple-lighter':     '#e4cff9',
        'purple-lightest':    '#f6effd',
        'pink':               '#f66d9b',
        'pink-darkest':       '#31161f',
        'pink-darker':        '#622c3e',
        'pink-dark':          '#c5577c',
        'pink-light':         '#f999b9',
        'pink-lighter':       '#fcd3e1',
        'pink-lightest':      '#fef0f5',
        'red':                '#e74c3c',
        'red-darkest':        '#2e0f0c',
        'red-darker':         '#5c1e18',
        'red-dark':           '#b93d30',
        'red-light':          '#ee8277',
        'red-lighter':        '#f8c9c5',
        'red-lightest':       '#fdedec',
        'orange':             '#fd9644',
        'orange-darkest':     '#331e0e',
        'orange-darker':      '#653c1b',
        'orange-dark':        '#ca7836',
        'orange-light':       '#feb67c',
        'orange-lighter':     '#fee0c7',
        'orange-lightest':    '#fff5ec',
        'yellow':             '#f1c40f',
        'yellow-darkest':     '#302703',
        'yellow-darker':      '#604e06',
        'yellow-dark':        '#c19d0c',
        'yellow-light':       '#f5d657',
        'yellow-lighter':     '#fbedb7',
        'yellow-lightest':    '#fef9e7',
        'lime':               '#7bd235',
        'lime-darkest':       '#192a0b',
        'lime-darker':        '#315415',
        'lime-dark':          '#62a82a',
        'lime-light':         '#a3e072',
        'lime-lighter':       '#d7f2c2',
        'lime-lightest':      '#f2fbeb',
        'green':              '#5eba00',
        'green-darkest':      '#132500',
        'green-darker':       '#264a00',
        'green-dark':         '#4b9500',
        'green-light':        '#8ecf4d',
        'green-lighter':      '#cfeab3',
        'green-lightest':     '#eff8e6',
        'teal':               '#2bcbba',
        'teal-darkest':       '#092925',
        'teal-darker':        '#11514a',
        'teal-dark':          '#22a295',
        'teal-light':         '#6bdbcf',
        'teal-lighter':       '#bfefea',
        'teal-lightest':      '#eafaf8',
        'cyan':               '#17a2b8',
        'cyan-darkest':       '#052025',
        'cyan-darker':        '#09414a',
        'cyan-dark':          '#128293',
        'cyan-light':         '#5dbecd',
        'cyan-lighter':       '#b9e3ea',
        'cyan-lightest':      '#e8f6f8',
        'gray':               '#868e96',
        'gray-darkest':       '#1b1c1e',
        'gray-darker':        '#36393c',
        'gray-light':         '#aab0b6',
        'gray-lighter':       '#dbdde0',
        'gray-lightest':      '#f3f4f5',
        'gray-dark':          '#343a40',
        'gray-dark-darkest':  '#0a0c0d',
        'gray-dark-darker':   '#15171a',
        'gray-dark-dark':     '#2a2e33',
        'gray-dark-light':    '#717579',
        'gray-dark-lighter':  '#c2c4c6',
        'gray-dark-lightest': '#ebebec'
    }
};

import tabler from 'tabler-core';

const main = app(
    state,
    actions,
    router,
    document.getElementById('app')
);

location.subscribe(main.location);

main.bootstrap();
setInterval(main.bootstrap, 30000);


================================================
FILE: src/frontend/js/lib/api.js
================================================
import $ from 'jquery';

/**
 * @param {String}  message
 * @param {*}       debug
 * @param {Integer} [code]
 * @constructor
 */
const ApiError = function (message, debug, code) {
    let temp  = Error.call(this, message);
    temp.name = this.name = 'ApiError';
    this.stack   = temp.stack;
    this.message = temp.message;
    this.debug   = debug;
    this.code    = code;
};

ApiError.prototype = Object.create(Error.prototype, {
    constructor: {
        value:        ApiError,
        writable:     true,
        configurable: true
    }
});

/**
 *
 * @param   {String} verb
 * @param   {String} path
 * @param   {Object} [data]
 * @param   {Object} [options]
 * @returns {Promise}
 */
function fetch (verb, path, data, options) {
    options = options || {};

    return new Promise(function (resolve, reject) {
        let api_url = '/api/';
        let url     = api_url + path;

        $.ajax({
            url:         url,
            data:        typeof data === 'object' ? JSON.stringify(data) : data,
            type:        verb,
            dataType:    'json',
            contentType: 'application/json; charset=UTF-8',
            crossDomain: true,
            timeout:     (options.timeout ? options.timeout : 15000),
            xhrFields:   {
                withCredentials: true
            },

            success: function (data, textStatus, response) {
                let total = response.getResponseHeader('X-Dataset-Total');
                if (total !== null) {
                    resolve({
                        data:       data,
                        pagination: {
                            total:  parseInt(total, 10),
                            offset: parseInt(response.getResponseHeader('X-Dataset-Offset'), 10),
                            limit:  parseInt(response.getResponseHeader('X-Dataset-Limit'), 10)
                        }
                    });
                } else {
                    resolve(response);
                }
            },

            error: function (xhr, status, error_thrown) {
                let code = 400;

                if (typeof xhr.responseJSON !== 'undefined' && typeof xhr.responseJSON.error !== 'undefined' && typeof xhr.responseJSON.error.message !== 'undefined') {
                    error_thrown = xhr.responseJSON.error.message;
                    code         = xhr.responseJSON.error.code || 500;
                }

                reject(new ApiError(error_thrown, xhr.responseText, code));
            }
        });
    });
}

export default {
    status: function () {
        return fetch('get', '');
    },

    Repos: {
        /**
         * @param   {Boolean}  [with_tags]
         * @returns {Promise}
         */
        getAll: function (with_tags) {
            return fetch('get', 'repos' + (with_tags ? '?tags=1' : ''));
        },

        /**
         * @param   {String}  name
         * @param   {Boolean} [full]
         * @returns {Promise}
         */
        get: function (name, full) {
            return fetch('get', 'repos/' + name + (full ? '?full=1' : ''));
        },

        /**
         * @param   {String}  name
         * @param   {String} [digest]
         * @returns {Promise}
         */
        delete: function (name, digest) {
            return fetch('delete', 'repos/' + name + '?digest=' + digest);
        }
    }
};


================================================
FILE: src/frontend/js/lib/manipulators.js
================================================
import {div} from '@hyperapp/html';
import {Link} from 'hyperapp-hash-router';

export default {

    /**
     * @returns {Function}
     */
    imageName: function () {
        return (value, cell) => {
            return Link({to: '/image/' + value}, value);
        }
    },

    /**
     * @param   {String} delimiter
     * @returns {Function}
     */
    joiner: delimiter => (value, cell) => value.join(delimiter)

};


================================================
FILE: src/frontend/js/lib/utils.js
================================================
import numeral from 'numeral';

export default {

    /**
     * @param   {Integer} number
     * @returns {String}
     */
    niceNumber: function (number) {
        return numeral(number).format('0,0');
    },

    /**
     * @param   {String}  digest
     * @returns {String}
     */
    getShortDigestId: function (digest) {
        return digest.replace(/^sha256:(.{12}).*/gim, '$1');
    }
};


================================================
FILE: src/frontend/js/router.js
================================================
import {Route} from 'hyperapp-hash-router';
import {div, span, a, p} from '@hyperapp/html';
import ImagesRoute from './routes/images';
import ImageRoute from './routes/image';
import PushingRoute from './routes/instructions/pushing';
import PullingRoute from './routes/instructions/pulling';
import DeletingRoute from './routes/instructions/deleting';
import BigError from './components/tabler/big-error';

export default (state, actions) => {
    if (state.isLoading) {
        return span({class: 'loader'});
    } else {

        if (state.globalError !== null && state.globalError) {
            return BigError(state.globalError.code || '500', state.globalError.message,
                [
                    p('There may be a problem communicating with the Registry'),
                    a({
                        class: 'btn btn-link', onclick: function () {
                            actions.bootstrap();
                        }
                    }, 'Refresh')
                ],
                true
            );
        } else {
            return div(
                Route({path: '/', render: ImagesRoute(state, actions)}),
                Route({path: '/image/:imageId', render: ImageRoute(state, actions)}),
                Route({path: '/image/:imageDomain/:imageId', render: ImageRoute(state, actions)}),
                Route({path: '/instructions/pushing', render: PushingRoute(state, actions)}),
                Route({path: '/instructions/pulling', render: PullingRoute(state, actions)}),
                Route({path: '/instructions/deleting', render: DeletingRoute(state, actions)})
            );
        }
    }
}


================================================
FILE: src/frontend/js/routes/image.js
================================================
import {div, h1, span, a, h4, button, p} from '@hyperapp/html';
import Nav from '../components/tabler/nav';
import BigError from '../components/tabler/big-error';
import ImageTag from '../components/app/image-tag';
import Modal from '../components/tabler/modal';
import moment from 'moment';

export default (state, actions) => params => {
    let image_id            = params.match.params.imageId;
    let view                = [];
    let delete_enabled      = state.status.config.REGISTRY_STORAGE_DELETE_ENABLED || false;
    let refresh             = false;
    let digest              = null;
    let now                 = parseInt(moment().format('X'), 10);
    let append_delete_model = false;
    let image               = null;

    if (typeof params.match.params.imageDomain !== 'undefined' && params.match.params.imageDomain.length > 0) {
      image_id = [params.match.params.imageDomain, image_id].join('/');
    }

    // if image doesn't exist in state: refresh
    if (typeof state.images[image_id] === 'undefined' || !state.images[image_id]) {
        refresh = true;
    } else {
        image = state.images[image_id];

        // if image does exist, but hasn't been refreshed in < 30 seconds, refresh
        if (image.timestamp < (now - 30)) {
            refresh = true;

            // if image does exist, but has error, show error
        } else if (image.err) {
            view.push(BigError(image.err.code, image.err.message,
                a({
                    class: 'btn btn-link', onclick: function () {
                        actions.fetchImage(image_id);
                    }
                }, 'Refresh')
            ));

            // if image does exist, but has no error and no data, 404
        } else if (!image.data || typeof image.data.tags === 'undefined' || image.data.tags === null || !image.data.tags.length) {
            view.push(BigError(404, image_id + ' does not exist in this Registry',
                a({
                    class: 'btn btn-link', onclick: function () {
                        actions.fetchImage(image_id);
                    }
                }, 'Refresh')
            ));
        } else {
            // Show it
            // This is where shit gets weird. Digest is the same for all tags, but only stored with a tag.
            digest              = image.data.tags[0].digest;
            append_delete_model = delete_enabled && state.confirmDeleteImage === image_id;

            view.push(h1({class: 'page-title mb-5'}, [
                delete_enabled ? a({
                    class: 'btn btn-secondary btn-sm ml-2 pull-right', onclick: function () {
                        actions.updateState({confirmDeleteImage: image_id});
                    }
                }, 'Delete') : null,
                image_id
            ]));
            view.push(div(image.data.tags.map(tag => ImageTag(tag, state.status.config))));
        }
    }

    if (refresh) {
        view.push(span({class: 'loader'}));
        actions.fetchImage(image_id);
    }

    return div(
        Nav(state.status.config.REGISTRY_STORAGE_DELETE_ENABLED),
        div({class: 'my-3 my-md-5'},
            div({class: 'container'}, view)
        ),
        // Delete modal
        append_delete_model ? Modal(
            div({class: 'modal-dialog'},
                div({class: 'modal-content'}, [
                    div({class: 'modal-header text-left'},
                        h4({class: 'modal-title'}, 'Confirm Delete')
                    ),
                    div({class: 'modal-body'},
                        p('Are you sure you want to delete this image and tag' + (image.data.tags.length === 1 ? '' : 's') + '?')
                    ),
                    div({class: 'modal-footer'}, [
                        button({
                            class:           'btn btn-danger',
                            type:            'button',
                            onclick:         actions.deleteImageClicked,
                            'data-image_id': image_id,
                            'data-digest':   digest
                        }, 'Yes I\'m sure'),
                        button({class: 'btn btn-default', type: 'button', 'data-dismiss': 'modal'}, 'Cancel')
                    ])
                ])
            ),
            // onclose function
            function () {
                actions.updateState({confirmDeleteImage: null});
            }) : null
    );
}


================================================
FILE: src/frontend/js/routes/images.js
================================================
import {Link} from 'hyperapp-hash-router';
import {div, h4, p} from '@hyperapp/html';
import Nav from '../components/tabler/nav';
import TableCard from '../components/tabler/table-card';
import Manipulators from '../lib/manipulators';
import {a} from '@hyperapp/html/dist/html';

export default (state, actions) => params => {
    let content = null;

    if (!state.repos || !state.repos.length) {
        // empty
        content = div({class: 'alert alert-success'}, [
            h4('Nothing to see here!'),
            p('There are no images in this Registry yet.'),
            div({class: 'btn-list'},
                Link({class: 'btn btn-success', to: '/instructions/pushing'}, 'How to push an image')
            )
        ]);

    } else {
        content = TableCard([
                'Name',
                'Tags'
            ], {
                name: {manipulator: Manipulators.imageName()},
                tags: {manipulator: Manipulators.joiner(', ')}
            }, state.repos);
    }

    return div(
        Nav(state.status.config.REGISTRY_STORAGE_DELETE_ENABLED),
        div({class: 'my-3 my-md-5'},
            div({class: 'container'}, content)
        ),
        p({class: 'text-center'},
            a({
                class: 'btn btn-link text-faded', onclick: function () {
                    actions.bootstrap();
                }
            }, 'Refresh')
        )
    );
}


================================================
FILE: src/frontend/js/routes/instructions/deleting.js
================================================
import {div, h1, h3, p, pre, code} from '@hyperapp/html';
import Nav from '../../components/tabler/nav';

export default (state, actions) => params => div(
    Nav(state.status.config.REGISTRY_STORAGE_DELETE_ENABLED),
    div({class: 'my-3 my-md-5'},
        div({class: 'container'}, [
            h1({class: 'page-title mb-5'}, 'Deleting from this Registry'),
            div({class: 'card'},
                div({class: 'card-body'},
                    p('Deleting from a Docker Registry is possible, but not very well implemented. For this reason, deletion options were disabled in this Registry UI project by default. However if you still want to be able to delete images from this registry you will need to set a few things up.'),
                )
            ),
            div({class: 'card'}, [
                div({class: 'card-header'},
                    h3({class: 'card-title'}, 'Permit deleting on the Registry')
                ),
                div({class: 'card-body'}, [
                    p('This step is pretty simple and involves adding an environment variable to your Docker Registry Container when you start it up:'),
                    pre(
                        code('docker run -d -p 5000:5000 -e REGISTRY_STORAGE_DELETE_ENABLED=true --name my-registry registry:2')
                    )
                ])
            ]),
            div({class: 'card'}, [
                div({class: 'card-header'},
                    h3({class: 'card-title'}, 'Cleaning up the Registry')
                ),
                div({class: 'card-body'}, [
                    p('When you delete an image from the registry this won\'t actually remove the blob layers as you might expect. For this reason you have to run this command on your docker registry host to perform garbage collection:'),
                    pre(
                        code('docker exec -it my-registry bin/registry garbage-collect /etc/docker/registry/config.yml')
                    ),
                    p('And if you wanted to make a cron job that runs every 30 mins:'),
                    pre(
                        code('0,30 * * * * /bin/docker exec -it my-registry bin/registry garbage-collect /etc/docker/registry/config.yml >> /dev/null 2>&1')
                    )
                ])
            ])
        ])
    )
);



================================================
FILE: src/frontend/js/routes/instructions/pulling.js
================================================
import {div, h1, p, pre, code} from '@hyperapp/html';
import Nav from '../../components/tabler/nav';
import Insecure from '../../components/app/insecure-registries';

export default (state, actions) => params => {
    let domain = state.status.config.REGISTRY_DOMAIN || window.location.hostname;

    return div(
        Nav(state.status.config.REGISTRY_STORAGE_DELETE_ENABLED),
        div({class: 'my-3 my-md-5'},
            div({class: 'container'}, [
                h1({class: 'page-title mb-5'}, 'Pulling from this Registry'),
                div({class: 'card'},
                    div({class: 'card-body'},
                        p('Viewing any Image from the Repositories menu will give you a command in the following format:'),
                        pre(
                            code('docker pull ' + domain + '/<someimage>:<tag>')
                        )
                    )
                ),
                Insecure(domain)
            ])
        )
    );
}


================================================
FILE: src/frontend/js/routes/instructions/pushing.js
================================================
import {div, h1, p, pre, code} from '@hyperapp/html';
import Nav from '../../components/tabler/nav';
import Insecure from '../../components/app/insecure-registries';

export default (state, actions) => params => {
    let domain = state.status.config.REGISTRY_DOMAIN || window.location.hostname;

    return div(
        Nav(state.status.config.REGISTRY_STORAGE_DELETE_ENABLED),
        div({class: 'my-3 my-md-5'},
            div({class: 'container'}, [
                h1({class: 'page-title mb-5'}, 'Pushing to this Registry'),
                div({class: 'card'},
                    div({class: 'card-body'},
                        p('After you pull or build an image:'),
                        pre(
                            code('docker tag <someimage> ' + domain + '/<someimage>:<tag>' + "\n" +
                                'docker push ' + domain + '/<someimage>:<tag>')
                        )
                    )
                ),
                Insecure(domain)
            ])
        )
    );
}


================================================
FILE: src/frontend/js/state.js
================================================
import {location} from 'hyperapp-hash-router';

export default {
    location:           location.state,
    isLoading:          true,
    globalError:        null,
    confirmDeleteImage: null,
    images:             {}
};


================================================
FILE: src/frontend/scss/styles.scss
================================================
@import "~tabler-ui/dist/assets/css/dashboard";

/* Before any JS content is loaded */
#app > .loader, .container > .loader {
    position: absolute;
    left: 49%;
    top: 40%;
    display: block;
}

.tag-card {
    .pull-command {
        font-family: Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    }
}

.pull-right {
    float: right;
}

.text-faded {
    opacity: 0.5;
}


================================================
FILE: webpack.config.js
================================================
const path                 = require('path');
const webpack              = require('webpack');
const HtmlWebPackPlugin    = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const Visualizer           = require('webpack-visualizer-plugin');
const CopyWebpackPlugin    = require('copy-webpack-plugin');

module.exports = {
    entry:        './src/frontend/js/index.js',
    output:       {
        path:       path.resolve(__dirname, 'dist'),
        filename:   'js/main.js',
        publicPath: '/'
    },
    resolve:      {
        alias: {
            'tabler-core':      'tabler-ui/dist/assets/js/core',
            'bootstrap':        'tabler-ui/dist/assets/js/vendors/bootstrap.bundle.min',
            'sparkline':        'tabler-ui/dist/assets/js/vendors/jquery.sparkline.min',
            'selectize':        'tabler-ui/dist/assets/js/vendors/selectize.min',
            'tablesorter':      'tabler-ui/dist/assets/js/vendors/jquery.tablesorter.min',
            'vector-map':       'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-2.0.3.min',
            'vector-map-de':    'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-de-merc',
            'vector-map-world': 'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-world-mill',
            'circle-progress':  'tabler-ui/dist/assets/js/vendors/circle-progress.min'
        }
    },
    module:       {
        rules: [
            // Shims for tabler-ui
            {
                test:   /assets\/js\/core/,
                loader: 'imports-loader?bootstrap'
            },
            {
                test:   /jquery-jvectormap-de-merc/,
                loader: 'imports-loader?vector-map'
            },
            {
                test:   /jquery-jvectormap-world-mill/,
                loader: 'imports-loader?vector-map'
            },

            // other:
            {
                test:    /\.js$/,
                exclude: /node_modules/,
                use:     {
                    loader: 'babel-loader'
                }
            },
            {
                test: /\.html$/,
                use:  [
                    {
                        loader:  'html-loader',
                        options: {
                            minimize: false,
                            hash:     true
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use:  [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'
                ]
            },
            {
                test: /.*tabler.*\.(jpe?g|gif|png|svg|eot|woff|ttf)$/,
                use:  [
                    {
                        loader:  'file-loader',
                        options: {
                            outputPath: 'assets/tabler-ui/'
                        }
                    }
                ]
            }
        ]
    },
    plugins:      [
        new webpack.ProvidePlugin({
            $:      'jquery',
            jQuery: 'jquery'
        }),
        new HtmlWebPackPlugin({
            template: './src/frontend/html/index.html',
            filename: './index.html'
        }),
        new MiniCssExtractPlugin({
            filename:      'css/[name].css',
            chunkFilename: 'css/[id].css'
        }),
        new Visualizer({
            filename: '../webpack_stats.html'
        }),
        new CopyWebpackPlugin([{
            from:    'src/frontend/app-images',
            to:      'images',
            toType:  'dir',
            context: '/app'
        }])
    ]
};
Download .txt
gitextract_kgk3g63p/

├── .babelrc
├── .gitignore
├── Dockerfile
├── Jenkinsfile
├── LICENCE
├── README.md
├── bin/
│   ├── build
│   ├── build-dev
│   ├── npm
│   ├── watch
│   └── yarn
├── doc/
│   └── full-stack/
│       └── docker-compose.yml
├── docker-compose.yml
├── nodemon.json
├── package.json
├── src/
│   ├── backend/
│   │   ├── app.js
│   │   ├── index.js
│   │   ├── internal/
│   │   │   └── repo.js
│   │   ├── lib/
│   │   │   ├── docker-registry.js
│   │   │   ├── error.js
│   │   │   ├── express/
│   │   │   │   ├── cors.js
│   │   │   │   └── pagination.js
│   │   │   ├── helpers.js
│   │   │   └── validator/
│   │   │       ├── api.js
│   │   │       └── index.js
│   │   ├── logger.js
│   │   ├── routes/
│   │   │   ├── api/
│   │   │   │   ├── main.js
│   │   │   │   └── repos.js
│   │   │   └── main.js
│   │   └── schema/
│   │       ├── definitions.json
│   │       ├── endpoints/
│   │       │   ├── rules.json
│   │       │   ├── services.json
│   │       │   ├── templates.json
│   │       │   ├── tokens.json
│   │       │   └── users.json
│   │       ├── examples.json
│   │       └── index.json
│   └── frontend/
│       ├── app-images/
│       │   └── favicons/
│       │       ├── browserconfig.xml
│       │       └── site.webmanifest
│       ├── html/
│       │   └── index.html
│       ├── js/
│       │   ├── actions.js
│       │   ├── components/
│       │   │   ├── app/
│       │   │   │   ├── image-tag.js
│       │   │   │   └── insecure-registries.js
│       │   │   └── tabler/
│       │   │       ├── big-error.js
│       │   │       ├── icon-stat-card.js
│       │   │       ├── modal.js
│       │   │       ├── nav.js
│       │   │       ├── stat-card.js
│       │   │       ├── table-body.js
│       │   │       ├── table-card.js
│       │   │       ├── table-head.js
│       │   │       └── table-row.js
│       │   ├── index.js
│       │   ├── lib/
│       │   │   ├── api.js
│       │   │   ├── manipulators.js
│       │   │   └── utils.js
│       │   ├── router.js
│       │   ├── routes/
│       │   │   ├── image.js
│       │   │   ├── images.js
│       │   │   └── instructions/
│       │   │       ├── deleting.js
│       │   │       ├── pulling.js
│       │   │       └── pushing.js
│       │   └── state.js
│       └── scss/
│           └── styles.scss
└── webpack.config.js
Download .txt
SYMBOL INDEX (8 symbols across 5 files)

FILE: src/backend/index.js
  function appStart (line 19) | function appStart () {

FILE: src/backend/internal/repo.js
  constant REGISTRY_HOST (line 3) | const REGISTRY_HOST = process.env.REGISTRY_HOST;
  constant REGISTRY_SSL (line 4) | const REGISTRY_SSL  = process.env.REGISTRY_SSL && process.env.REGISTRY_S...
  constant REGISTRY_USER (line 5) | const REGISTRY_USER = process.env.REGISTRY_USER;
  constant REGISTRY_PASS (line 6) | const REGISTRY_PASS = process.env.REGISTRY_PASS;

FILE: src/backend/lib/validator/api.js
  function apiValidator (line 20) | function apiValidator(schema, payload/*, description*/) {

FILE: src/backend/lib/validator/index.js
  function validator (line 25) | function validator (schema, payload) {

FILE: src/frontend/js/lib/api.js
  function fetch (line 34) | function fetch (verb, path, data, options) {
Condensed preview — 65 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (135K chars).
[
  {
    "path": ".babelrc",
    "chars": 184,
    "preview": "{\n  \"presets\": [\n    [\"env\", {\n      \"targets\": {\n        \"browsers\": [\"Chrome >= 65\"]\n      },\n      \"debug\": false,\n  "
  },
  {
    "path": ".gitignore",
    "chars": 122,
    "preview": ".idea\n._*\n.DS_Store\nnode_modules\ndist/*\npackage-lock.json\nyarn-error.log\nyarn.lock\nwebpack_stats.html\ntmp/*\n.env\n.yarnrc"
  },
  {
    "path": "Dockerfile",
    "chars": 587,
    "preview": "FROM jc21/node:latest\n\nMAINTAINER Jamie Curnow <jc@jc21.com>\nLABEL maintainer=\"Jamie Curnow <jc@jc21.com>\"\n\nRUN apt-get "
  },
  {
    "path": "Jenkinsfile",
    "chars": 3749,
    "preview": "pipeline {\n\tagent any\n\toptions {\n\t\tbuildDiscarder(logRotator(numToKeepStr: '10'))\n\t\tdisableConcurrentBuilds()\n\t}\n\tenviro"
  },
  {
    "path": "LICENCE",
    "chars": 1119,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2017 Jamie Curnow, Brisbane Australia (https://jc21.com)\n\nPermission is hereby gran"
  },
  {
    "path": "README.md",
    "chars": 4789,
    "preview": "![Docker Registry UI](https://public.jc21.com/docker-registry-ui/github.png \"Docker Registry UI\")\n\n# Docker Registry UI\n"
  },
  {
    "path": "bin/build",
    "chars": 100,
    "preview": "#!/bin/bash\n\nsudo /usr/local/bin/docker-compose run --no-deps --rm app npm run-script build\nexit $?\n"
  },
  {
    "path": "bin/build-dev",
    "chars": 98,
    "preview": "#!/bin/bash\n\nsudo /usr/local/bin/docker-compose run --no-deps --rm app npm run-script dev\nexit $?\n"
  },
  {
    "path": "bin/npm",
    "chars": 86,
    "preview": "#!/bin/bash\n\nsudo /usr/local/bin/docker-compose run --no-deps --rm app npm $@\nexit $?\n"
  },
  {
    "path": "bin/watch",
    "chars": 137,
    "preview": "#!/bin/bash\n\nsudo docker run --rm -it \\\n  -p 8124:8080 \\\n  -v $(pwd):/app \\\n  -w /app \\\n  jc21/node:latest npm run-scrip"
  },
  {
    "path": "bin/yarn",
    "chars": 87,
    "preview": "#!/bin/bash\n\nsudo /usr/local/bin/docker-compose run --no-deps --rm app yarn $@\nexit $?\n"
  },
  {
    "path": "doc/full-stack/docker-compose.yml",
    "chars": 664,
    "preview": "version: \"2\"\nservices:\n  registry:\n    image: registry:2\n    environment:\n      - REGISTRY_HTTP_SECRET=o43g2kjgn2iuhv2k4"
  },
  {
    "path": "docker-compose.yml",
    "chars": 568,
    "preview": "version: \"2\"\nservices:\n  app:\n    image: jc21/node:latest\n    ports:\n      - 4000:80\n    environment:\n      - DEBUG=\n   "
  },
  {
    "path": "nodemon.json",
    "chars": 99,
    "preview": "{\n    \"verbose\": false,\n    \"ignore\": [\"dist\", \"data\", \"src/frontend\"],\n    \"ext\": \"js json ejs\"\n}\n"
  },
  {
    "path": "package.json",
    "chars": 2220,
    "preview": "{\n    \"name\": \"docker-registry-ui\",\n    \"version\": \"2.0.2\",\n    \"description\": \"A nice web interface for managing your D"
  },
  {
    "path": "src/backend/app.js",
    "chars": 2382,
    "preview": "'use strict';\n\nconst express     = require('express');\nconst bodyParser  = require('body-parser');\nconst compression = r"
  },
  {
    "path": "src/backend/index.js",
    "chars": 1290,
    "preview": "#!/usr/bin/env node\n\n'use strict';\n\nconst logger = require('./logger').global;\nconst config = require('config');\n\nlet po"
  },
  {
    "path": "src/backend/internal/repo.js",
    "chars": 8674,
    "preview": "'use strict';\n\nconst REGISTRY_HOST = process.env.REGISTRY_HOST;\nconst REGISTRY_SSL  = process.env.REGISTRY_SSL && proces"
  },
  {
    "path": "src/backend/lib/docker-registry.js",
    "chars": 5793,
    "preview": "'use strict';\n\nconst _    = require('lodash');\nconst rest = require('restler');\n\n/**\n *\n * @param   {String}   domain\n *"
  },
  {
    "path": "src/backend/lib/error.js",
    "chars": 1383,
    "preview": "'use strict';\n\nconst _    = require('lodash');\nconst util = require('util');\n\nmodule.exports = {\n\n    ItemNotFoundError:"
  },
  {
    "path": "src/backend/lib/express/cors.js",
    "chars": 1089,
    "preview": "'use strict';\n\nconst validator = require('../validator');\n\nmodule.exports = function (req, res, next) {\n\n    if (req.hea"
  },
  {
    "path": "src/backend/lib/express/pagination.js",
    "chars": 1835,
    "preview": "'use strict';\n\nlet _ = require('lodash');\n\nmodule.exports = function (default_sort, default_offset, default_limit, max_l"
  },
  {
    "path": "src/backend/lib/helpers.js",
    "chars": 4056,
    "preview": "'use strict';\n\nconst moment = require('moment');\nconst _      = require('lodash');\n\nmodule.exports = {\n\n    /**\n     * T"
  },
  {
    "path": "src/backend/lib/validator/api.js",
    "chars": 1465,
    "preview": "'use strict';\n\nconst error  = require('../error');\nconst path   = require('path');\nconst parser = require('json-schema-r"
  },
  {
    "path": "src/backend/lib/validator/index.js",
    "chars": 1388,
    "preview": "'use strict';\n\nconst _           = require('lodash');\nconst error       = require('../error');\nconst definitions = requi"
  },
  {
    "path": "src/backend/logger.js",
    "chars": 253,
    "preview": "const {Signale} = require('signale');\n\nmodule.exports = {\n    global:   new Signale({scope: 'Global  '}),\n    migrate:  "
  },
  {
    "path": "src/backend/routes/api/main.js",
    "chars": 1014,
    "preview": "'use strict';\n\nconst express = require('express');\nconst pjson   = require('../../../../package.json');\n\nlet router = ex"
  },
  {
    "path": "src/backend/routes/api/repos.js",
    "chars": 3094,
    "preview": "'use strict';\n\nconst express      = require('express');\nconst validator    = require('../../lib/validator');\nconst pagin"
  },
  {
    "path": "src/backend/routes/main.js",
    "chars": 984,
    "preview": "'use strict';\n\nconst express = require('express');\nconst fs      = require('fs');\n\nconst router = express.Router({\n    c"
  },
  {
    "path": "src/backend/schema/definitions.json",
    "chars": 4473,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"definitions\",\n  \"definitions\": {\n    \"id\": {\n      \""
  },
  {
    "path": "src/backend/schema/endpoints/rules.json",
    "chars": 7418,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"endpoints/rules\",\n  \"title\": \"Rules\",\n  \"description"
  },
  {
    "path": "src/backend/schema/endpoints/services.json",
    "chars": 4936,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"endpoints/services\",\n  \"title\": \"Services\",\n  \"descr"
  },
  {
    "path": "src/backend/schema/endpoints/templates.json",
    "chars": 5583,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"endpoints/templates\",\n  \"title\": \"Templates\",\n  \"des"
  },
  {
    "path": "src/backend/schema/endpoints/tokens.json",
    "chars": 2441,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"endpoints/tokens\",\n  \"title\": \"Token\",\n  \"descriptio"
  },
  {
    "path": "src/backend/schema/endpoints/users.json",
    "chars": 6177,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"endpoints/users\",\n  \"title\": \"Users\",\n  \"description"
  },
  {
    "path": "src/backend/schema/examples.json",
    "chars": 627,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"examples\",\n  \"type\": \"object\",\n  \"definitions\": {\n  "
  },
  {
    "path": "src/backend/schema/index.json",
    "chars": 603,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"Juxtapose REST API\",\n  \"description\": \"This is the"
  },
  {
    "path": "src/frontend/app-images/favicons/browserconfig.xml",
    "chars": 262,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo"
  },
  {
    "path": "src/frontend/app-images/favicons/site.webmanifest",
    "chars": 458,
    "preview": "{\n    \"name\": \"\",\n    \"short_name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"/images/favicons/android-chrome-192"
  },
  {
    "path": "src/frontend/html/index.html",
    "chars": 3594,
    "preview": "<!doctype html>\n<html lang=\"en\" dir=\"ltr\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" conte"
  },
  {
    "path": "src/frontend/js/actions.js",
    "chars": 3405,
    "preview": "import {location} from 'hyperapp-hash-router';\nimport Api from './lib/api';\nimport $ from 'jquery';\nimport moment from '"
  },
  {
    "path": "src/frontend/js/components/app/image-tag.js",
    "chars": 1544,
    "preview": "import {div, h3, p, a} from '@hyperapp/html';\nimport Utils from '../../lib/utils';\n\nexport default (tag, config) => {\n  "
  },
  {
    "path": "src/frontend/js/components/app/insecure-registries.js",
    "chars": 847,
    "preview": "import {div, h3, h4, p, pre, code} from '@hyperapp/html';\n\nexport default domain => div({class: 'card'},\n    div({class:"
  },
  {
    "path": "src/frontend/js/components/tabler/big-error.js",
    "chars": 711,
    "preview": "import {div, i, h1, p, a} from '@hyperapp/html';\n\n/**\n * @param {Number}  code\n * @param {String}  message\n * @param {*}"
  },
  {
    "path": "src/frontend/js/components/tabler/icon-stat-card.js",
    "chars": 810,
    "preview": "import {div, i, span, h4, small} from '@hyperapp/html';\nimport Utils from '../../lib/utils';\n\n/**\n * @param {String|Numb"
  },
  {
    "path": "src/frontend/js/components/tabler/modal.js",
    "chars": 360,
    "preview": "import {div} from '@hyperapp/html';\nimport $ from 'jquery';\n\nexport default (content, onclose) => div({class: 'modal fad"
  },
  {
    "path": "src/frontend/js/components/tabler/nav.js",
    "chars": 1780,
    "preview": "import {div, i, ul, li, a} from '@hyperapp/html';\nimport {Link} from 'hyperapp-hash-router';\n\nexport default (show_delet"
  },
  {
    "path": "src/frontend/js/components/tabler/stat-card.js",
    "chars": 810,
    "preview": "import {div, i} from '@hyperapp/html';\nimport Utils from '../../lib/utils';\n\n/**\n * @param {String|Number}  big_stat\n * "
  },
  {
    "path": "src/frontend/js/components/tabler/table-body.js",
    "chars": 396,
    "preview": "import {tbody} from '@hyperapp/html';\nimport Trow from './table-row';\nimport _ from 'lodash';\n\n/**\n * @param   {Object} "
  },
  {
    "path": "src/frontend/js/components/tabler/table-card.js",
    "chars": 515,
    "preview": "import {div, table} from '@hyperapp/html';\nimport Thead from './table-head';\nimport Tbody from './table-body';\n\n/**\n * @"
  },
  {
    "path": "src/frontend/js/components/tabler/table-head.js",
    "chars": 465,
    "preview": "import {thead, tr, th} from '@hyperapp/html';\nimport _ from 'lodash';\n\n/**\n * @param {Array}   header\n */\nexport default"
  },
  {
    "path": "src/frontend/js/components/tabler/table-row.js",
    "chars": 770,
    "preview": "import {tr, td} from '@hyperapp/html';\nimport _ from 'lodash';\n\n/**\n * @param   {Object}  row\n * @param   {Object}  fiel"
  },
  {
    "path": "src/frontend/js/index.js",
    "chars": 4555,
    "preview": "// This has to exist here so that Webpack picks it up\nimport '../scss/styles.scss';\n\nimport $ from 'jquery';\nimport {app"
  },
  {
    "path": "src/frontend/js/lib/api.js",
    "chars": 3374,
    "preview": "import $ from 'jquery';\n\n/**\n * @param {String}  message\n * @param {*}       debug\n * @param {Integer} [code]\n * @constr"
  },
  {
    "path": "src/frontend/js/lib/manipulators.js",
    "chars": 425,
    "preview": "import {div} from '@hyperapp/html';\nimport {Link} from 'hyperapp-hash-router';\n\nexport default {\n\n    /**\n     * @return"
  },
  {
    "path": "src/frontend/js/lib/utils.js",
    "chars": 400,
    "preview": "import numeral from 'numeral';\n\nexport default {\n\n    /**\n     * @param   {Integer} number\n     * @returns {String}\n    "
  },
  {
    "path": "src/frontend/js/router.js",
    "chars": 1648,
    "preview": "import {Route} from 'hyperapp-hash-router';\nimport {div, span, a, p} from '@hyperapp/html';\nimport ImagesRoute from './r"
  },
  {
    "path": "src/frontend/js/routes/image.js",
    "chars": 4460,
    "preview": "import {div, h1, span, a, h4, button, p} from '@hyperapp/html';\nimport Nav from '../components/tabler/nav';\nimport BigEr"
  },
  {
    "path": "src/frontend/js/routes/images.js",
    "chars": 1411,
    "preview": "import {Link} from 'hyperapp-hash-router';\nimport {div, h4, p} from '@hyperapp/html';\nimport Nav from '../components/tab"
  },
  {
    "path": "src/frontend/js/routes/instructions/deleting.js",
    "chars": 2329,
    "preview": "import {div, h1, h3, p, pre, code} from '@hyperapp/html';\nimport Nav from '../../components/tabler/nav';\n\nexport default"
  },
  {
    "path": "src/frontend/js/routes/instructions/pulling.js",
    "chars": 985,
    "preview": "import {div, h1, p, pre, code} from '@hyperapp/html';\nimport Nav from '../../components/tabler/nav';\nimport Insecure fro"
  },
  {
    "path": "src/frontend/js/routes/instructions/pushing.js",
    "chars": 1022,
    "preview": "import {div, h1, p, pre, code} from '@hyperapp/html';\nimport Nav from '../../components/tabler/nav';\nimport Insecure fro"
  },
  {
    "path": "src/frontend/js/state.js",
    "chars": 225,
    "preview": "import {location} from 'hyperapp-hash-router';\n\nexport default {\n    location:           location.state,\n    isLoading: "
  },
  {
    "path": "src/frontend/scss/styles.scss",
    "chars": 396,
    "preview": "@import \"~tabler-ui/dist/assets/css/dashboard\";\n\n/* Before any JS content is loaded */\n#app > .loader, .container > .loa"
  },
  {
    "path": "webpack.config.js",
    "chars": 3688,
    "preview": "const path                 = require('path');\nconst webpack              = require('webpack');\nconst HtmlWebPackPlugin  "
  }
]

About this extraction

This page contains the full source code of the jc21/docker-registry-ui GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 65 files (120.3 KB), approximately 30.8k tokens, and a symbol index with 8 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!