Compare commits

...

124 Commits
v0.3.0 ... 1.x

Author SHA1 Message Date
dicedtomato
7354e561e3 Update README.md 2021-08-23 11:19:20 -07:00
dicedtomato
de7a2e7795 Zipline v1 is discontinued 2020-10-13 12:24:04 -07:00
dicedtomato
c7ff821e79 Update README.md 2020-10-08 19:16:12 -07:00
dicedtomato
fcddcc0ee7 rqetieruktyh32jo4l2j 2020-10-08 17:36:34 -07:00
dicedtomatoreal
06777cca92 #37 format 2020-10-05 14:50:32 -07:00
dicedtomato
7d68dc78eb Merge pull request #36 from Vetlix/master
make code go more brrrr
2020-09-29 10:00:42 -07:00
Vetlix
d0615d6fdb Speeeeeeeed!!!! 2020-09-29 18:52:30 +02:00
dicedtomato
c1c096c211 Merge pull request #30 from mzch/master
Fix 2 bugs
2020-09-19 19:53:10 -07:00
Koichi MATSUMOTO
beb82735fa fix typo
fix typo
2020-09-20 11:48:07 +09:00
Koichi MATSUMOTO
8cff5295e5 rename variable proxy to p`
rename variable `proxy` to `p`
2020-09-20 11:31:08 +09:00
Koichi MATSUMOTO
4d352bc0da fix config entry error
fix config entry `user.tokenLength` error.
2020-09-20 11:03:49 +09:00
Koichi MATSUMOTO
e25c3dee15 Fix 2 bugs
1. Fix a bug which ignores X-Forwarded-Proto header.
    For this it may be needed to add `core.trustedProxy` entry.
    It's defalt is `loopback`
2. Wrong config entry, it's renamed from `upload.uploadDIr` to `uploader.uploada`
2020-09-20 10:56:36 +09:00
dicedtomato
9d57b15dfc Fix "could not formulate upload route" 2020-09-18 13:34:45 -07:00
dicedtomato
4ab13292ca Update index.ts 2020-09-17 17:13:31 -07:00
dicedtomato
1d43e783f3 add register button 2020-09-17 20:47:13 +00:00
dicedtomato
3cead9dcc0 add public registration 2020-09-17 20:42:13 +00:00
dicedtomato
8ae2f85a45 Fix imports in cookies middleware 2020-09-17 16:09:20 +00:00
dicedtomato
0ce137a467 Update cookiesForAPI.ts 2020-09-17 08:59:58 -07:00
dicedtomato
10b51a88cb Change config from cookies 2020-09-17 08:59:39 -07:00
dicedtomato
8a04f42130 Delete codeql-analysis.yml 2020-09-17 08:57:04 -07:00
dicedtomato
62c207fbc3 Create workflow 2020-09-17 08:56:55 -07:00
dicedtomato
03c0306552 Merge pull request #21 from ZiplineProject/dependabot/npm_and_yarn/bcrypt-5.0.0
Bump bcrypt from 4.0.1 to 5.0.0
2020-09-17 08:49:03 -07:00
dicedtomato
4db128deb8 Merge pull request #22 from ZiplineProject/dependabot/npm_and_yarn/bl-2.2.1
Bump bl from 2.2.0 to 2.2.1
2020-09-17 08:47:50 -07:00
dicedtomato
ff4d3d81d3 Fix config options for orm 2020-09-17 08:46:01 -07:00
dicedtomato
254be71007 Merge pull request #23 from ZiplineProject/dependabot/npm_and_yarn/node-fetch-2.6.1
Bump node-fetch from 2.6.0 to 2.6.1
2020-09-12 14:00:50 -07:00
dependabot[bot]
1f682c4976 Bump node-fetch from 2.6.0 to 2.6.1
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-12 20:50:52 +00:00
dependabot[bot]
a258474ac0 Bump bl from 2.2.0 to 2.2.1
Bumps [bl](https://github.com/rvagg/bl) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/rvagg/bl/releases)
- [Commits](https://github.com/rvagg/bl/compare/v2.2.0...v2.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-03 21:59:35 +00:00
dependabot[bot]
376fa58c10 Bump bcrypt from 4.0.1 to 5.0.0
Bumps [bcrypt](https://github.com/kelektiv/node.bcrypt.js) from 4.0.1 to 5.0.0.
- [Release notes](https://github.com/kelektiv/node.bcrypt.js/releases)
- [Changelog](https://github.com/kelektiv/node.bcrypt.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kelektiv/node.bcrypt.js/compare/v4.0.1...v5.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-20 17:45:35 +00:00
dicedtomatoreal
e9fe41e7d2 fix issues with stats & new config 2020-08-12 10:59:17 -07:00
dicedtomatoreal
e718506c2f better extention figuringing outing & version update 2020-08-11 15:15:58 -07:00
dicedtomatoreal
b699a0f979 main files config update 2020-08-11 15:00:43 -07:00
dicedtomatoreal
e626d24a1f updating api with new config 2020-08-11 15:00:29 -07:00
dicedtomatoreal
3e31fb8acf add version/database to stat 2020-08-11 12:02:02 -07:00
dicedtomatoreal
4160c1c5b6 /api/stats route with useful stats! 2020-08-11 11:38:50 -07:00
dicedtomatoreal
061840c297 add clicks functionality 2020-08-11 11:38:33 -07:00
dicedtomatoreal
e29acadd41 add clicks statistic to shortens 2020-08-11 11:38:19 -07:00
dicedtomatoreal
b84907e281 remove logging 2020-08-11 08:46:21 -07:00
dicedtomatoreal
1485f2f75e delete img, and fixes 2020-08-11 08:25:40 -07:00
dicedtomatoreal
294be241af random 2020-07-08 11:21:26 -07:00
dicedtomatoreal
91ca59cef5 removing sharex jsons, changing TypeX to zipline
update script
2020-07-08 11:18:48 -07:00
dicedtomato
0635545a49 Set theme jekyll-theme-cayman 2020-07-08 09:50:22 -07:00
dicedtomato
18de95ac76 gg 2020-07-06 14:17:04 -07:00
dicedtomato
39a38cd243 Create codeql-analysis.yml 2020-06-27 14:46:21 -07:00
dicedtomato
6787dc8966 Create LICENSE 2020-06-27 14:45:13 -07:00
dicedtomato
2ec4f61805 Merge pull request #17 from dicedtomatoreal/add-code-of-conduct-1
Create CODE_OF_CONDUCT.md
2020-06-27 14:43:40 -07:00
dicedtomato
1e0ebb42ff Create CODE_OF_CONDUCT.md 2020-06-27 14:43:28 -07:00
dicedtomato
e594a21c4d Update issue templates 2020-06-27 14:34:50 -07:00
dicedtomato
fcd396b265 fix some conflicts 2020-06-27 14:28:19 -07:00
dicedtomato
54964e307e new uigit add .git add . 2020-06-27 14:20:30 -07:00
dicedtomatoreal
1d4469a7e7 notes backend 2020-06-02 08:36:06 -07:00
dicedtomatoreal
1fc264d08b notes 2020-05-30 08:53:18 -07:00
dicedtomato
0e26c8fb7d Merge pull request #15 from KryskZ09/master
Add number localization
2020-05-28 18:38:31 -07:00
Clayton Jensen
bda833f713 Add number localization 2020-05-28 20:32:21 -05:00
dicedtomato
ddb36a1d77 Merge pull request #14 from KryskZ09/master
Design Changes (w/out Ordering Change)
2020-05-28 18:26:58 -07:00
Clayton Jensen
51ed15c845 Revert tab ordering 2020-05-28 20:24:01 -05:00
Clayton Jensen
b4407616f6 Design changes 2020-05-28 19:36:17 -05:00
dicedtomatoreal
1d3087fcad add shortens list 2020-05-28 17:07:59 -07:00
dicedtomatoreal
66da53caad add shortens js 2020-05-28 17:04:16 -07:00
dicedtomatoreal
26c8ca79d6 fix sort again^6 2020-05-28 16:57:40 -07:00
dicedtomatoreal
d1a6134a95 fix sort again^5 2020-05-28 16:56:25 -07:00
dicedtomatoreal
58f8ee63d8 fix sort again^4 2020-05-28 16:54:16 -07:00
dicedtomatoreal
cb1e15344d fix sort again^3 2020-05-28 16:51:16 -07:00
dicedtomatoreal
4943a6904d fix sort again^2 2020-05-28 16:50:18 -07:00
dicedtomatoreal
2d185faef3 fix sort again 2020-05-28 16:47:59 -07:00
dicedtomatoreal
388076f35b remove console log smh 2020-05-28 16:44:31 -07:00
dicedtomatoreal
15d86bf097 fix sort issue 2020-05-28 16:44:03 -07:00
dicedtomatoreal
6d0db59e5e add js to get statistics 2020-05-28 16:40:04 -07:00
dicedtomatoreal
2fe01b860e add statistics html 2020-05-28 16:39:54 -07:00
dicedtomatoreal
231a17471b add statistics 2020-05-28 16:17:34 -07:00
dicedtomatoreal
d45953a7f8 update readme with new stuff 2020-05-28 16:17:24 -07:00
dicedtomato
3fa5b1e0a2 Merge pull request #12 from KryskZ09/master
fontawesome icons and design rework!
2020-05-28 16:05:03 -07:00
Clayton Jensen
0c7c0ef27e fontawesome icons and design rework 2020-05-28 18:02:41 -05:00
dicedtomatoreal
7b288f1503 fix type on shorten 2020-05-28 15:25:47 -07:00
dicedtomatoreal
91b2b9b2de versioning 2020-05-28 09:02:14 -07:00
dicedtomatoreal
4fe9370ce2 shortening api added 2020-05-28 09:01:34 -07:00
dicedtomatoreal
db02b1ddb3 frontend uses new api routes and backend fixed 2020-05-28 08:59:27 -07:00
dicedtomatoreal
73267e3e90 typo in user api 2020-05-28 08:33:49 -07:00
dicedtomatoreal
93edf6ec19 image api refactor 2020-05-28 08:30:52 -07:00
dicedtomatoreal
6f2b0bbd0d users api refactor 2020-05-28 08:09:50 -07:00
dicedtomatoreal
79b5110e21 reset password api thing 2020-05-27 21:40:52 -07:00
dicedtomatoreal
cf5df1150d update version cause i didnt do it for a few commits 2020-05-27 21:34:45 -07:00
dicedtomato
f4793e7f30 Delete main.yml 2020-05-27 21:30:46 -07:00
dicedtomato
76acc54e84 Merge pull request #9 from KryskZ09/master
 Add uploader templates :
2020-05-27 21:28:35 -07:00
Clayton Jensen
044c9adc0a Add uploader templates 2020-05-27 23:26:14 -05:00
dicedtomato
5fb9d80cfd Update main.yml 2020-05-27 21:26:08 -07:00
dicedtomatoreal
930056b323 pp 2020-05-27 21:24:41 -07:00
dicedtomatoreal
9acc2d8768 idk 2020-05-27 21:22:47 -07:00
dicedtomato
1f32cadff0 Merge pull request #8 from TacticalTechJay/patch-3
Update README.md
2020-05-27 21:21:10 -07:00
dicedtomatoreal
85d4cd6ae1 remove style on all ejs and add to head.ejs 2020-05-27 21:02:55 -07:00
dicedtomato
1ea6ffffec ADD DISCORD WORKFLOW 2020-05-27 21:00:07 -07:00
dicedtomatoreal
132c55397e fix invalid login, and pagination! 2020-05-27 20:25:41 -07:00
dicedtomatoreal
c9fa30cd65 maybe this fixes it 2020-05-26 18:25:55 -07:00
dicedtomatoreal
3b4e3da1cc cool dashboard thing :OOOO 2020-05-26 17:52:34 -07:00
TacticalCoderJay
90df30de59 Update README.md
Added more hyperlinks and stuff.
2020-05-25 11:39:24 -07:00
dicedtomatoreal
171b7111bb meta and login page fixes 2020-05-21 10:14:51 -07:00
dicedtomatoreal
20dbb14903 readme 2020-05-17 07:45:21 -07:00
dicedtomatoreal
07aad78993 url shortening 2020-05-17 07:27:34 -07:00
dicedtomatoreal
0e03736923 fix webhooks 2020-05-16 08:33:57 -07:00
dicedtomatoreal
cb8bde63f0 new ui update 2020-05-16 08:33:11 -07:00
dicedtomatoreal
5494f52ddf dont show hash on password input bruh 2020-05-13 16:17:50 -07:00
dicedtomatoreal
29a94ec843 added encryption 2020-05-13 16:16:24 -07:00
dicedtomatoreal
c37e933cc9 add ogp 2020-05-12 20:41:09 -07:00
dicedtomatoreal
01bf445644 fix administrator 2020-05-04 22:01:25 -07:00
dicedtomatoreal
20c3bf9910 cool 2020-05-04 20:00:13 -07:00
dicedtomato
5c2ee55994 Merge pull request #3 from TacticalTechJay/patch-2
booooooooo
2020-04-27 13:50:47 -07:00
dicedtomato
8790d9b2c0 Merge pull request #4 from Puyodead1/master
[View Image button] Open images in new tab
2020-04-27 13:50:03 -07:00
Puyodead1
46fa7a84e3 View Image button opens image in new tab 2020-04-27 14:34:31 -04:00
dicedtomatoreal
bacb4d5d8d versioning 2020-04-27 10:03:03 -07:00
dicedtomatoreal
28e9d1194e add returnProtocol and imageutil 2020-04-27 10:01:28 -07:00
dicedtomatoreal
811ee2a87d add returnProtocol and imageutil 2020-04-27 10:00:09 -07:00
TacticalCoderJay
281580b879 Update README.md
I unfricked the fricked
2020-04-27 09:41:31 -07:00
dicedtomato
aaeac1b7e3 Merge pull request #2 from TacticalTechJay/patch-1
Adding clicky bois
2020-04-27 09:36:49 -07:00
TacticalCoderJay
f9d17e9765 Update README.md 2020-04-27 09:34:46 -07:00
TacticalCoderJay
409207ab7d Update README.md 2020-04-27 09:30:44 -07:00
dicedtomatoreal
ba33756416 git aupdate image click action with modal 2020-04-27 08:52:13 -07:00
dicedtomatoreal
a6cf2a5c46 check for needed routes, added logs for indexcontroller 2020-04-26 19:57:37 -07:00
dicedtomatoreal
9d2eb09789 update version 2020-04-26 17:54:31 -07:00
dicedtomatoreal
346ed4d17a update readme 2020-04-26 17:54:18 -07:00
dicedtomatoreal
d66d50ef73 git add .add logging for everything 2020-04-26 17:53:15 -07:00
dicedtomatoreal
4be174e325 test 2020-04-26 17:28:55 -07:00
dicedtomato
9023978d0f whats this 2020-04-26 15:11:17 -07:00
dicedtomatoreal
8ddd678864 updated readme with new stuff 2020-04-26 14:59:49 -07:00
dicedtomatoreal
1c18828e1b config finding, 404 and 500 pages added 2020-04-26 14:41:55 -07:00
dicedtomatoreal
c2553c853f find file added for config search 2020-04-26 14:34:42 -07:00
48 changed files with 8672 additions and 1134 deletions

33
.github/ISSUE_TEMPLATE/bug_report.md vendored Executable file
View File

@@ -0,0 +1,33 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: dicedtomatoreal
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Arch]
- Browser [e.g. chrome, firefox, chrome mobile]
- Version [e.g. 2.0.0]
**Additional context**
Add any other context about the problem here.

27
.github/workflows/node.js.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build --if-present

5
.gitignore vendored Normal file → Executable file
View File

@@ -2,8 +2,9 @@ node_modules
temp
tmp
uploads
config.json
config*.json
out
public/assets/pd_logo.png
.idea
.vscode
ssl/localhost.key
ssl/localhost.crt

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
dist
scripts
public
views

1
.prettierrc.json Normal file
View File

@@ -0,0 +1 @@
{}

76
CODE_OF_CONDUCT.md Executable file
View File

@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at . All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

21
LICENSE Executable file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 dicedtomato
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.

328
OLDREADME.md Executable file
View File

@@ -0,0 +1,328 @@
# TypeX
A TypeScript based Image/File uploading server. Fast and Elegant.
## Table of Contents
1. [Prerequisites](#prerequisites)
1. [Node](#node--typescript)
2. [Common Databases](#common-databases)
3. [Installing Database Drivers](#installing-database-drivers)
1. [PostgreSQL](#getting-postgresql-drivers)
2. [CockroachDB](#getting-cockroachdb-drivers)
3. [MySQL](#getting-mysql-drivers)
4. [MariaDB](#getting-mariadb-drivers)
5. [Microsoft SQL Server](#getting-microsoft-sql-drivers)
2. [Updating TypeX](#updating-typex)
3. [Installation](#installation)
1. [Get the Source](#get-the-source--install-dependencies)
2. [Setting up configurations](#configuration-options)
1. [Upload Size](#upload)
2. [User Settings](#user-settings)
3. [Site Settings](#site-settings)
4. [SSL Settings](#site-ssl-settings)
5. [Administrator user](#administrator-user)
6. [Database configuration](#database-configuration)
7. [Session Secret](#session-secret)
8. [Particles.JS](#meta-configuration)
3. [Example Config](#example-config)
4. [Compiling Source](#compiling-typescript-for-running)
5. [Running Compiled Source](#running-compiled-source)
## Prerequisites
Dependencies needed for running TypeX
### Node & Typescript
Node.JS is what runs the show, and you will need it before anything else. Install it [here](https://nodejs.org)
Once Node is installed install Typescript by doing
```sh
npm i typescript -g
```
Verify your installation by running these commands
```sh
tsc -v
npm -v
node -v
```
They should all output something along the lines of
```sh
-> tsc -v
Version 3.8.3
-> npm -v
node 6.14.4
-> node -v
v13.13.0
```
### Common Databases
- [PostgreSQL](https://www.postgresql.org/ "PostgresSQL")
- [CockroachDB](https://www.cockroachlabs.com/ "Cockroach Labs")
- [MySQL](https://www.mysql.com/ "MySQL")
- [MariaDB](https://www.mariadb.com/ "MariaDB")
- [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/ "Microsoft SQL Server")
- [MongoDB](https://www.mongodb.com/ "MongoDB") (Coming soon!)
(check out [this](https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md) for all types, you will need to use a different ORM config later on, view [this](https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md#common-connection-options) for every option, more on this on Database configuration setup step)
### Installing Database Drivers
In this installation step, you will be installing the drivers of your choice database.
#### Getting PostgreSQL Drivers
Run the following command in order to get PostgreSQL drivers
```sh
npm i pg --save-dev
```
#### Getting CockroachDB Drivers
Run the following command in order to get CockroachDB drivers
```sh
npm i cockroachdb --save-dev
```
#### Getting MySQL Drivers
Run the following command in order to get MySQL drivers
```sh
npm i mysql --save-dev
```
#### Getting MariaDB Drivers
Run the following command in order to get MariaDB drivers
```sh
npm i mariadb --save-dev
```
#### Getting Microsoft SQL Drivers
Run the following command in order to get Microsoft SQL drivers
```sh
npm i mssql --save-dev
```
## Updating TypeX
Updating TypeX, is very simple. You can use this one-liner to update and compile code.
1. Run the following in the `typex` directory, if there were config changes, you should change them before this command.
```sh
git pull && tsc -p .
```
2. After that, you just need to restart the process for changes to take effect.
## Installation
Now that you have considered what prerequisites you would like, lets actually install this! This installation is based on Linux systems, yet will work on both MacOSX and Windows with their respective commands
### Get the Source & Install Dependencies
```sh
git clone https://github.com/dicedtomatoreal/typex
cd typex
tsc -p .
npm start
```
### Configuration Options
Every single configuration option will be listed here
#### Upload
**Config Property:** `upload`
| Config Property | Type | Description / Expected Values |
| ------------------- | ------- | ------------------------------------------------------------ |
| `upload.fileLength` | integer | how long the random id for a file should be |
| `upload.tempDir` | string | temporary directory, files are stored here and then deleted. |
| `upload.uploadDir` | string | upload directory (where all uploads are stored) |
| `upload.route` | string | Route for uploads, default is /u, ex.`/u/hd27ua.png` |
#### User Settings
**Config Property:** `user`
| Config Property | Type | Description / Expected Values |
| ------------------ | ------- | ---------------------------------------------------- |
| `user.tokenLength` | integer | How long the randomly generated user token should be |
#### Site Settings
**Config Property:** `site`
| Config Property | Type | Description / Expected Values |
| ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `site.protocol` | integer | protocol (http or https) |
| `site.serveHTTP` | string | Port to run the web server on with HTTP (can be used with nginx + CloudFlare as a reverse proxy and let CloudFlare take care of SSL) |
| `site.serveHTTPS` | string | Port to run the web server on with HTTPS (only will be used if `site.protocol` is `https`) (you will need SSL certificates! See [this](#site-ssl-settings)) |
| `site.logRoutes` | boolean | Wether or not to log routes when they are requested |
#### Site SSL Settings
**Config Property:** `site.ssl`
| Config Property | Type | Description / Expected Values |
| --------------- | ------ | ----------------------------------------------- |
| `site.ssl.key` | string | path to ssl private key. ex: `./ssl/server.key` |
| `site.ssl.cert` | string | path to ssl certificate. ex: `./ssl/cert.crt` |
#### Administrator User
**Config Property:** `administrator`
| Config Property | Type | Description / Expected Values |
| ------------------------ | ------ | -------------------------------------------------------------------------------------------------------- |
| `administrator.password` | string | password of administrator user (NOT RECOMENDED to use administrator user, set this to a SECURE password) |
#### Database Configuration
**Config Property:** `orm`
| Config Property | Type | Description / Expected Values |
| ----------------- | -------- | --------------------------------------------------------------------------------- |
| `orm.type` | string | `mariadb`, `mysql`, `postgres`, `cockroach`, `mssql` |
| `orm.host` | string | `localhost` or different IP |
| `orm.port` | integer | `5432` or different pot |
| `orm.username` | string | username |
| `orm.password` | string | password |
| `orm.database` | string | database to use |
| `orm.synchronize` | boolean | synchronize database to database, or not |
| `orm.logging` | boolean | log all queries |
| `orm.entities` | string[] | entity paths (should not be edited, and should be `["out/src/entities/**/*.js"]`) |
#### Session Secret
**Config Property:** `sessionSecret`
A Random string of characters (anything)
#### Session Secret
**Config Property:** `saltRounds`
The ammount of salt rounds needed to salt a password! (used for password encryption)
#### Meta Configuration
**Config Property:** `meta`
Particles.JS, can be enabled and it's config can be changed willingly.
| Config Property | Type | Description / Expected Values |
| --------------- | ------ | -------------------------------------------------------------------------------------------- |
| `meta.favicon` | string | has to be in /public/assets folder and should be formatted as `"/public/assets/<file name>"` |
| `meta.title` | string | title of your server shows up like `<title> - Login` or `<title> - Dashboard` |
### Example Config
```json
{
"upload": {
"fileLength": 6,
"tempDir": "./temp",
"uploadDir": "./uploads",
"route": "/u"
},
"shorten": {
"idLength": 4,
"route": "/s"
},
"user": {
"tokenLength": 32
},
"site": {
"protocol": "http",
"returnProtocol": "https",
"ssl": {
"key": "./ssl/server.key",
"cert": "./ssl/server.crt"
},
"serveHTTPS": 8000,
"serveHTTP": 443,
"logRoutes": true
},
"administrator": {
"password": "1234"
},
"orm": {
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "user",
"password": "1234",
"database": "typex",
"synchronize": true,
"logging": false,
"entities": ["out/src/entities/**/*.js"]
},
"sessionSecret": "1234",
"saltRounds": 10, // You might get an error if its over a certain number, so choose carefully.
"meta": {
"favicon": "/public/assets/typex_small_circle.png",
"title": "TypeX"
},
"discordWebhook": {
"enabled": true,
"url": "https://canary.discordapp.com/api/webhooks/id/token",
"username": "TypeX Logs",
"avatarURL": "https://domain/public/assets/typex_small_circle.png"
}
}
```
### Compiling Typescript for running
Compile the Typescript code before running the code, or you can run it with `ts-node` which is not recommended. **_MAKE SURE YOU ARE IN THE PROJECT DIR!_**
```sh
tsc -p .
```
### Running Compiled Source
Run the webserver by running
```sh
node out/src
```
## How you can upload
These are the options you must pass when uploading a url or image/file
### Uploader
| Property | Value |
| --------- | ----------------------------------- |
| URL | `https://<DOMAIN>/api/upload` |
| Header | `authorization: <TOKEN>` |
| Header | `content-type: multipart/form-data` |
| File name | `file` |
### URL Shortener
| Property | Value |
| -------- | -------------------------------- |
| URL | `https://<DOMAIN>/api/shorten` |
| Header | `authorization: <TOKEN>` |
| Header | `content-type: application/json` |
| Data | `{"url": "<URL>"}` |

400
README.md Normal file → Executable file
View File

@@ -1,387 +1,31 @@
# TypeX
# Stop using Zipline 1!
This current branch is discontinued, and will not be getting any updates. So...
Switch to [Zipline-Next](https://github.com/ziplineproject/zipline/tree/next)!<br>
Switch to [Zipline-Next](https://github.com/ziplineproject/zipline/tree/next)!<br>
Switch to [Zipline-Next](https://github.com/ziplineproject/zipline/tree/next)!<br>
Switch to [Zipline-Next](https://github.com/ziplineproject/zipline/tree/next)!<br>
# Zipline
A TypeScript based Image/File uploading server. Fast and Elegant.
Zipline is a Typescript based image/file uploading service & URL shortening service! Simple, and elegant.
## Table of Contents
# Bleeding Edge
1. Prerequisites
1. Node
2. Common Databases
3. Installing Database Drivers
1. PostgreSQL
2. CockroachDB
3. MySQL
4. MariaDB
5. Microsoft SQL Server
2. Installation
1. Get the Source
2. Setting up configurations
1. Upload Size
2. Site Settings
3. SSL Settings
4. Administrator user
5. Database configuration
6. Session Secret
7. Particles.JS
3. Example Config
4. Compiling Source
5. Running Compiled Source
Want to try the bleeding edge version of Zipline? Fear no more, [click here to view the branch (Zipline-Next)](https://github.com/ziplineproject/zipline/tree/next)! As it is still a work in progress piece, no documentation is written for it, and there is very little functionality within it, so try at your own risk! If you would like help to set it up join our [Discord](https://discord.gg/PWU8rxy).
## Prerequisites
# Images
Dependencies needed for running TypeX
![](https://cdn.diced.wtf/u/F1vtRX.png)
![](https://cdn.diced.wtf/u/a5BTaP.png)
![](https://cdn.diced.wtf/u/bdntjm.png)
![](https://cdn.diced.wtf/u/s8ulbP.png)
![](https://cdn.diced.wtf/u/DU7Bbr.png)
![](https://cdn.diced.wtf/u/fQMe1r.png)
![](https://cdn.diced.wtf/u/VTXMbo.png)
### Node & Typescript
# Bugs?
Node.JS is what runs the show, and you will need it before anything else. Install it [here](https://nodejs.org)
Make sure to open an issue if you think you ran into an issue, or need help with anything.
Once Node is installed install Typescript by doing
# Where is all the old information here?
```sh
npm i typescript -g
```
Verify your installation by running these commands
```sh
tsc -v
npm -v
node -v
```
They should all output something along the lines of
```sh
-> tsc -v
Version 3.8.3
-> npm -v
node 6.14.4
-> node -v
v13.13.0
```
### Common Databases
- PostgreSQL
- CockroachDB
- MySQL
- MariaDB
- Microsoft SQL Server
- MongoDB (Coming soon!)
(check out [this](https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md) for all types, you will need to use a different ORM config later on, view [this](https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md#common-connection-options) for every option, more on this on Database configuration setup step)
### Installing Database Drivers
In this installation step, you will be installing the drivers of your choice database.
#### Getting PostgreSQL Drivers
Run the following command in order to get PostgreSQL drivers
```sh
npm i pg --save-dev
```
#### Getting CockroachDB Drivers
Run the following command in order to get CockroachDB drivers
```sh
npm i cockroachdb --save-dev
```
#### Getting MySQL Drivers
Run the following command in order to get MySQL drivers
```sh
npm i mysql --save-dev
```
#### Getting MariaDB Drivers
Run the following command in order to get MariaDB drivers
```sh
npm i mariadb --save-dev
```
#### Getting Microsoft SQL Drivers
Run the following command in order to get Microsoft SQL drivers
```sh
npm i mssql --save-dev
```
## Installation
Now that you have considered what prerequisites you would like, lets actually install this! This installation is based on Linux systems, yet will work on both MacOSX and Windows with their respective commands
### Get the Source & Install Dependencies
You can get the source from the releases
```sh
wget <RELEASE TAR BALL>
tar -xvf <REALASE>
cd <REALASE>
npm i
```
### Configuration Options
Every single configuration option will be listed here
#### Upload
**Config Property:** `upload`
| Config Property | Type | Description / Expected Values |
| ------------------- | ------- | ------------------------------------------------------------ |
| `upload.fileLength` | integer | how long the random id for a file should be |
| `upload.tempDir` | string | temporary directory, files are stored here and then deleted. |
| `upload.uploadDir` | string | upload directory (where all uploads are stored) |
#### Site Settings
**Config Property:** `site`
| Config Property | Type | Description / Expected Values |
| --------------- | ------- | ------------------------------------------------------ |
| `site.protocol` | integer | protocol (http or https) |
| `site.serveHTTP` | string | Port to run the web server on with HTTP (can be used with nginx + CloudFlare as a reverse proxy and let CloudFlare take care of SSL) |
| `site.serveHTTPS` | string | Port to run the web server on with HTTPS (only will be used if `site.protocol` is `https`) (you will need SSL certificates! See [this](#ssl-settings)) |
#### SSL Settings
**Config Property:** `site.ssl`
| Config Property | Type | Description / Expected Values |
| --------------- | ------ | ----------------------------------------------- |
| `site.ssl.key` | string | path to ssl private key. ex: `./ssl/server.key` |
| `site.ssl.cert` | string | path to ssl certificate. ex: `./ssl/cert.crt` |
#### Administrator User
**Config Property:** `administrator`
| Config Property | Type | Description / Expected Values |
| ----------------------------- | ------ | -------------------------------------------------------------------------------------------------------- |
| `administrator.password` | string | password of administrator user (NOT RECOMENDED to use administrator user, set this to a SECURE password) |
| `administrator.authorization` | string | authorization token that could be used for uploading (NOT RECOMENDED, set this to a SECURE master token) |
#### Database Configuration
**Config Property:** `orm`
| Config Property | Type | Description / Expected Values |
| ----------------- | -------- | --------------------------------------------------------------------------------- |
| `orm.type` | string | `mariadb`, `mysql`, `postgres`, `cockroach`, `mssql` |
| `orm.host` | string | `localhost` or different IP |
| `orm.port` | integer | `5432` or different pot |
| `orm.username` | string | username |
| `orm.password` | string | password |
| `orm.database` | string | database to use |
| `orm.synchronize` | boolean | synchronize database to database, or not |
| `orm.logging` | boolean | log all queries |
| `orm.entities` | string[] | entity paths (should not be edited, and should be `["out/src/entities/**/*.js"]`) |
#### Session Secret
**Config Property:** `sessionSecret`
A Random string of characters (anything)
#### Meta Configuration
**Config Property:** `meta`
Particles.JS, can be enabled and it's config can be changed willingly.
| Config Property | Type | Description / Expected Values |
| --------------- | ------ | -------------------------------------------------------------------------------------------- |
| `meta.favicon` | string | has to be in /public/assets folder and should be formatted as `"/public/assets/<file name>"` |
| `meta.title` | string | title of your server shows up like `<title> - Login` or `<title> - Dashboard` |
#### Particles.JS Configuration
**Config Property:** `particles`
Particles.JS, can be enabled and it's config can be changed willingly.
| Config Property | Type | Description / Expected Values |
| ------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `particles.enabled` | boolean | Whether particles should show up on routes |
| `particles.config` | particles config | Config from [this](https://vincentgarreau.com/particles.js/) play around with the configuration, then paste the JSON to here |
### Example Config
```json
{
"upload": {
"fileLength": 6,
"tempDir": "./temp",
"uploadDir": "./uploads"
},
"meta": {
"favicon": "/public/assets/typex_small.png",
"title": "TypeX"
},
"site": {
"protocol": "https",
"ssl": {
"key": "./ssl/server.key",
"cert": "./ssl/server.crt"
},
"serveHTTPS": 443, // https port to serve on (will be used if protocol is https)
"serveHTTP": 8000 // http port to serve on (will be used if protocol is http)
},
"administrator": {
"password": "1234",
"authorization": "Administrator 1234"
},
"orm": {
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "user",
"password": "1234",
"database": "typex",
"synchronize": true,
"logging": false,
"entities": ["out/src/entities/**/*.js"]
},
"sessionSecret": "qwertyuiopasdfghjklzxcvbnm",
"particles": {
"enabled": true,
"settings": {
"particles": {
"number": {
"value": 52,
"density": {
"enable": true,
"value_area": 800
}
},
"color": {
"value": "#cd4c4c"
},
"shape": {
"type": "circle",
"stroke": {
"width": 0,
"color": "#000000"
},
"polygon": {
"nb_sides": 9
},
"image": {
"src": "img/github.svg",
"width": 60,
"height": 100
}
},
"opacity": {
"value": 0.5,
"random": false,
"anim": {
"enable": false,
"speed": 1,
"opacity_min": 0.1,
"sync": false
}
},
"size": {
"value": 0,
"random": true,
"anim": {
"enable": false,
"speed": 40,
"size_min": 0.1,
"sync": false
}
},
"line_linked": {
"enable": true,
"distance": 150,
"color": "#ffffff",
"opacity": 0.4,
"width": 1
},
"move": {
"enable": true,
"speed": 6,
"direction": "none",
"random": false,
"straight": false,
"out_mode": "out",
"bounce": false,
"attract": {
"enable": false,
"rotateX": 600,
"rotateY": 1200
}
}
},
"interactivity": {
"detect_on": "canvas",
"events": {
"onhover": {
"enable": false,
"mode": "grab"
},
"onclick": {
"enable": false,
"mode": "repulse"
},
"resize": true
},
"modes": {
"grab": {
"distance": 400,
"line_linked": {
"opacity": 1
}
},
"bubble": {
"distance": 400,
"size": 40,
"duration": 2,
"opacity": 8,
"speed": 3
},
"repulse": {
"distance": 200,
"duration": 0.4
},
"push": {
"particles_nb": 4
},
"remove": {
"particles_nb": 2
}
}
},
"retina_detect": true
}
}
}
```
### Compiling Typescript for running
Compile the Typescript code before running the code, or you can run it with `ts-node` which is not recommended. **_MAKE SURE YOU ARE IN THE PROJECT DIR!_**
```sh
tsc -p .
```
### Running Compiled Source
Run the webserver by running
```sh
node out/src
```
All configuration options have been moved to the wiki, as the README was getting too long and hard to manage.

582
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "typex",
"version": "0.1.0",
"version": "2.1.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -12,16 +12,6 @@
"fecha": "^4.1.0"
}
},
"@forevolve/bootstrap-dark": {
"version": "1.0.0-alpha.981",
"resolved": "https://registry.npmjs.org/@forevolve/bootstrap-dark/-/bootstrap-dark-1.0.0-alpha.981.tgz",
"integrity": "sha512-3oN8O8nrbfziK4d0QJ3xcL7zctaEWiNaPGX90xsuXxCoW9kVwRbb1Mz97W/yf/8vklq3vycqz9ieCAUY92P89A==",
"requires": {
"bootstrap": "^4.4.1",
"jquery": "^3.4.1",
"popper.js": "^1.16.1"
}
},
"@overnightjs/core": {
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/@overnightjs/core/-/core-1.6.15.tgz",
@@ -32,6 +22,11 @@
"tslib": "^1.9.3"
}
},
"@types/bcrypt": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-3.0.0.tgz",
"integrity": "sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ=="
},
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
@@ -41,6 +36,14 @@
"@types/node": "*"
}
},
"@types/centra": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/centra/-/centra-2.2.0.tgz",
"integrity": "sha512-TUpM1QoIgXa2VL1LnWbTgde39+rRnsOtvb0v8ZrX8t51g8K1Wwu4HAEXcBg5a56GV5Vm/KazyE5+g4AW8YbKNg==",
"requires": {
"@types/node": "*"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@@ -119,6 +122,14 @@
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
},
"@types/semver": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.1.tgz",
"integrity": "sha512-ooD/FJ8EuwlDKOI6D9HWxgIgJjMg2cuziXm/42npDC8y4NjxplBUn9loewZiBNCt44450lHAU0OSb51/UqXeag==",
"requires": {
"@types/node": "*"
}
},
"@types/serve-static": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
@@ -128,6 +139,11 @@
"@types/mime": "*"
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@@ -165,6 +181,49 @@
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"are-we-there-yet": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -188,6 +247,57 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"bcrypt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz",
"integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==",
"requires": {
"node-addon-api": "^3.0.0",
"node-pre-gyp": "0.15.0"
}
},
"bl": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
"dev": true,
"requires": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@@ -205,11 +315,6 @@
"type-is": "~1.6.17"
}
},
"bootstrap": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.4.1.tgz",
"integrity": "sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -219,6 +324,12 @@
"concat-map": "0.0.1"
}
},
"bson": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.4.tgz",
"integrity": "sha512-S/yKGU1syOMzO86+dGpg2qGoDL0zvzcb262G+gqEy6TgP6rt6z6qxSFX/8X6vLC91P7G7C3nLs0+bvDzmvBA3Q==",
"dev": true
},
"buffer": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
@@ -258,6 +369,11 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"centra": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/centra/-/centra-2.4.2.tgz",
"integrity": "sha512-f1RaP0V1HqVNEXfLfjNBthB2yy3KnSGnPCnOPCFLUk9e/Z4rNJ8nBaJNnghflnp88mi1IT8mfmW+HlMS1/H+bg=="
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -268,6 +384,11 @@
"supports-color": "^5.3.0"
}
},
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"cli-highlight": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.4.tgz",
@@ -355,6 +476,11 @@
"wrap-ansi": "^6.2.0"
}
},
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -413,6 +539,11 @@
}
}
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
@@ -463,6 +594,22 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"denque": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==",
"dev": true
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -473,6 +620,11 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
@@ -634,11 +786,67 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"requires": {
"minipass": "^2.6.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"requires": {
"number-is-nan": "^1.0.0"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
}
}
}
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -677,6 +885,11 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
},
"highlight.js": {
"version": "9.18.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz",
@@ -712,6 +925,14 @@
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"ignore-walk": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"requires": {
"minimatch": "^3.0.4"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -726,6 +947,11 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -741,11 +967,6 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"jquery": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz",
"integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ=="
},
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
@@ -768,6 +989,13 @@
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"dev": true,
"optional": true
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -809,6 +1037,23 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"requires": {
"minipass": "^2.9.0"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@@ -817,6 +1062,20 @@
"minimist": "^1.2.5"
}
},
"mongodb": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.8.tgz",
"integrity": "sha512-jz7mR58z66JKL8Px4ZY+FXbgB7d0a0hEGCT7kw8iye46/gsqPrOEpZOswwJ2BQlfzsrCLKdsF9UcaUfGVN2HrQ==",
"dev": true,
"requires": {
"bl": "^2.2.0",
"bson": "^1.1.4",
"denque": "^1.4.1",
"require_optional": "^1.0.1",
"safe-buffer": "^5.1.2",
"saslprep": "^1.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -847,11 +1106,113 @@
"thenify-all": "^1.0.0"
}
},
"needle": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz",
"integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==",
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"node-addon-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz",
"integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg=="
},
"node-pre-gyp": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz",
"integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==",
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.3",
"needle": "^2.5.0",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
"rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4.4.2"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
},
"npm-bundled": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"requires": {
"npm-normalize-package-bin": "^1.0.1"
}
},
"npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
},
"npm-packlist": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1",
"npm-normalize-package-bin": "^1.0.1"
}
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -878,6 +1239,25 @@
"wrappy": "1"
}
},
"os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"osenv": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"requires": {
"os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@@ -957,6 +1337,14 @@
"pg-types": "^2.1.0",
"pgpass": "1.x",
"semver": "4.3.2"
},
"dependencies": {
"semver": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz",
"integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=",
"dev": true
}
}
},
"pg-connection-string": {
@@ -1005,11 +1393,6 @@
"split": "^1.0.0"
}
},
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
},
"postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
@@ -1037,6 +1420,12 @@
"xtend": "^4.0.0"
}
},
"prettier": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
"integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
"dev": true
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -1077,6 +1466,17 @@
"unpipe": "1.0.0"
}
},
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
}
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
@@ -1103,6 +1503,38 @@
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"require_optional": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
"dev": true,
"requires": {
"resolve-from": "^2.0.0",
"semver": "^5.1.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
}
},
"resolve-from": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=",
"dev": true
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"requires": {
"glob": "^7.1.3"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -1113,16 +1545,25 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"saslprep": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
"dev": true,
"optional": true,
"requires": {
"sparse-bitfield": "^3.0.3"
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz",
"integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=",
"dev": true
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
},
"send": {
"version": "0.17.1",
@@ -1186,6 +1627,26 @@
"safe-buffer": "^5.0.1"
}
},
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
"dev": true,
"optional": true,
"requires": {
"memory-pager": "^1.0.2"
}
},
"spectre.css": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/spectre.css/-/spectre.css-0.5.8.tgz",
"integrity": "sha512-3N4WocWY+Dl6b3e5v3nsZYyp+VSDcBfGDzyyHw/H78ie9BoAhHkxmrhLxo9y8RadxYzVrPjfPdlev3hXEUzR2w=="
},
"split": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
@@ -1233,6 +1694,11 @@
"ansi-regex": "^5.0.0"
}
},
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -1241,6 +1707,20 @@
"has-flag": "^3.0.0"
}
},
"tar": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.8.6",
"minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
}
},
"thenify": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz",
@@ -1357,6 +1837,43 @@
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"requires": {
"string-width": "^1.0.2 || 2"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "^3.0.0"
}
}
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@@ -1420,6 +1937,11 @@
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"yargonaut": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz",

View File

@@ -1,16 +1,22 @@
{
"name": "typex",
"version": "0.3.0",
"private": true,
"scripts": {},
"version": "1.2.0",
"scripts": {
"build": "tsc -p .",
"start": "node out/src"
},
"dependencies": {
"@ayanaware/logger": "^2.2.1",
"@forevolve/bootstrap-dark": "^1.0.0-alpha.981",
"@overnightjs/core": "^1.6.15",
"@types/bcrypt": "^3.0.0",
"@types/centra": "^2.2.0",
"@types/cookie-parser": "^1.4.2",
"@types/express-session": "^1.17.0",
"@types/mime": "^2.0.1",
"@types/multer": "^1.4.3",
"@types/semver": "^7.3.1",
"bcrypt": "^5.0.0",
"centra": "^2.4.2",
"cookie-parser": "^1.4.5",
"ejs": "^3.0.2",
"express": "^4.17.1",
@@ -18,9 +24,13 @@
"http-status-codes": "^1.4.0",
"mime": "^2.4.4",
"multer": "^1.4.2",
"semver": "^7.3.2",
"spectre.css": "^0.5.8",
"typeorm": "^0.2.24"
},
"devDependencies": {
"pg": "^8.0.3"
"mongodb": "^3.5.8",
"pg": "^8.0.3",
"prettier": "2.1.2"
}
}
}

BIN
public/assets/typex-small.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

0
public/assets/typex.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

0
public/assets/typex_circle.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

0
public/assets/typex_small.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

0
public/assets/typex_small_circle.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,17 +0,0 @@
canvas{
vertical-align: text-bottom;
}
#particles-js canvas{
position:absolute;
z-index: -1;
width: 100%;
height: auto;
background-image: url("");
background-repeat: no-repeat;
background-size: cover;
background-position: 50% 50%; }
.count-particles{
border-radius: 0 0 3px 3px;
}

1227
public/css/spectre-exp.css Executable file

File diff suppressed because it is too large Load Diff

597
public/css/spectre-icons.css Executable file
View File

@@ -0,0 +1,597 @@
/*! Spectre.css Icons v0.5.8 | MIT License | github.com/picturepan2/spectre */
.icon {
box-sizing: border-box;
display: inline-block;
font-size: inherit;
font-style: normal;
height: 1em;
position: relative;
text-indent: -9999px;
vertical-align: middle;
width: 1em;
}
.icon::before,
.icon::after {
content: "";
display: block;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.icon.icon-2x {
font-size: 1.6rem;
}
.icon.icon-3x {
font-size: 2.4rem;
}
.icon.icon-4x {
font-size: 3.2rem;
}
.accordion .icon,
.btn .icon,
.toast .icon,
.menu .icon {
vertical-align: -10%;
}
.btn-lg .icon {
vertical-align: -15%;
}
.icon-arrow-down::before,
.icon-arrow-left::before,
.icon-arrow-right::before,
.icon-arrow-up::before,
.icon-downward::before,
.icon-back::before,
.icon-forward::before,
.icon-upward::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-right: 0;
height: .65em;
width: .65em;
}
.icon-arrow-down::before {
transform: translate(-50%, -75%) rotate(225deg);
}
.icon-arrow-left::before {
transform: translate(-25%, -50%) rotate(-45deg);
}
.icon-arrow-right::before {
transform: translate(-75%, -50%) rotate(135deg);
}
.icon-arrow-up::before {
transform: translate(-50%, -25%) rotate(45deg);
}
.icon-back::after,
.icon-forward::after {
background: currentColor;
height: .1rem;
width: .8em;
}
.icon-downward::after,
.icon-upward::after {
background: currentColor;
height: .8em;
width: .1rem;
}
.icon-back::after {
left: 55%;
}
.icon-back::before {
transform: translate(-50%, -50%) rotate(-45deg);
}
.icon-downward::after {
top: 45%;
}
.icon-downward::before {
transform: translate(-50%, -50%) rotate(-135deg);
}
.icon-forward::after {
left: 45%;
}
.icon-forward::before {
transform: translate(-50%, -50%) rotate(135deg);
}
.icon-upward::after {
top: 55%;
}
.icon-upward::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.icon-caret::before {
border-left: .3em solid transparent;
border-right: .3em solid transparent;
border-top: .3em solid currentColor;
height: 0;
transform: translate(-50%, -25%);
width: 0;
}
.icon-menu::before {
background: currentColor;
box-shadow: 0 -.35em, 0 .35em;
height: .1rem;
width: 100%;
}
.icon-apps::before {
background: currentColor;
box-shadow: -.35em -.35em, -.35em 0, -.35em .35em, 0 -.35em, 0 .35em, .35em -.35em, .35em 0, .35em .35em;
height: 3px;
width: 3px;
}
.icon-resize-horiz::before,
.icon-resize-horiz::after,
.icon-resize-vert::before,
.icon-resize-vert::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-right: 0;
height: .45em;
width: .45em;
}
.icon-resize-horiz::before,
.icon-resize-vert::before {
transform: translate(-50%, -90%) rotate(45deg);
}
.icon-resize-horiz::after,
.icon-resize-vert::after {
transform: translate(-50%, -10%) rotate(225deg);
}
.icon-resize-horiz::before {
transform: translate(-90%, -50%) rotate(-45deg);
}
.icon-resize-horiz::after {
transform: translate(-10%, -50%) rotate(135deg);
}
.icon-more-horiz::before,
.icon-more-vert::before {
background: currentColor;
border-radius: 50%;
box-shadow: -.4em 0, .4em 0;
height: 3px;
width: 3px;
}
.icon-more-vert::before {
box-shadow: 0 -.4em, 0 .4em;
}
.icon-plus::before,
.icon-minus::before,
.icon-cross::before {
background: currentColor;
height: .1rem;
width: 100%;
}
.icon-plus::after,
.icon-cross::after {
background: currentColor;
height: 100%;
width: .1rem;
}
.icon-cross::before {
width: 100%;
}
.icon-cross::after {
height: 100%;
}
.icon-cross::before,
.icon-cross::after {
transform: translate(-50%, -50%) rotate(45deg);
}
.icon-check::before {
border: .1rem solid currentColor;
border-right: 0;
border-top: 0;
height: .5em;
transform: translate(-50%, -75%) rotate(-45deg);
width: .9em;
}
.icon-stop {
border: .1rem solid currentColor;
border-radius: 50%;
}
.icon-stop::before {
background: currentColor;
height: .1rem;
transform: translate(-50%, -50%) rotate(45deg);
width: 1em;
}
.icon-shutdown {
border: .1rem solid currentColor;
border-radius: 50%;
border-top-color: transparent;
}
.icon-shutdown::before {
background: currentColor;
content: "";
height: .5em;
top: .1em;
width: .1rem;
}
.icon-refresh::before {
border: .1rem solid currentColor;
border-radius: 50%;
border-right-color: transparent;
height: 1em;
width: 1em;
}
.icon-refresh::after {
border: .2em solid currentColor;
border-left-color: transparent;
border-top-color: transparent;
height: 0;
left: 80%;
top: 20%;
width: 0;
}
.icon-search::before {
border: .1rem solid currentColor;
border-radius: 50%;
height: .75em;
left: 5%;
top: 5%;
transform: translate(0, 0) rotate(45deg);
width: .75em;
}
.icon-search::after {
background: currentColor;
height: .1rem;
left: 80%;
top: 80%;
transform: translate(-50%, -50%) rotate(45deg);
width: .4em;
}
.icon-edit::before {
border: .1rem solid currentColor;
height: .4em;
transform: translate(-40%, -60%) rotate(-45deg);
width: .85em;
}
.icon-edit::after {
border: .15em solid currentColor;
border-right-color: transparent;
border-top-color: transparent;
height: 0;
left: 5%;
top: 95%;
transform: translate(0, -100%);
width: 0;
}
.icon-delete::before {
border: .1rem solid currentColor;
border-bottom-left-radius: .1rem;
border-bottom-right-radius: .1rem;
border-top: 0;
height: .75em;
top: 60%;
width: .75em;
}
.icon-delete::after {
background: currentColor;
box-shadow: -.25em .2em, .25em .2em;
height: .1rem;
top: .05rem;
width: .5em;
}
.icon-share {
border: .1rem solid currentColor;
border-radius: .1rem;
border-right: 0;
border-top: 0;
}
.icon-share::before {
border: .1rem solid currentColor;
border-left: 0;
border-top: 0;
height: .4em;
left: 100%;
top: .25em;
transform: translate(-125%, -50%) rotate(-45deg);
width: .4em;
}
.icon-share::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-radius: 75% 0;
border-right: 0;
height: .5em;
width: .6em;
}
.icon-flag::before {
background: currentColor;
height: 1em;
left: 15%;
width: .1rem;
}
.icon-flag::after {
border: .1rem solid currentColor;
border-bottom-right-radius: .1rem;
border-left: 0;
border-top-right-radius: .1rem;
height: .65em;
left: 60%;
top: 35%;
width: .8em;
}
.icon-bookmark::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-top-left-radius: .1rem;
border-top-right-radius: .1rem;
height: .9em;
width: .8em;
}
.icon-bookmark::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-left: 0;
border-radius: .1rem;
height: .5em;
transform: translate(-50%, 35%) rotate(-45deg) skew(15deg, 15deg);
width: .5em;
}
.icon-download,
.icon-upload {
border-bottom: .1rem solid currentColor;
}
.icon-download::before,
.icon-upload::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-right: 0;
height: .5em;
transform: translate(-50%, -60%) rotate(-135deg);
width: .5em;
}
.icon-download::after,
.icon-upload::after {
background: currentColor;
height: .6em;
top: 40%;
width: .1rem;
}
.icon-upload::before {
transform: translate(-50%, -60%) rotate(45deg);
}
.icon-upload::after {
top: 50%;
}
.icon-copy::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-radius: .1rem;
border-right: 0;
height: .8em;
left: 40%;
top: 35%;
width: .8em;
}
.icon-copy::after {
border: .1rem solid currentColor;
border-radius: .1rem;
height: .8em;
left: 60%;
top: 60%;
width: .8em;
}
.icon-time {
border: .1rem solid currentColor;
border-radius: 50%;
}
.icon-time::before {
background: currentColor;
height: .4em;
transform: translate(-50%, -75%);
width: .1rem;
}
.icon-time::after {
background: currentColor;
height: .3em;
transform: translate(-50%, -75%) rotate(90deg);
transform-origin: 50% 90%;
width: .1rem;
}
.icon-mail::before {
border: .1rem solid currentColor;
border-radius: .1rem;
height: .8em;
width: 1em;
}
.icon-mail::after {
border: .1rem solid currentColor;
border-right: 0;
border-top: 0;
height: .5em;
transform: translate(-50%, -90%) rotate(-45deg) skew(10deg, 10deg);
width: .5em;
}
.icon-people::before {
border: .1rem solid currentColor;
border-radius: 50%;
height: .45em;
top: 25%;
width: .45em;
}
.icon-people::after {
border: .1rem solid currentColor;
border-radius: 50% 50% 0 0;
height: .4em;
top: 75%;
width: .9em;
}
.icon-message {
border: .1rem solid currentColor;
border-bottom: 0;
border-radius: .1rem;
border-right: 0;
}
.icon-message::before {
border: .1rem solid currentColor;
border-bottom-right-radius: .1rem;
border-left: 0;
border-top: 0;
height: .8em;
left: 65%;
top: 40%;
width: .7em;
}
.icon-message::after {
background: currentColor;
border-radius: .1rem;
height: .3em;
left: 10%;
top: 100%;
transform: translate(0, -90%) rotate(45deg);
width: .1rem;
}
.icon-photo {
border: .1rem solid currentColor;
border-radius: .1rem;
}
.icon-photo::before {
border: .1rem solid currentColor;
border-radius: 50%;
height: .25em;
left: 35%;
top: 35%;
width: .25em;
}
.icon-photo::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-left: 0;
height: .5em;
left: 60%;
transform: translate(-50%, 25%) rotate(-45deg);
width: .5em;
}
.icon-link::before,
.icon-link::after {
border: .1rem solid currentColor;
border-radius: 5em 0 0 5em;
border-right: 0;
height: .5em;
width: .75em;
}
.icon-link::before {
transform: translate(-70%, -45%) rotate(-45deg);
}
.icon-link::after {
transform: translate(-30%, -55%) rotate(135deg);
}
.icon-location::before {
border: .1rem solid currentColor;
border-radius: 50% 50% 50% 0;
height: .8em;
transform: translate(-50%, -60%) rotate(-45deg);
width: .8em;
}
.icon-location::after {
border: .1rem solid currentColor;
border-radius: 50%;
height: .2em;
transform: translate(-50%, -80%);
width: .2em;
}
.icon-emoji {
border: .1rem solid currentColor;
border-radius: 50%;
}
.icon-emoji::before {
border-radius: 50%;
box-shadow: -.17em -.1em, .17em -.1em;
height: .15em;
width: .15em;
}
.icon-emoji::after {
border: .1rem solid currentColor;
border-bottom-color: transparent;
border-radius: 50%;
border-right-color: transparent;
height: .5em;
transform: translate(-50%, -40%) rotate(-135deg);
width: .5em;
}

3718
public/css/spectre.css Executable file

File diff suppressed because it is too large Load Diff

483
public/js/dashboard.js Executable file
View File

@@ -0,0 +1,483 @@
const TypeX = {
currentImagePage: 0,
pagedNumbers: [],
currentID: null
}
function whitespace(str) {
return str === null || str.match(/^ *$/) !== null;
}
function showAlert(type, message) {
if (type === 'error') {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: message,
footer: 'Try again later...'
})
} else if (type === 'success') {
Swal.fire({
icon: 'success',
title: 'Success',
text: message,
footer: 'You did it!'
})
}
}
function copyText(text) {
var input = document.createElement('textarea');
input.innerHTML = text;
document.body.appendChild(input);
input.select();
var result = document.execCommand('copy');
document.body.removeChild(input);
return result;
}
async function redoImageGrid(page, mode = null) {
if (!page && !mode) return;
document.getElementById('typexImages').innerHTML = '';
let url = '';
if (mode === 'prev') {
if (TypeX.currentImagePage === 0) {
url = '/api/images/user/pages?page=0';
TypeX.currentImagePage = 0;
} else {
url = `/api/images/user/pages?page=${TypeX.currentImagePage - 1}`;
TypeX.currentImagePage--;
} //could be better :DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
} else if (mode === 'next') {
if (TypeX.pagedNumbers[TypeX.pagedNumbers.length - 1] <= TypeX.currentImagePage + 1) {
url = `/api/images/user/pages?page=${TypeX.pagedNumbers[TypeX.pagedNumbers.length - 1]}`
TypeX.currentImagePage = TypeX.pagedNumbers[TypeX.pagedNumbers.length - 1];
} else {
url = `/api/images/user/pages?page=${TypeX.currentImagePage + 1}`
TypeX.currentImagePage++;
}
} else if (mode === 'normal') {
url = `/api/images/user/pages?page=${page}`;
TypeX.currentImagePage = Number(page);
}
$("#typexImagePaginationDropdown").val(TypeX.currentImagePage);
const resp = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const json = await resp.json();
if (json.error || json.code) return showAlert('error', json.error);
try {
json.page.forEach(image => {
$('#typexImages').append(`
<div class="column col-4">
<div class="card">
<div class="card-image">
<img src="${image.url}" class="img-responsive" onclick="document.getElementById('modal-image-mngt-${image.id}').classList.add('active')">
<div class="modal" id="modal-image-mngt-${image.id}">
<a href="#close" class="modal-overlay" aria-label="Close" onclick="document.getElementById('modal-image-mngt-${image.id}').classList.remove('active')"></a>
<div class="modal-container bg-dark">
<div class="modal-header">
<a href="#close" class="btn btn-clear float-right text-light" aria-label="Close" onclick="document.getElementById('modal-image-mngt-${image.id}').classList.remove('active')"></a>
<div class="modal-title text-light h5">Manage Image ${image.id}</div>
</div>
<div class="modal-body">
This image is viewable at <a href="${image.url}">${image.url}</a> and has <b>${image.views}</b> views.
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" onclick="deleteImage('${image.id}')">Delete Image</button>
</div>
</div>
</div>
</div>
</div>
</div>
`);
});
} catch (e) {
document.getElementById('emptyImages').innerHTML = `
<div class="empty bg-dark">
<div class="empty-icon">
<i class="icon icon-photo"></i>
</div>
<p class="empty-title h5">You have no imaages</p>
<p class="empty-subtitle">Use the API to start uploading!</p>
</div>
`;
document.getElementById('typexImagePagination').innerHTML = '';
}
}
document.getElementById('updateImages').addEventListener('click', async () => {
redoImageGrid('0', 'normal');
document.getElementById('emptyImages').innerHTML = '';
document.getElementById('typexImagePagination').innerHTML = '';
const resp = await fetch('/api/images/user/pages', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const json = await resp.json();
try {
$('#typexImagePagination').append(`
<li class="page-item">
<a class="page-link" aria-label="First" onclick="redoImageGrid('0', 'normal')">
First
</a>
</li>`);
$('#typexImagePagination').append(`
<li class="page-item">
<a class="page-link" aria-label="Previous" onclick="redoImageGrid(null, 'prev')">
Prev
</a>
</li>`);
$('#typexImagePagination').append(`
<li class="page-item">
<select class="form-select" id="typexImagePaginationDropdown">
</select>
</li>`)
TypeX.pagedNumbers = json.pagedNums;
json.pagedNums.forEach(p => {
$('#typexImagePaginationDropdown').append(`
<option onclick="redoImageGrid('${p}', 'normal')" value="${p}">${p + 1}</option>
`)
});
$('#typexImagePagination').append(`
<li class="page-item">
<a onclick="redoImageGrid(null, 'next')">
Next
</a>
</li>`);
$('#typexImagePagination').append(`
<li class="page-item">
<a onclick="redoImageGrid(TypeX.pagedNumbers[TypeX.pagedNumbers.length-1], 'normal')">
Last
</a>
</li>`);
} catch (e) {
console.error(e)
}
});
document.getElementById('updateStatistics').addEventListener('click', async () => {
const resp = await fetch('/api/images/statistics', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const json = await resp.json();
try {
document.getElementById('statsDescription').innerHTML = `You have an average of <b>${Math.floor(json.average).toLocaleString()} views</b> on your images, you have <b>${json.totalViews.toLocaleString()} views total</b>, you currently have <b>${json.images.toLocaleString()} images</b>!`
document.getElementById('statsLeaderboardImages').innerHTML = '';
document.getElementById('statsLeaderboardImageViews').innerHTML = '';
for (let i = 0; i < json.table.images.length; i++) {
const c = json.table.images[i];
$('#statsLeaderboardImages').append(`
<tr>
<th>${i + 1}</th>
<td>${c.username}</td>
<td>${c.count.toLocaleString()}</td>
</tr>
`)
}
for (let i = 0; i < json.table.views.length; i++) {
const c = json.table.views[i];
$('#statsLeaderboardImageViews').append(`
<tr>
<th>${i + 1}</th>
<td>${c.username}</td>
<td>${c.count.toLocaleString()}</td>
</tr>
`)
}
} catch (e) {
console.error(e)
}
});
document.getElementById('updateShortens').addEventListener('click', async () => {
const resp = await fetch('/api/shortens', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const json = await resp.json();
try {
document.getElementById('shortensTableShortens').innerHTML = '';
for (const shorten of json) {
$('#shortensTableShortens').append(`
<tr>
<th>${shorten.id}</th>
<td><a href="${shorten.origin}">${shorten.origin}</a></td>
<td><a href="${shorten.url}">${shorten.url}</a></td>
</tr>
`)
}
} catch (e) {
console.error(e)
}
});
const deleteImage = (id, url) => {
Swal.fire({
title: 'Are you sure?',
text: `You are proceeding to delete image (${id}), you will not be able to recover it!`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, delete it.'
}).then(async (result) => {
if (result.value) {
try {
const res = await fetch('/api/images/' + id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
Swal.fire(
'Deleted!',
`Deleted image (${id}) successfully.`,
'success'
);
document.getElementById(`modal-image-mngt-${id}`).classList.remove('active')
redoImageGrid('0', 'normal')
}
} catch (e) {
console.error(e)
}
} catch (e) {
console.error(e)
}
}
});
}
const deleteSpecificUser = (id, username) => {
Swal.fire({
title: 'Are you sure?',
text: `You are proceeding to delete user ${username} (${id}), you will not be able to recover them!`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: `Yes, delete ${username}.`
}).then(async (result) => {
if (result.value) {
try {
const res = await fetch('/api/user/' + id, {
method: 'DELETE'
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
Swal.fire(
'Deleted!',
`Deleted user ${username} (${id}) successfully.`,
'success'
);
window.location.href = '/'
}
} catch (e) {
console.error(e)
}
} catch (e) {
console.error(e)
}
}
});
}
const saveUser = (id) => {
Swal.fire({
title: 'Are you sure?',
text: "You are proceeding to edit your user.",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save changes!'
}).then(async (result) => {
if (result.value) {
const username = document.getElementById('usernameSave').value;
const password = document.getElementById('passwordSave').value;
if (whitespace(username)) return showAlert('error', 'Please input a username.')
const res = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
payload: 'USER_EDIT',
username,
password
})
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
Swal.fire(
'Saved Changes!',
'Changes were saved successfully!',
'success'
);
window.location.href = '/'
}
} catch (e) {
console.error(e)
}
}
});
};
async function shortURL(token, url) {
if (whitespace(url)) return showAlert('error', 'Please input a URL.')
const res = await fetch('/api/shorten', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'authorization': token
},
body: JSON.stringify({
url
})
});
try {
let te = await res.text();
Swal.fire(
'URL Shortened!',
`Shorten: <a target="_blank" href="${te}">${te}</a>`,
'success'
);
return;
} catch (e) {
if (e.message.startsWith('Unexpected token < in JSON at position')) {
let te = await res.text();
Swal.fire(
'URL Shortened!',
`Shorten: <a target="_blank" href="${te}">${te}</a>`,
'success'
);
return;
} else {
console.error(e)
}
}
};
const copyToken = (token) => {
Swal.fire({
title: 'Are you sure?',
text: "You are proceeding to copy your token, make sure NO ONE sees it.",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3`085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, copy it!'
}).then((result) => {
if (result.value) {
copyText(token);
Swal.fire(
'Copied!',
'Your API Token has been copied.',
'success'
);
}
});
};
function regenToken(id) {
Swal.fire({
title: 'Are you sure?',
text: "You are proceeding to regenerate your token, remember all apps using your current one will stop working.",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, regenerate it!'
}).then(async (result) => {
if (result.value) {
const res = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
payload: 'USER_TOKEN_RESET'
})
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
Swal.fire(
'Regenerated!',
'Your API Token has been regenerated.',
'success'
);
return window.location.href = '/'
}
} catch (e) {
console.error(e)
}
}
});
};
async function createUser() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (whitespace(username)) return showAlert('error', 'Please input a username.')
const res = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
password,
administrator: document.getElementById('administrator').checked
})
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
showAlert('success', `Created user ${json.username} (${json.id})`)
return window.location.href = '/'
}
} catch (e) {
console.error(e)
}
}
document.getElementById('addUser').addEventListener('click', async () => {
if (document.getElementById('administrator').checked) {
Swal.fire({
title: 'Are you sure?',
text: "You are proceeding to create a user with administrator permissions, they can do whatever they want!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, create user!'
}).then(async (result) => {
if (result.value) {
createUser()
}
});
} else {
createUser();
}
})

1
scripts/update.sh Executable file
View File

@@ -0,0 +1 @@
echo "Updating Zipline\n\n\n\n\n" && git pull && tsc -p .

596
src/controllers/APIController.ts Normal file → Executable file
View File

@@ -1,128 +1,564 @@
import { OK, BAD_REQUEST, FORBIDDEN } from 'http-status-codes';
import { Controller, Middleware, Get, Post, Put, Delete, Patch } from '@overnightjs/core';
import { Request, Response } from 'express';
import config from '../../config.json';
import { ORMHandler } from '..';
import { randomId, getUser, getImage } from '../util';
import { createReadStream, createWriteStream, unlinkSync, existsSync, mkdirSync } from 'fs'
import multer from 'multer'
import { getExtension } from 'mime';
import { User } from '../entities/User';
import { sep } from 'path';
import { cookiesForAPI } from '../middleware/cookiesForAPI';
const upload = multer({ dest: config.upload.tempDir });
import { BAD_REQUEST, FORBIDDEN } from "http-status-codes";
import {
Controller,
Middleware,
Get,
Post,
Delete,
Patch,
} from "@overnightjs/core";
import { Request, Response } from "express";
import { ORMHandler } from "..";
import {
randomId,
getImage,
findFile,
getShorten,
hashPassword,
} from "../util";
import {
createReadStream,
createWriteStream,
unlinkSync,
existsSync,
mkdirSync,
readFileSync,
} from "fs";
import { User } from "../entities/User";
import { sep } from "path";
import { cookiesForAPI } from "../middleware/cookiesForAPI";
import { DiscordWebhook } from "../structures/DiscordWebhook";
import { ImageUtil } from "../structures/ImageUtil";
import { ShortenUtil } from "../structures/ShortenUtil";
import { Note } from "../entities/Note";
import Logger from "@ayanaware/logger";
import multer from "multer";
@Controller('api')
if (!findFile("config.json", process.cwd())) {
Logger.get("FS").error(
`No config.json exists in the ${__dirname}, exiting...`
);
process.exit(1);
}
const config = JSON.parse(
readFileSync(findFile("config.json", process.cwd()), "utf8")
);
const upload = multer({ dest: config.uploader.temp });
@Controller("api")
export class APIController {
public orm: ORMHandler;
@Post('upload')
@Middleware(upload.single('file'))
@Post("upload")
@Middleware(upload.single("file"))
private async upload(req: Request, res: Response) {
if (req.headers['authorization'] === config.administrator.authorization) return res.status(BAD_REQUEST).json({ code: BAD_REQUEST, message: "You can't upload files with the administrator account." })
const users = await this.orm.repos.user.find({ where: { token: req.headers['authorization'] } });
if (!users[0]) return res.status(FORBIDDEN).json({ code: FORBIDDEN, message: "Unauthorized" })
if (req.headers['authorization'] !== users[0].token) return res.status(FORBIDDEN).json({ code: FORBIDDEN, message: "Unauthorized" })
const users = await this.orm.repos.user.find({
where: { token: req.headers["authorization"] },
});
if (!users[0])
return res
.status(FORBIDDEN)
.json({ code: FORBIDDEN, message: "Unauthorized" });
if (req.headers["authorization"] !== users[0].token)
return res
.status(FORBIDDEN)
.json({ code: FORBIDDEN, message: "Unauthorized" });
const user = users[0];
const file = req.file;
const id = randomId(config.upload.fileLength);
const extension = getExtension(file.mimetype);
const source = createReadStream(file.path);
if (!existsSync(config.upload.uploadDir)) mkdirSync(config.upload.uploadDir);
const destination = createWriteStream(`${config.upload.uploadDir}${sep}${id}.${extension}`);
const id = randomId(config.uploader.length);
if (
config.uploader.blacklistedExt.includes(
req.file.originalname.split(".").pop()
)
)
return res
.status(BAD_REQUEST)
.json({
code: BAD_REQUEST,
message: "The extension used in this file is blacklisted.",
});
const source = createReadStream(req.file.path);
if (!existsSync(config.uploader.upload)) mkdirSync(config.uploader.upload);
const destination = createWriteStream(
`${config.uploader.upload}${sep}${id}.${req.file.originalname
.split(".")
.pop()}`
);
source.pipe(destination, { end: false });
source.on("end", function () {
unlinkSync(file.path);
unlinkSync(req.file.path);
});
getImage(this.orm, `${req.protocol}://${req.headers['host']}/u/${id}.${extension}`, user.id)
return res.status(200).send(`${req.protocol}://${req.headers['host']}/u/${id}.${extension}`)
const img = await getImage(
this.orm,
`${req.protocol}://${req.headers["host"]}${
config.uploader.route
}/${id}.${req.file.originalname.split(".").pop()}`,
user.id
);
Logger.get("TypeX.Uploader").info(
`New image uploaded ${img.url} (${img.id}) by ${user.username} (${user.id})`
);
if (config.discordWebhook.enabled)
new DiscordWebhook(config.discordWebhook.url).sendImageUpdate(
user,
ImageUtil.parseURL(img.url),
config
);
return res
.status(200)
.send(
`${req.protocol}://${req.headers["host"]}${
config.uploader.route
}/${id}.${req.file.originalname.split(".").pop()}`
);
}
@Post('user')
@Post("shorten")
private async shorten(req: Request, res: Response) {
const users = await this.orm.repos.user.find({
where: { token: req.headers["authorization"] },
});
if (!users[0])
return res
.status(FORBIDDEN)
.json({ code: FORBIDDEN, message: "Unauthorized" });
if (req.headers["authorization"] !== users[0].token)
return res
.status(FORBIDDEN)
.json({ code: FORBIDDEN, message: "Unauthorized" });
const user = users[0];
const id = randomId(config.shortener.length);
const shrt = await getShorten(
this.orm,
id,
req.body.url,
`${req.protocol}://${req.headers["host"]}${config.shortener.route}/${id}`,
user.id
);
Logger.get("TypeX.Shortener").info(
`New url shortened ${shrt.url} (${req.body.url}) (${shrt.id}) by ${user.username} (${user.id})`
);
if (config.discordWebhook.enabled)
new DiscordWebhook(config.discordWebhook.url).sendShortenUpdate(
user,
shrt,
ShortenUtil.parseURL(shrt.url),
config
);
return res
.status(200)
.send(
`${req.protocol}://${req.headers["host"]}${config.shortener.route}/${id}`
);
}
@Post("note")
private async note(req: Request, res: Response) {
const users = await this.orm.repos.user.find({
where: { token: req.headers["authorization"] },
});
if (!users[0])
return res
.status(FORBIDDEN)
.json({ code: FORBIDDEN, message: "Unauthorized" });
if (req.headers["authorization"] !== users[0].token)
return res
.status(FORBIDDEN)
.json({ code: FORBIDDEN, message: "Unauthorized" });
const user = users[0];
const id = randomId(config.notes.length);
const note = await this.orm.repos.note.save(
new Note().set({
key: id,
user: user.id,
content: req.body.content,
expiration: req.body.expiration ? req.body.expiration : null,
})
);
Logger.get("TypeX.Notes").info(
`New note created ${note.id} and ${
note.expriation
? `will expire in ${note.expriation},`
: `will not expire,`
} by ${user.username} (${user.id})`
);
// if (config.discordWebhook.enabled) new DiscordWebhook(config.discordWebhook.url).sendShortenUpdate(user, shrt, ShortenUtil.parseURL(shrt.url), config);
return res
.status(200)
.send(
`${req.protocol}://${req.headers["host"]}${config.notes.route}/${id}`
);
}
@Get("users")
@Middleware(cookiesForAPI)
private async newUser(req: Request, res: Response) {
if (!req.session.user.administrator) return res.status(FORBIDDEN).json({ code: FORBIDDEN, message: 'Unauthorized' });
const data = req.body;
private async getUsers(req: Request, res: Response) {
if (!req.session.user.administrator)
return res
.status(FORBIDDEN)
.json({ code: FORBIDDEN, message: "Unauthorized" });
try {
let user = await this.orm.repos.user.findOne({ username: data.username })
if (user) return res.status(BAD_REQUEST).json({ error: "Could not create user: user exists already" })
user = await this.orm.repos.user.save(new User().set({ username: data.username, password: data.password, administrator: data.administrator }))
return res.status(200).json(user);
let users = await this.orm.repos.user.find({ order: { id: "ASC" } });
return res.status(200).json(users);
} catch (e) {
return res.status(BAD_REQUEST).json({ error: "Could not create user: " + e.message })
return res
.status(BAD_REQUEST)
.json({ error: "Could not create user: " + e.message });
}
}
@Patch('user')
@Post("users")
@Middleware(cookiesForAPI)
private async createUser(req: Request, res: Response) {
if (!req.session.user.administrator)
return res
.status(FORBIDDEN)
.json({ code: FORBIDDEN, message: "Unauthorized" });
const data = req.body;
try {
let user = await this.orm.repos.user.findOne({ username: data.username });
if (user)
return res
.status(BAD_REQUEST)
.json({ error: "Could not create user: user exists already" });
user = await this.orm.repos.user.save(
new User().set({
username: data.username,
password: hashPassword(data.password, config.core.saltRounds),
administrator: data.administrator,
})
);
Logger.get("TypeX.User.Create").info(
`User ${user.username} (${user.id}) was created`
);
return res.status(200).json(user);
} catch (e) {
return res
.status(BAD_REQUEST)
.json({ error: "Could not create user: " + e.message });
}
}
@Post("users/register")
private async registerUser(req: Request, res: Response) {
const data = req.body;
if (!config.core.public)
return res
.status(BAD_REQUEST)
.json({
error:
"This zipline server does not have public enabled, therefore can't create a user.",
});
try {
let user = await this.orm.repos.user.findOne({ username: data.username });
if (user)
return res
.status(BAD_REQUEST)
.json({ error: "Could not create user: user exists already" });
user = await this.orm.repos.user.save(
new User().set({
username: data.username,
password: hashPassword(data.password, config.core.saltRounds),
administrator: false,
})
);
return res.status(200).json({ success: true });
} catch (e) {
return res
.status(BAD_REQUEST)
.json({ error: `Couldn't create user: ${e.message}` });
}
}
@Patch("users/:id")
@Middleware(cookiesForAPI)
private async patchUser(req: Request, res: Response) {
const data = req.body;
try {
let user = await this.orm.repos.user.findOne({ id: req.session.user.id })
if (!user) return res.status(BAD_REQUEST).json({ error: "Could not edit user: user doesnt exist" })
this.orm.repos.user.update({ id: req.session.user.id }, data)
return res.status(200).json(user);
} catch (e) {
return res.status(BAD_REQUEST).json({ error: "Could not edit user: " + e.message })
if (!data.payload)
return res
.status(FORBIDDEN)
.json({ code: BAD_REQUEST, message: "No payload specified." });
if (data.payload === "USER_EDIT") {
if (Number(req.params.id) !== Number(req.session.user.id))
return res
.status(FORBIDDEN)
.json({ code: FORBIDDEN, message: "Unauthorized" });
data.password = hashPassword(data.password, config.core.saltRounds);
try {
let user = await this.orm.repos.user.findOne({
id: Number(req.params.id),
});
if (!user)
return res
.status(BAD_REQUEST)
.json({ error: "Could not edit user: user doesnt exist" });
this.orm.repos.user.update(
{ id: Number(req.params.id) },
{ username: data.username, password: data.password }
);
Logger.get("TypeX.User.Edit").info(
`User ${user.username} (${user.id}) was edited`
);
return res.status(200).json(user);
} catch (e) {
return res
.status(BAD_REQUEST)
.json({ error: "Could not edit user: " + e.message });
}
} else if (data.payload === "USER_RESET_PASSWORD") {
data.password = hashPassword(data.password, config.core.saltRounds);
try {
let user = await this.orm.repos.user.findOne({
id: Number(req.params.id),
});
if (!user)
return res
.status(BAD_REQUEST)
.json({ error: "Could not reset password: user doesnt exist" });
this.orm.repos.user.update(
{ id: Number(req.params.id) },
{ password: data.password }
);
Logger.get("TypeX.User.Edit.ResetPassword").info(
`User ${user.username} (${user.id}) had their password reset.`
);
return res.status(200).json(user);
} catch (e) {
return res
.status(BAD_REQUEST)
.json({ error: "Could not reset password: " + e.message });
}
} else if (data.payload === "USER_TOKEN_RESET") {
try {
let user = await this.orm.repos.user.findOne({
id: req.session.user.id,
});
if (!user)
return res
.status(BAD_REQUEST)
.json({ error: "Could not regen token: user doesnt exist" });
user.token = randomId(config.core.userTokenLength);
req.session.user = user;
await this.orm.repos.user.save(user);
Logger.get("TypeX.User.Token").info(
`User ${user.username} (${user.id}) token was regenerated`
);
return res.status(200).json(user);
} catch (e) {
return res
.status(BAD_REQUEST)
.json({ error: "Could not regen token: " + e.message });
}
} else {
console.log(data);
}
}
@Delete('user')
@Delete("users/:id")
@Middleware(cookiesForAPI)
private async deleteUser(req: Request, res: Response) {
const q = req.query.user;
if (!req.session.user.administrator)
return res
.status(FORBIDDEN)
.json({ code: FORBIDDEN, message: "Unauthorized" });
try {
let user = await this.orm.repos.user.findOne({ id: Number(q) || req.session.user.id })
if (!user) return res.status(BAD_REQUEST).json({ error: "Could not delete user: user doesnt exist" })
this.orm.repos.user.delete({ id: Number(q) || req.session.user.id })
let user = await this.orm.repos.user.findOne({
id: Number(req.params.id),
});
if (!user)
return res
.status(BAD_REQUEST)
.json({ error: "Could not delete user: user doesnt exist" });
this.orm.repos.user.delete({ id: Number(req.params.id) });
Logger.get("TypeX.User.Delete").info(
`User ${user.username} (${user.id}) was deleted`
);
return res.status(200).json(user);
} catch (e) {
return res.status(BAD_REQUEST).json({ error: "Could not delete user: " + e.message })
return res
.status(BAD_REQUEST)
.json({ error: "Could not delete user: " + e.message });
}
}
@Post('token')
@Get("images")
@Middleware(cookiesForAPI)
private async postToken(req: Request, res: Response) {
try {
let user = await this.orm.repos.user.findOne({ id: req.session.user.id })
if (!user) return res.status(BAD_REQUEST).json({ error: "Could not regen token: user doesnt exist" })
user.token = randomId(config.user.tokenLength);
req.session.user = user;
await this.orm.repos.user.save(user);
return res.status(200).json(user);
} catch (e) {
return res.status(BAD_REQUEST).json({ error: "Could not regen token: " + e.message })
}
}
@Get('images/user')
@Middleware(cookiesForAPI)
private async imagesUser(req: Request, res: Response) {
const userId = req.query.user;
const all = await this.orm.repos.image.find({ where: { user: req.session.user.id }, order: { id: 'ASC' } });
private async allImages(req: Request, res: Response) {
const all = await this.orm.repos.image.find({
where: { user: req.session.user.id },
order: { id: "ASC" },
});
return res.status(200).json(all);
}
@Delete('images')
@Get("images/statistics")
@Middleware(cookiesForAPI)
private async statistics(req: Request, res: Response) {
const all = await this.orm.repos.image.find({
where: { user: req.session.user.id },
order: { id: "ASC" },
});
const totalViews =
all.map((i) => i.views).length !== 0
? all.map((i) => i.views).reduce((a, b) => Number(a) + Number(b))
: 0;
const users = await this.orm.repos.user.find();
const images = [];
const views = [];
for (const user of users) {
const i = await this.orm.repos.image.find({
where: { user: user.id },
order: { views: "ASC" },
});
images.push({
username: user.username,
count: i.length,
});
views.push({
username: user.username,
count:
i.map((i) => i.views).length !== 0
? i.map((i) => i.views).reduce((a, b) => Number(a) + Number(b))
: 0,
});
}
return res.status(200).json({
totalViews,
images: all.length,
average: totalViews / all.length,
table: {
images: images.sort((a, b) => b.count - a.count),
views: views.sort((a, b) => b.count - a.count),
},
});
}
@Delete("images/:id")
@Middleware(cookiesForAPI)
private async deleteImage(req: Request, res: Response) {
const img = req.query.image;
try {
let image = await this.orm.repos.image.findOne({ id: Number(img) })
if (!image) return res.status(BAD_REQUEST).json({ error: "Could not delete image: image doesnt exist in database" })
this.orm.repos.image.delete({ id: Number(img) });
let image = await this.orm.repos.image.findOne({
id: Number(req.params.id),
});
if (!image)
return res
.status(BAD_REQUEST)
.json({
error: "Could not delete image: image doesnt exist in database",
});
this.orm.repos.image.delete({ id: Number(req.params.id) });
const url = new URL(image.url);
unlinkSync(`${config.upload.uploadDir}${sep}${url.pathname.slice(3)}`);
unlinkSync(`${config.uploader.upload}${sep}${url.pathname.slice(3)}`);
Logger.get("TypeX.Images.Delete").info(
`Image ${image.url} (${image.id}) was deleted from ${
config.uploader.upload
}${sep}${url.pathname.slice(3)}`
);
return res.status(200).json(image);
} catch (e) {
return res.status(BAD_REQUEST).json({ error: "Could not delete user: " + e.message })
return res
.status(BAD_REQUEST)
.json({ error: "Could not delete image: " + e.message });
}
}
@Get("images/:id")
@Middleware(cookiesForAPI)
private async imagesUser(req: Request, res: Response) {
const all = await this.orm.repos.image.find({
where: { id: req.params.id },
order: { id: "ASC" },
});
return res.status(200).json(all);
}
@Get("images/user/pages")
@Middleware(cookiesForAPI)
private async pagedUser(req: Request, res: Response) {
const all = await this.orm.repos.image.find({
where: { user: req.session.user.id },
order: { id: "ASC" },
});
const paged = [];
const pagedNums = [];
while (all.length) paged.push(all.splice(0, 25));
for (let x = 0; x < paged.length; x++) pagedNums.push(x);
if (!req.query.page) return res.status(200).json({ pagedNums });
else return res.status(200).json({ page: paged[Number(req.query.page)] });
}
@Get("shortens")
@Middleware(cookiesForAPI)
private async allShortens(req: Request, res: Response) {
const all = await this.orm.repos.shorten.find({
where: { user: req.session.user.id },
order: { id: "ASC" },
});
return res.status(200).json(all);
}
@Get("shortens/:id")
@Middleware(cookiesForAPI)
private async getShorten(req: Request, res: Response) {
const all = await this.orm.repos.shorten.find({
where: { user: req.session.user.id, id: Number(req.params.id) },
order: { id: "ASC" },
});
return res.status(200).json(all);
}
@Get("notes")
@Middleware(cookiesForAPI)
private async allNotes(req: Request, res: Response) {
const all = await this.orm.repos.note.find({
where: { user: req.session.user.id },
order: { id: "ASC" },
});
return res.status(200).json(all);
}
@Get("notes/:id")
@Middleware(cookiesForAPI)
private async getNote(req: Request, res: Response) {
const all = await this.orm.repos.note.find({
where: { user: req.session.user.id, id: Number(req.params.id) },
order: { id: "ASC" },
});
return res.status(200).json(all);
}
@Get("stats")
private async getStats(req: Request, res: Response) {
const memory = process.memoryUsage();
const views: number = (await this.orm.repos.image.find())
.map((a) => a.views)
.reduce((a, b) => Number(a) + Number(b), 0);
const clicks: number = (await this.orm.repos.shorten.find())
.map((a) => a.clicks)
.reduce((a, b) => Number(a) + Number(b), 0);
return res.status(200).json({
memory,
uploadedStatistics: {
views,
clicks,
},
count: {
image: await this.orm.repos.image.count(),
note: await this.orm.repos.note.count(),
shorten: await this.orm.repos.shorten.count(),
user: await this.orm.repos.user.count(),
},
zipline: {
version: JSON.parse(
readFileSync(findFile("package.json", process.cwd()), "utf8")
).version,
database: this.orm.connection.options.type,
},
});
}
public set(orm: ORMHandler) {
this.orm = orm;
return this;
}
}
}

129
src/controllers/IndexController.ts Normal file → Executable file
View File

@@ -1,66 +1,117 @@
import { OK, BAD_REQUEST, FORBIDDEN } from 'http-status-codes';
import { Controller, Middleware, Get, Post, Put, Delete } from '@overnightjs/core';
import { Request, Response } from 'express';
import config from '../../config.json';
import { ORMHandler } from '..';
import { randomId, getUser, getImage } from '../util';
import { createReadStream, createWriteStream, unlinkSync, existsSync, mkdirSync } from 'fs'
import multer from 'multer'
import { getExtension } from 'mime';
import { User } from '../entities/User';
import { cookies } from '../middleware/cookies';
const upload = multer({ dest: 'temp/' });
import { Controller, Middleware, Get, Post } from "@overnightjs/core";
import { Request, Response } from "express";
import { ORMHandler } from "..";
import { findFile, checkPassword } from "../util";
import { readFileSync } from "fs";
import { cookies } from "../middleware/cookies";
import Logger from "@ayanaware/logger";
@Controller('')
if (!findFile("config.json", process.cwd())) {
Logger.get("FS").error(
`No config.json exists in the ${__dirname}, exiting...`
);
process.exit(1);
}
const config = JSON.parse(
readFileSync(findFile("config.json", process.cwd()), "utf8")
);
@Controller("")
export class IndexController {
public orm: ORMHandler;
@Get('')
@Get("")
@Middleware(cookies)
private async index(req: Request, res: Response) {
const images = await this.orm.repos.image.find({ where: { user: req.session.user.id } });
const users = await this.orm.repos.user.find({ order: { id: 'ASC' } });
return res.render('index', { user: req.session.user, images, users, config })
const images = await this.orm.repos.image.find({
where: { user: req.session.user.id },
});
const users = await this.orm.repos.user.find({ order: { id: "ASC" } });
const userImages = [];
for (let i = 0; i < users.length; i++) {
userImages[i] = await (
await this.orm.repos.image.find({ where: { user: users[i].id } })
).length;
}
return res.render("index", {
user: req.session.user,
images,
users,
userImages,
config,
});
}
@Get('login')
@Get("login")
private async login(req: Request, res: Response) {
if (req.session.user || req.cookies.typex_user) return res.redirect('/');
return res.status(200).render('login', { failed: false, config })
if (req.session.user || req.cookies.typex_user) return res.redirect("/");
return res.status(200).render("login", { failed: false, config });
}
@Get('logout')
@Get("logout")
private async logout(req: Request, res: Response) {
Logger.get("TypeX.Auth").info(
`User ${req.session.user?.username} (${req.session.user?.id}) logged out`
);
req.session.user = null;
res.clearCookie('typex_user');
res.redirect('/login');
res.clearCookie("typex_user");
res.redirect("/login");
}
@Post('login')
@Post("login")
private async postLogin(req: Request, res: Response) {
if (req.session.user || req.cookies.typex_user) return res.redirect('/');
if (req.body.username == 'administrator' && req.body.password === config.administrator.password) {
if (req.session.user || req.cookies.typex_user) return res.redirect("/");
if (
req.body.username === "administrator" &&
req.body.password === config.core.adminPassword
) {
//@ts-ignore
req.session.user = {
id: 0,
username: 'administrator',
password: config.administrator.password,
token: config.administrator.authorization,
administrator: true
}
res.cookie('typex_user', req.session.user.id, { maxAge: 1036800000 });
return res.redirect('/')
username: "administrator",
password: config.core.adminPassword,
administrator: true,
};
res.cookie("typex_user", req.session.user.id, { maxAge: 1036800000 });
Logger.get("TypeX.Auth").info(
`Administrator has logged in from IP ${
req.headers["x-forwarded-for"] || req.connection.remoteAddress
}`
);
return res.redirect("/");
}
const user = await this.orm.repos.user.findOne({ where: { username: req.body.username } });
if (!user) return res.status(200).render('login', { failed: true, config })
if (req.body.password !== user.password) return res.status(200).render('login', { failed: true, config })
const user = await this.orm.repos.user.findOne({
where: { username: req.body.username },
});
if (!user) return res.status(200).render("login", { failed: true, config });
if (!checkPassword(req.body.password, user.password))
return res.status(200).render("login", { failed: true, config });
req.session.user = user;
res.cookie('typex_user', req.session.user.id, { maxAge: 1036800000 });
return res.redirect('/')
res.cookie("typex_user", req.session.user.id, { maxAge: 1036800000 });
return res.redirect("/");
}
@Get(`${config.shortener.route.slice(1)}/:id`)
private async getShorten(req: Request, res: Response) {
const shorten = await this.orm.repos.shorten.findOne({
key: req.params.id,
});
if (!shorten) return res.render("404");
shorten.clicks++;
this.orm.repos.shorten.save(shorten);
return res.redirect(shorten.origin);
}
@Get(`${config.notes.route.slice(1)}/:id`)
private async getNote(req: Request, res: Response) {
const note = await this.orm.repos.note.findOne({ key: req.params.id });
if (!note) return res.render("404");
return res.send(note.content);
}
public set(orm: ORMHandler) {
this.orm = orm;
return this;
}
}
}

22
src/core/Console.ts Normal file → Executable file
View File

@@ -1,12 +1,16 @@
import Logger, {ConsoleTransport, DefaultFormatter} from "@ayanaware/logger";
import {ConsoleFormatter} from "../structures/ConsoleFormatter";
import Logger, { ConsoleTransport, DefaultFormatter } from "@ayanaware/logger";
import { ConsoleFormatter } from "../structures/ConsoleFormatter";
Logger.setFormatter(new DefaultFormatter({
disableDefaultColors: true
}));
Logger.setFormatter(
new DefaultFormatter({
disableDefaultColors: true,
})
);
Logger.addTransport(new ConsoleTransport({
formatter: new ConsoleFormatter()
}));
Logger.addTransport(
new ConsoleTransport({
formatter: new ConsoleFormatter(),
})
);
Logger.disableDefaultTransport();
Logger.disableDefaultTransport();

30
src/entities/Image.ts Normal file → Executable file
View File

@@ -1,19 +1,23 @@
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Image {
@PrimaryGeneratedColumn({ type: 'bigint' })
id: number;
@PrimaryGeneratedColumn({ type: "bigint" })
id: number;
@Column("text")
url: string;
@Column("text")
url: string;
@Column("bigint")
user: number;
@Column("bigint")
user: number;
set(options: { url: string, user: number }) {
this.url = options.url;
this.user = options.user;
return this;
}
}
@Column("bigint", { default: 0 })
views: number;
set(options: { url: string; user: number }) {
this.url = options.url;
this.user = options.user;
this.views = 0;
return this;
}
}

36
src/entities/Note.ts Executable file
View File

@@ -0,0 +1,36 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Note {
@PrimaryGeneratedColumn({ type: "bigint" })
id: number;
@Column("bigint")
user: number;
@Column("text")
key: string;
@Column("bigint")
creation: number;
@Column("bigint", { nullable: true, default: null })
expriation: number;
@Column("text")
content: string;
set(options: {
user: number;
key: string;
content: string;
expiration?: number;
}) {
this.user = options.user;
this.key = options.key;
this.content = options.content;
this.creation = Date.now();
this.expriation = options.expiration ? options.expiration : null;
return this;
}
}

35
src/entities/Shorten.ts Executable file
View File

@@ -0,0 +1,35 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Shorten {
@PrimaryGeneratedColumn({ type: "bigint" })
id: number;
@Column("text")
origin: string;
@Column("text")
url: string;
@Column("text", { default: "" })
key: string;
@Column("bigint")
user: number;
@Column("bigint", { default: 0 })
views: number;
@Column("bigint", { default: 0 })
clicks: number;
set(options: { key: string; origin: string; url: string; user: number }) {
this.key = options.key;
this.origin = options.origin;
this.url = options.url;
this.user = options.user;
this.views = 0;
this.clicks = 0;
return this;
}
}

26
src/entities/User.ts Normal file → Executable file
View File

@@ -1,10 +1,22 @@
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
import { randomId } from "../util";
import config from '../../config.json';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { randomId, findFile } from "../util";
import Logger from "@ayanaware/logger";
import { readFileSync } from "fs";
if (!findFile("config.json", process.cwd())) {
Logger.get("FS").error(
`No config.json exists in the ${__dirname}, exiting...`
);
process.exit(1);
}
const config = JSON.parse(
readFileSync(findFile("config.json", process.cwd()), "utf8")
);
@Entity()
export class User {
@PrimaryGeneratedColumn({ type: 'bigint' })
@PrimaryGeneratedColumn({ type: "bigint" })
id: number;
@Column("text")
@@ -19,11 +31,11 @@ export class User {
@Column("boolean")
administrator: boolean;
set(options: { username: string, password: string, administrator: boolean }) {
set(options: { username: string; password: string; administrator: boolean }) {
this.username = options.username;
this.password = options.password;
this.administrator = options.administrator;
this.token = randomId(config.user.tokenLength)
this.token = randomId(config.core.userTokenLength);
return this;
}
}
}

67
src/index.ts Normal file → Executable file
View File

@@ -1,19 +1,39 @@
import "./core/Console";
import {
Repository,
Connection,
createConnection,
ConnectionOptions,
} from "typeorm";
import { Repository, Connection, createConnection } from "typeorm";
import { User } from "./entities/User";
import { TypeXServer } from "./server";
import config from "../config.json";
import { ZiplineServer } from "./server";
import Logger from "@ayanaware/logger";
import { Image } from "./entities/Image";
import { findFile } from "./util";
import { readFileSync } from "fs";
import { Shorten } from "./entities/Shorten";
import { Note } from "./entities/Note";
import { notes } from "./interval";
import { GitHub } from "./structures/GitHub";
import { compare } from "semver";
import chalk from "chalk";
if (!findFile("config.json", process.cwd())) {
Logger.get("FS").error(`No config.json exists in ${__dirname}, exiting...`);
process.exit(1);
}
const config = JSON.parse(
readFileSync(findFile("config.json", process.cwd()), "utf8")
);
if (!config.uploader?.route) {
Logger.get("Zipline.Config").error(
`Missing needed property on configuration: upload.route`
);
process.exit(1);
}
export interface ORMRepos {
user?: Repository<User>;
image?: Repository<Image>;
shorten?: Repository<Shorten>;
note?: Repository<Note>;
}
export interface ORMHandler {
@@ -21,19 +41,44 @@ export interface ORMHandler {
connection: Connection;
}
const pk = JSON.parse(
readFileSync(findFile("package.json", process.cwd()), "utf8")
);
(async () => {
const connection = await createConnection(config.orm as ConnectionOptions);
if (
compare(
JSON.parse(await GitHub.getFile("package.json")).version,
pk.version
) == 1
)
Logger.get(`Zipline`).info(
`Zipline is ${chalk.bold.redBright(
"outdated"
)}, you should run ${chalk.bold.whiteBright(
"./scripts/update.sh"
)} to get the best features.`
);
Logger.get("Zipline").info(`Starting Zipline ${pk.version}`);
if (!config.database)
return Logger.get("Config").error("Database is not found in config.");
const connection = await createConnection(config.database);
const orm: ORMHandler = {
connection,
repos: {
user: connection.getRepository(User),
image: connection.getRepository(Image),
shorten: connection.getRepository(Shorten),
note: connection.getRepository(Note),
},
};
if (orm.connection.isConnected)
Logger.get(Connection).info(
`Successfully initialized database type: ${config.orm.type}`
`Successfully initialized database type: ${config.database.type}`
);
const server = new TypeXServer(orm);
const server = new ZiplineServer(orm);
server.start();
Logger.get("Interval").info("Starting Notes interval");
notes(orm);
})();

23
src/interval.ts Executable file
View File

@@ -0,0 +1,23 @@
import { ORMHandler } from ".";
import Logger from "@ayanaware/logger";
export function notes(orm: ORMHandler) {
return setInterval(async () => {
const all = await orm.repos.note.find();
for (const note of all) {
if (note.expriation) {
const expiration = Number(note.creation) + Number(note.expriation);
if (Date.now() > expiration) {
orm.repos.note.delete({ id: note.id });
Logger.get("TypeX.Notes").info(
`Note deleted ${note.id} and ${
note.expriation
? `expired in ${note.expriation}`
: `never expired`
}`
);
}
}
}
}, 5000);
}

57
src/middleware/cookies.ts Normal file → Executable file
View File

@@ -1,24 +1,41 @@
import Logger from "@ayanaware/logger";
import { Request, Response } from "express";
import config from '../../config.json';
import { getConnection } from 'typeorm';
import { getConnection } from "typeorm";
import { User } from "../entities/User";
import { findFile } from "../util";
import { readFileSync } from "fs";
if (!findFile("config.json", process.cwd())) {
Logger.get("FS").error(`No config.json exists in ${__dirname}, exiting...`);
process.exit(1);
}
const config = JSON.parse(
readFileSync(findFile("config.json", process.cwd()), "utf8")
);
export async function cookies(req: Request, res: Response, next: any) {
if (req.cookies.typex_user) {
if (typeof req.cookies.typex_user !== 'string') return res.send('Please clear your browser cookies and refresh this page.')
if (req.cookies.typex_user === 0) req.session.user = {
id: 0,
username: 'administrator',
password: config.administrator.password,
token: config.administrator.authorization,
administrator: true
}
else req.session.user = await getConnection().getRepository(User).findOne({ id: req.cookies.typex_user });
if (!req.session.user) {
res.clearCookie('typex_user');
req.session.user = null;
return res.redirect('/login')
}
} else return res.redirect('/login');
return next();
}
if (req.cookies.typex_user) {
if (typeof req.cookies.typex_user !== "string")
return res.send(
"Please clear your browser cookies and refresh this page."
);
if (Number(req.cookies.typex_user) === 0) {
req.session.user = {
id: 0,
username: "administrator",
password: config.core.adminPassword,
administrator: true,
};
} else
req.session.user = await getConnection()
.getRepository(User)
.findOne({ id: req.cookies.typex_user });
if (!req.session.user) {
res.clearCookie("typex_user");
req.session.user = null;
return res.redirect("/login");
}
} else return res.redirect("/login");
return next();
}

64
src/middleware/cookiesForAPI.ts Normal file → Executable file
View File

@@ -1,21 +1,51 @@
import Logger from "@ayanaware/logger";
import { Request, Response } from "express";
import config from '../../config.json';
import { BAD_REQUEST, INTERNAL_SERVER_ERROR, FORBIDDEN } from 'http-status-codes'
import { getConnection } from 'typeorm';
import {
BAD_REQUEST,
INTERNAL_SERVER_ERROR,
FORBIDDEN,
} from "http-status-codes";
import { getConnection } from "typeorm";
import { User } from "../entities/User";
import { findFile } from "../util";
import { readFileSync } from "fs";
if (!findFile("config.json", process.cwd())) {
Logger.get("FS").error(`No config.json exists in ${__dirname}, exiting...`);
process.exit(1);
}
const config = JSON.parse(
readFileSync(findFile("config.json", process.cwd()), "utf8")
);
export async function cookiesForAPI(req: Request, res: Response, next: any) {
if (req.cookies.typex_user) {
if (typeof req.cookies.typex_user !== 'string') return res.status(BAD_REQUEST).send({ code: BAD_REQUEST, message: "Please clear browser cookies." })
if (req.cookies.typex_user === 0) req.session.user = {
id: 0,
username: 'administrator',
password: config.administrator.password,
token: config.administrator.authorization,
administrator: true
}
else req.session.user = await getConnection().getRepository(User).findOne({ id: req.cookies.typex_user });
if (!req.session.user) return res.status(INTERNAL_SERVER_ERROR).send({ code: INTERNAL_SERVER_ERROR, message: "The user that is logged in does not exist" })
} else return res.status(FORBIDDEN).send({ code: FORBIDDEN, message: "Unauthorized" })
return next();
}
if (req.cookies.typex_user) {
if (typeof req.cookies.typex_user !== "string")
return res
.status(BAD_REQUEST)
.send({ code: BAD_REQUEST, message: "Please clear browser cookies." });
if (Number(req.cookies.typex_user) === 0)
req.session.user = {
id: 0,
username: "administrator",
password: config.core.adminPassword,
administrator: true,
};
else
req.session.user = await getConnection()
.getRepository(User)
.findOne({ id: req.cookies.typex_user });
if (!req.session.user)
return res
.status(INTERNAL_SERVER_ERROR)
.send({
code: INTERNAL_SERVER_ERROR,
message: "The user that is logged in does not exist",
});
} else
return res
.status(FORBIDDEN)
.send({ code: FORBIDDEN, message: "Unauthorized" });
return next();
}

103
src/server.ts Normal file → Executable file
View File

@@ -1,9 +1,7 @@
import * as bodyParser from "body-parser";
import { Server } from "@overnightjs/core";
import { Connection } from "typeorm";
import { ORMHandler } from ".";
import Logger from "@ayanaware/logger";
import config from "../config.json";
import * as express from "express";
import * as http from "http";
import * as https from "https";
@@ -12,23 +10,78 @@ import session from "express-session";
import cookies from "cookie-parser";
import { APIController } from "./controllers/APIController";
import { IndexController } from "./controllers/IndexController";
import { findFile } from "./util";
export class TypeXServer extends Server {
if (!findFile("config.json", process.cwd())) {
Logger.get("FS").error(
`No config.json exists in the ${__dirname}, exiting...`
);
process.exit(1);
}
const config = JSON.parse(
fs.readFileSync(findFile("config.json", process.cwd()), "utf8")
);
export class ZiplineServer extends Server {
constructor(orm: ORMHandler) {
super();
var p = "loopback";
if (config.core.trustedProxy) {
p += "," + config.core.trustedProxy;
}
this.app.set("trust proxy", p);
this.app.set("view engine", "ejs");
this.app.use(
session({
secret: config.sessionSecret,
secret: config.core.sessionSecret,
resave: false,
saveUninitialized: false,
})
);
this.app.use(async (req, res, next) => {
if (!req.url.startsWith(config.uploader.route)) return next();
const upload = await orm.repos.image.findOne({
url: `${config.secure ? "https" : "http"}://${req.headers["host"]}${
req.url
}`,
});
if (!upload) return next();
upload.views++;
orm.repos.image.save(upload);
return next();
});
this.app.use(cookies());
this.app.set("view engine", "ejs");
this.app.use("/u", express.static("uploads"));
try {
this.app.use(
config.uploader.route,
express.static(config.uploader.upload)
);
} catch (e) {
Logger.get("TypeX.Routes").error(
`Could not formulate upload static route`
);
process.exit(1);
}
this.app.use("/public", express.static("public"));
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({ extended: true }));
this.app.use(async (req, res, next) => {
if (!config.core.log) return next();
if (req.url.startsWith(config.uploader.route)) return next();
let user = req.session.user;
const users = await orm.repos.user.find({
where: { token: req.headers["authorization"] },
});
if (users[0]) user = users[0];
Logger.get("TypeX.Route").info(
`Route ${req.url} was accessed by ${
user ? user.username : "<no user found>"
}`
);
return next();
});
this.setupControllers(orm);
}
@@ -36,30 +89,46 @@ export class TypeXServer extends Server {
const api = new APIController().set(orm);
const index = new IndexController().set(orm);
super.addControllers([index, api]);
this.app.get("*", (req, res) => {
return res.status(200).render("404");
});
this.app.use((err, req, res, next) => {
Logger.get(this.app).error(err);
return res.status(500).render("error");
});
}
public start(): void {
// this.app.listen(port, () => {
// Logger.get(TypeXServer).info('Started server on port ' + port);
// })
let server;
if (config.site.protocol === "https") {
if (config.core.secure) {
try {
const creds = {
key: fs.readFileSync(config.site.ssl.key, "utf-8"),
cert: fs.readFileSync(config.site.ssl.cert, "utf-8")
key: fs.readFileSync(config.core.ssl.key, "utf-8"),
cert: fs.readFileSync(config.core.ssl.cert, "utf-8"),
};
server = https.createServer(creds, this.app);
} catch (e) {
if (e.code === 'ENOENT') {
Logger.get('TypeXServer.FS').error(`No file/directory found for ${e.path}`);
if (e.code === "ENOENT") {
Logger.get("ZiplineServer.FS").error(
`No file/directory found for ${e.path}`
);
process.exit(1);
}
}
} else server = http.createServer(this.app);
server.listen(config.site.protocol === 'https' ? config.site.serveHTTPS : config.site.serveHTTP, () => {
Logger.get(TypeXServer).info('Started server on port ' + String(config.site.protocol === 'https' ? config.site.serveHTTPS : config.site.serveHTTP));
})
server.listen(
config.core.secure ? config.core.port.secure : config.core.port.unsecure,
() => {
Logger.get(ZiplineServer).info(
"Started server on port " +
String(
config.core.secure
? config.core.port.secure
: config.core.port.unsecure
)
);
}
);
}
}

24
src/structures/ConsoleFormatter.ts Normal file → Executable file
View File

@@ -1,5 +1,5 @@
import { Formatter, LogLevel, LogMeta } from "@ayanaware/logger";
import chalk from 'chalk';
import chalk from "chalk";
export class ConsoleFormatter extends Formatter {
public formatError(meta: Readonly<LogMeta>, error: Error): string {
@@ -7,31 +7,33 @@ export class ConsoleFormatter extends Formatter {
}
public formatMessage(meta: Readonly<LogMeta>, message: string): string {
return `${this.formatTimestamp()} ${this.formatLevel(meta.level)} ${this.formatName(meta.origin.name)}: ${message}`
return `${this.formatTimestamp()} ${this.formatLevel(
meta.level
)} ${this.formatName(meta.origin.name)}: ${message}`;
}
public formatName(name: string): string {
return `[${chalk.greenBright(name)}]`
return `${chalk.greenBright(name)}`;
}
public formatTimestamp(): string {
return new Date().toLocaleString().split(', ').join(' ');
return new Date().toLocaleString().split(", ").join(" ");
}
public formatLevel(level: LogLevel): string {
switch (level) {
case LogLevel.DEBUG:
return `${chalk.yellowBright('debug')}`;
return `${chalk.yellowBright("debug")}`;
case LogLevel.ERROR:
return `${chalk.redBright('err')} `;
return `${chalk.redBright("err")} `;
case LogLevel.INFO:
return `${chalk.blue('info')} `;
return `${chalk.blue("info")} `;
case LogLevel.OFF:
return `${chalk.white('off')} `;
return `${chalk.white("off")} `;
case LogLevel.TRACE:
return `${chalk.magenta('trace')} `;
return `${chalk.magenta("trace")} `;
case LogLevel.WARN:
return `${chalk.yellow('warn')} `;
return `${chalk.yellow("warn")} `;
}
}
}
}

View File

@@ -0,0 +1,56 @@
import req from "centra";
import { User } from "../entities/User";
import { Shorten } from "../entities/Shorten";
import { Imaged } from "./ImageUtil";
import { Shortened } from "./ShortenUtil";
export class DiscordWebhook {
public url: string;
constructor(url: string) {
this.url = url;
this.checkExists();
}
async checkExists(): Promise<boolean> {
const json = await (await req(this.url).send()).json();
if (json.code === 10015) throw new Error("Unknown Webhook");
else if (json.code === 50027) throw new Error("Invalid Webhook Token");
else if (json.code)
throw new Error(`DiscordAPIError[${json.code}]: ${json.message}`);
return json.code ? false : true;
}
async sendImageUpdate(user: User, image: Imaged, config: any) {
const res = await req(this.url, "POST")
.header({
"Content-Type": "application/json",
})
.body({
user: config.discordWebhook.username,
avatar_url: config.discordWebhook.avatarURL,
content: `New image uploaded to <${image.origin}> by ${user.username} (${user.id}). ${image.url})`,
})
.send();
if (res.statusCode !== 200)
throw new Error(`Couldn't send webhook. (Status: ${res.statusCode})`);
}
async sendShortenUpdate(
user: User,
shorten: Shorten,
ex: Shortened,
config: any
) {
const res = await req(this.url, "POST")
.header({
"Content-Type": "application/json",
})
.body({
user: config.discordWebhook.username,
avatar_url: config.discordWebhook.avatarURL,
content: `New shortened url added to <${ex.origin}> by ${user.username} (${user.id}). <${shorten.origin}> -> <${shorten.url}>`,
})
.send();
if (res.statusCode !== 200)
throw new Error(`Couldn't send webhook. (Status: ${res.statusCode})`);
}
}

10
src/structures/GitHub.ts Executable file
View File

@@ -0,0 +1,10 @@
import req from "centra";
export class GitHub {
public static async getFile(filePath: string) {
const res = await req(
`https://raw.githubusercontent.com/ZiplineProject/Zipline/master/${filePath}`
).send();
return res.text();
}
}

44
src/structures/ImageUtil.ts Executable file
View File

@@ -0,0 +1,44 @@
import { getType } from "mime";
import { findFile } from "../util";
import Logger from "@ayanaware/logger";
import { readFileSync } from "fs";
if (!findFile("config.json", process.cwd())) {
Logger.get("FS").error(
`No config.json exists in the ${__dirname}, exiting...`
);
process.exit(1);
}
const config = JSON.parse(
readFileSync(findFile("config.json", process.cwd()), "utf8")
);
export interface Imaged {
url: string;
origin: string;
protocol: string;
key: string;
extension: string;
mime: string;
}
export class ImageUtil {
static parseURL(url: string): Imaged {
const parsed = new URL(url);
return {
url: parsed.href,
origin: parsed.origin,
protocol: parsed.protocol.slice(0, -1),
key: parsed.pathname.startsWith(config.upload.route)
? parsed.pathname.slice(3).split(".")[0]
: null,
extension: parsed.pathname.startsWith(config.upload.route)
? parsed.pathname.slice(3).split(".")[1]
: null,
mime: parsed.pathname.startsWith(config.upload.route)
? getType(parsed.pathname.slice(3).split(".")[1])
: null,
};
}
}

44
src/structures/ShortenUtil.ts Executable file
View File

@@ -0,0 +1,44 @@
import { getType } from "mime";
import { findFile } from "../util";
import Logger from "@ayanaware/logger";
import { readFileSync } from "fs";
if (!findFile("config.json", process.cwd())) {
Logger.get("FS").error(
`No config.json exists in the ${__dirname}, exiting...`
);
process.exit(1);
}
const config = JSON.parse(
readFileSync(findFile("config.json", process.cwd()), "utf8")
);
export interface Shortened {
url: string;
origin: string;
protocol: string;
key: string;
extension: string;
mime: string;
}
export class ShortenUtil {
static parseURL(url: string): Shortened {
const parsed = new URL(url);
return {
url: parsed.href,
origin: parsed.origin,
protocol: parsed.protocol.slice(0, -1),
key: parsed.pathname.startsWith(config.shorten.route)
? parsed.pathname.slice(3).split(".")[0]
: null,
extension: parsed.pathname.startsWith(config.shorten.route)
? parsed.pathname.slice(3).split(".")[1]
: null,
mime: parsed.pathname.startsWith(config.shorten.route)
? getType(parsed.pathname.slice(3).split(".")[1])
: null,
};
}
}

86
src/util.ts Normal file → Executable file
View File

@@ -1,33 +1,41 @@
import { Request, Response } from 'express';
import { ORMHandler } from '.';
import { User } from './entities/User';
import { Image } from './entities/Image';
import bcrypt from "bcrypt";
import { ORMHandler } from ".";
import { User } from "./entities/User";
import { Image } from "./entities/Image";
import { statSync, readdirSync } from "fs";
import { join, basename } from "path";
import { Shorten } from "./entities/Shorten";
export function randomId(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
for (var i = 0; i < length; i++)
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
export function renderTemplate(res, req, template, data = {}) {
const baseData = {
path: req.path,
user: req.isAuthenticated() ? req.user : null,
auth: req.isAuthenticated()
auth: req.isAuthenticated(),
};
res.render(template, Object.assign(baseData, data));
}
export async function getUser(orm: ORMHandler, username: string, password: string, administrator: boolean = false) {
export async function getUser(
orm: ORMHandler,
username: string,
password: string,
administrator: boolean = false
) {
const user = await orm.repos.user.findOne({ username });
if (!user) return orm.repos.user.save(new User().set({ username, password, administrator }));
if (!user)
return orm.repos.user.save(
new User().set({ username, password, administrator })
);
return user;
}
@@ -35,4 +43,52 @@ export async function getImage(orm: ORMHandler, url: string, user: number) {
const image = await orm.repos.image.findOne({ url, user });
if (!image) return orm.repos.image.save(new Image().set({ url, user }));
return image;
}
}
export async function getShorten(
orm: ORMHandler,
key: string,
origin: string,
url: string,
user: number
) {
const image = await orm.repos.shorten.findOne({ key });
if (!image)
return orm.repos.shorten.save(
new Shorten().set({ key, origin, url, user })
);
return image;
}
export function findFile(file, directory) {
const result = [];
(function read(dir) {
const files = readdirSync(dir);
for (const file of files) {
const filepath = join(dir, file);
if (
file !== ".git" &&
file !== "node_modules" &&
statSync(filepath).isDirectory()
) {
read(filepath);
} else {
result.push(filepath);
}
}
})(directory);
for (const f of result) {
const base = basename(f);
if (base === file) return f;
}
return null;
}
export function checkPassword(pass: string, hash: string): boolean {
return bcrypt.compareSync(pass, hash);
}
export function hashPassword(pass: string, saltRounds: number): string {
// console.log(bcrypt.hashSync(pass,saltRounds));
return bcrypt.hashSync(pass, saltRounds);
}

26
tsconfig.json Normal file → Executable file
View File

@@ -1,16 +1,12 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"module": "commonjs",
"target": "esnext",
"resolveJsonModule": true,
"outDir": "./out",
"esModuleInterop": true
},
"exclude": [
"node_modules"
],
"include": [
"src"
]
}
"compilerOptions": {
"experimentalDecorators": true,
"module": "commonjs",
"target": "esnext",
"resolveJsonModule": true,
"outDir": "./out",
"esModuleInterop": true
},
"exclude": ["node_modules"],
"include": ["src"]
}

23
views/404.ejs Executable file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include('./partials/head') %>
<meta property="og:title" content="Not Found" />
<meta name="theme-color" content="#09122B" />
<meta property="og:description" content="This page does not exist...">
<title>Not Found</title>
</head>
<body>
<div id="particles-js"></div>
<div class="container h-100 d-flex justify-content-center">
<div class="jumbotron my-auto">
<h1 class="display-4" style="text-align: center;">404 - Not Found</h1>
<h5 style="text-align: center;">Looks like you hit a dead end... Return <a href="/"
style="color: #036ffc;">home</a></p>
</div>
</div>
</body>
</html>

22
views/error.ejs Executable file
View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include('./partials/head') %>
<meta property="og:title" content="Something went wrong..." />
<meta name="theme-color" content="#09122B" />
<meta property="og:description" content="Something happened!">
<title>Something went wrong...</title>
</head>
<body>
<div class="container h-100 d-flex justify-content-center">
<div class="jumbotron my-auto">
<h1 class="display-4" style="text-align: center;">500 - Internal Server Error</h1>
<h5 style="text-align: center;">Looks like something went wrong when processing your request... Return <a
href="/" style="color: #036ffc;">home</a></p>
</div>
</div>
</body>
</html>

639
views/index.ejs Normal file → Executable file
View File

@@ -2,440 +2,257 @@
<html lang="en">
<head>
<style>
body,
html {
height: 100%;
}
.typex-image-actions {
display: inline-block;
position: relative;
}
.typex-image-action {
position: absolute;
bottom: 0;
}
/*
#copyToken {
transition: backgroundColor .3s;
} */
</style>
<%- include('./partials/head') %>
<link rel="icon" href="<%=config.meta.favicon%>" type="image/png">
<title><%= config.meta.title %> - Dashboard</title>
</head>
<body>
<div id="particles-js"></div>
<div class="container h-100 d-flex justify-content-center">
<div class="jumbotron my-auto">
<ul class="nav nav-pills mb-3" id="main-view-tab-list" role="tablist">
<li class="nav-item">
<a style="border-radius: 50px;" class="nav-link active" id="home-tab" data-toggle="tab" href="#home"
role="tab" aria-controls="home" aria-selected="true">Home</a>
</li>
<li class="nav-item" id="updateImages">
<a style="border-radius: 50px;" class="nav-link" id="profile-tab" data-toggle="tab" href="#images"
role="tab" aria-controls="images" aria-selected="false">Your
Images</a>
</li>
<% if (user.administrator) { %>
<li class="nav-item">
<a style="border-radius: 50px;" class="nav-link" id="contact-tab" data-toggle="tab" href="#users"
role="tab" aria-controls="users" aria-selected="false">Users</a>
</li>
<% } %>
</ul>
<div class="tab-content" id="main-view">
<div class="tab-pane fade show active m-3" id="home" role="tabpanel" aria-labelledby="home-tab">
<h2>Welcome back, <%= user.username %> <a class="btn btn-sm btn-danger" href="/logout"
style="border-radius: 50px;">Logout</a>
</h2>
<p>You have <b><%= images.length %></b> images saved, and you
<%= user.administrator ? 'are an administrator' : 'are not an administrator' %>.</p>
<h4>API Token</h4>
<button type="button" class="btn btn-sm btn-primary" id="copyToken"
style="border-radius: 50px;">Copy</button>
<button type="button" class="btn btn-sm btn-danger" id="regenToken"
style="border-radius: 50px;">Regenerate</button>
<h4 style="margin-top:12px">Update your Profile</h4>
<form>
<div class="form-group">
<label for="usernameSave">Username</label>
<input value="<%= user.username %>" type="text" class="form-control" id="usernameSave"
placeholder="Username">
</div>
<div class="text-light">
<input type="radio" id="home-tab" name="tabs" class="tab-locator" hidden checked />
<input type="radio" id="stats-tab" name="tabs" class="tab-locator" hidden />
<input type="radio" id="urls-tab" name="tabs" class="tab-locator" hidden />
<input type="radio" id="notes-tab" name="tabs" class="tab-locator" hidden />
<input type="radio" id="images-tab" name="tabs" class="tab-locator" hidden />
<% if (user.administrator) { %>
<input type="radio" id="users-tab" name="tabs" class="tab-locator" hidden />
<% } %>
<ul class="tab tab-block" style="border:none;">
<li class="tab-item text-light"><label for="home-tab"><a><i class="icon icon-apps"></i> Home</a></label>
</li>
<li class="tab-item text-light" id="updateStatistics"><label for="stats-tab"><a><i
class="icon icon-message"></i> Statistics</a></label></li>
<li class="tab-item text-light" id="updateShortens"><label for="urls-tab"><a><i class="icon icon-link"></i>
URLS</a></label></li>
<li class="tab-item text-light" id="updateNotes"><label for="notes-tab"><a><i class="icon icon-copy"></i>
Notes</a></label></li>
<li class="tab-item text-light" id="updateImages"><label for="images-tab"><a><i class="icon icon-photo"></i>
Images</a></label></li>
<% if (user.administrator) { %>
<li class="tab-item text-light"><label for="users-tab"><a><i class="icon icon-people"></i> Users</a></label>
</li>
<% } %>
</ul>
<div class="form-group">
<label for="passwordSave">Password</label>
<input value="<%= user.password %>" type="password" class="form-control" id="passwordSave"
placeholder="Password">
</div>
<button type="button" class="btn btn-primary" id="saveUser"
style="border-radius: 50px; width:100%;">Save</button>
</form>
</div>
<div class="tab-pane fade" id="images" role="tabpanel" aria-labelledby="images-tab">
<h3>Your Images</h3>
<div class="container-fluid pt-2">
<div class="card-columns" id="typexImages">
</div>
<div class="tabs">
<div class="tab-content" style="margin: 50px;">
<div class="card bg-dark" style="border-color: #303742;">
<div class="card-header">
<div class="card-title h5">Welcome back, <%=user.username%>!</div>
<% if (user.username === "administrator") { %>
<div class="card-subtitle text-gray"><b>This account is not designed for general usage, only to
create a user for uploading, and other administrator duties.</b></div>
<% } else { %>
<div class="card-subtitle text-gray">You have <b><%=images.length%></b> images.</div>
<% } %>
</div>
<div class="card-body">
<% if (user.username !== "administrator") { %>
<div class="card-title h5">Token</div>
<div class="container">
<div class="columns">
<div class="column col-6">
<button style="margin-top:12px;width:100%" type="button"
class="btn btn-primary input-group-btn" id="copyToken"
onclick="copyToken('<%=user.token%>')">Copy Token</button>
</div>
<div class="column col-6">
<button style="margin-top:12px;width:100%" type="button"
class="btn btn-primary input-group-btn" id="regenToken"
onclick="regenToken('<%=user.id%>')">Regen Token</button>
</div>
</div>
</div>
<div class="card-title h5">Update your profile</div>
<form>
<label class="form-label" for="usernameSave">Username</label>
<input class="form-input" type="text" id="usernameSave" name="username"
placeholder="Username">
<label class="form-label" for="passwordSave">Password</label>
<input class="form-input" type="password" id="passwordSave" name="password"
placeholder="Password">
</form>
<% } %>
</div>
<div class="card-footer">
<% if (user.username === "administrator") { %>
<a href="/logout" style="margin-top:12px;width:100%" type="button"
class="btn btn-error input-group-btn">Logout</a>
<% } else { %>
<div class="container">
<div class="columns">
<div class="column col-6">
<button style="margin-top:12px;width:100%" type="button"
class="btn btn-primary input-group-btn"
onclick="saveUser('<%=user.id%>')">Update</button>
</div>
<div class="column col-6">
<a href="/logout" style="margin-top:12px;width:100%" type="button"
class="btn btn-error input-group-btn">Logout</a>
</div>
</div>
</div>
<% } %>
</div>
</div>
<% if (user.administrator) { %>
<div class="tab-pane fade m-3" id="users" role="tabpanel" aria-labelledby="users-tab">
<table class="table">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Username</th>
<th scope="col">Administrator</th>
</tr>
</thead>
<tbody>
<% users.forEach(u => { %>
<tr>
<th scope="row"><%= u.id %></th>
<td><%= u.username %></td>
<% if (u.administrator) { %>
<td class="text-success">Yes</td>
<% } else { %>
<td class="text-danger">No</td>
<% } %>
<td>
<% if (!u.administrator) { %>
<button type="button" class="btn btn-sm btn-danger"
style="border-radius: 50px;">Delete</button>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
<button style="border-radius: 50px;" type="button" class="btn btn-primary" data-toggle="modal"
data-target="#modal">Create new
User</button>
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">New User</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="tab-content" style="margin:50px;">
<h3>Statistics</h3>
<h5 id="statsDescription"></h5>
<div class="container">
<div class="columns">
<div class="column col-6">
<div class="card bg-dark" style="border-color: #303742; margin:12px;">
<div class="card-header">
<div class="card-title h5">Leaderboard (Image)</div>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username"
placeholder="Username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password"
placeholder="Password">
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="administrator">
<label class="custom-control-label"
for="administrator">Administrator?</label>
</div>
</div>
</form>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Username</th>
<th scope="col">Images</th>
</tr>
</thead>
<tbody id="statsLeaderboardImages">
</tbody>
</table>
</div>
<div class="modal-footer">
<div class="row" style="width:100%;">
<div class="col-sm-6">
<button type="button" id="createUser" class="btn btn-primary"
style="border-radius: 50px; width:100%;">Create</button>
</div>
<div class="col-sm-6">
<button type="button" class="btn btn-danger" data-dismiss="modal"
style="border-radius: 50px; width:100%;">Cancel</button>
</div>
</div>
</div>
</div>
<div class="column col-6">
<div class="card bg-dark" style="border-color: #303742; margin:12px;">
<div class="card-header">
<div class="card-title h5">Leaderboard (Views)</div>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Username</th>
<th scope="col">Views</th>
</tr>
</thead>
<tbody id="statsLeaderboardImageViews">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<% } %>
</div>
<div class="tab-content" style="margin:50px;">
<h3>Shorten a URL</h3>
<form>
<div class="form-group">
<label class="form-label" for="urlToShorten">Name</label>
<input class="form-input" type="text" id="urlToShorten" placeholder="URL">
</div>
<button type="button" class="btn btn-primary" id="shortenURL"
onclick="shortURL('<%=user.token%>', document.getElementById('urlToShorten').value)"
style="width:100%;">Shorten</button>
</form>
<h3 style="margin-top:5px;">Your Shortens</h3>
<table class="table" style="margin:12px;">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Origin</th>
<th scope="col">Shortened</th>
</tr>
</thead>
<tbody id="shortensTableShortens">
</tbody>
</table>
</div>
<div class="tab-content" style="margin:50px;">
<h3>Create a Note</h3>
<p>Comming Soon!</p>
</div>
<div class="tab-content" style="margin:50px;">
<h3>Your Images</h3>
<div id="emptyImages"></div>
<div class="container" style="padding:12px;">
<div class="container">
<div id="typexImages" class="columns">
</div>
</div>
</div>
<ul class="pagination" style="justify-content: center;margin:12px;" id="typexImagePagination">
</ul>
</div>
<div class="tab-content" style="margin:50px;">
<div class="container">
<button class="btn btn-primary"
onclick="document.getElementById('modal-create-user').classList.add('active')">Create new
user</button>
<div class="modal" id="modal-create-user">
<a href="#close" class="modal-overlay" aria-label="Close"></a>
<div class="modal-container bg-dark">
<div class="modal-header">
<a href="#close" class="btn btn-clear float-right" aria-label="Close"></a>
<div class="modal-title text-light h5">Create new user</div>
</div>
<div class="modal-body">
<div class="content">
<form>
<label class="form-label" for="username">Username</label>
<input class="form-input" type="text" id="username" name="username"
placeholder="Username">
<label class="form-label" for="password">Password</label>
<input class="form-input" type="password" id="password" name="password"
placeholder="Password">
<label class="form-switch">
<input type="checkbox" id="administrator">
<i class="form-icon"></i> Administrator
</label>
</form>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="addUser" type="button">Create</button>
</div>
</div>
</div>
<div class="columns">
<% for (let b = 0; b < users.length; b++) { let u = users[b]; let imgs = userImages[b]; %>
<div class="column col-4">
<div class="card bg-dark" style="border-color: #303742; margin:12px;">
<div class="card-header">
<div class="card-title h5"><%=u.username%> <button class="btn btn-error btn-sm"
onclick="deleteSpecificUser('<%=u.id%>', '<%=u.username%>')">Delete</button>
</div>
<div class="card-subtitle text-gray">
<%=u.administrator ? 'Administrator' : 'User'%>
</div>
</div>
<div class="card-body">
<b>ID: </b> <%=u.id%><br>
<b>Images: </b> <%=imgs%>
</div>
</div>
</div>
<%}%>
</div>
</div>
</div>
</div>
<p style="display:none" id="partenabled"><%=config.particles.enabled%></p>
<p style="display:none" id="part"><%=JSON.stringify(config.particles.settings) %></p>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<script src="//cdn.jsdelivr.net/npm/sweetalert2@9/dist/sweetalert2.min.js"></script>
<script src="http://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
<script>if (document.getElementById('partenabled').innerText === "true") particlesJS("particles-js", JSON.parse(document.getElementById('part').innerText));</script>
<script src="/public/js/dashboard.js"></script>
<script>
function whitespace(str) {
return str === null || str.match(/^ *$/) !== null;
}
function showAlert(type, message) {
if (type === 'error') {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: message,
footer: 'Try again later...'
})
} else if (type === 'success') {
Swal.fire({
icon: 'success',
title: 'Success',
text: message,
footer: 'You did it!'
})
}
}
document.getElementById('updateImages').addEventListener('click', async () => {
document.getElementById('typexImages').innerHTML = '';
const res = await fetch('/api/images/user', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
json.forEach(image => {
$('#typexImages').append(`
<div class="card typex-image-actions typex-image-buttons"
style="background-color: transparent;">
<img class="card-img-top" src="${image.url}" onclick="window.location.href='${image.url}'"/>
<input disabled type="button" class="typex-image-action btn btn-sm"
value="${image.id}" />
</div>
`)
});
}
} catch (e) {
console.error(e)
}
});
const copyText = (text) => {
const el = document.createElement('textarea'); el.value = text; document.body.appendChild(el); el.select(); document.execCommand('copy'); document.body.removeChild(el);
}
const deleteImage = (id, url) => {
Swal.fire({
title: 'Are you sure?',
text: `You are proceeding to delete image (${id}), you will not be able to recover it!`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, delete it.'
}).then(async (result) => {
if (result.value) {
const username = document.getElementById('usernameSave').value;
const password = document.getElementById('passwordSave').value;
if (whitespace(username)) return showAlert('error', 'Please input a username.')
const res = await fetch('/api/user', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
password
})
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
const res = await fetch('/api/images?image=' + id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
Swal.fire(
'Deleted!',
`Deleted image (${id}) successfully.`,
'success'
);
}
} catch (e) {
console.error(e)
}
}
} catch (e) {
console.error(e)
}
}
});
}
document.getElementById('saveUser').addEventListener('click', async () => {
Swal.fire({
title: 'Are you sure?',
text: "You are proceeding to edit your user.",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save changes!'
}).then(async (result) => {
if (result.value) {
const username = document.getElementById('usernameSave').value;
const password = document.getElementById('passwordSave').value;
if (whitespace(username)) return showAlert('error', 'Please input a username.')
const res = await fetch('/api/user', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
password
})
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
Swal.fire(
'Saved Changes!',
'Changes were saved successfully!',
'success'
);
}
} catch (e) {
console.error(e)
}
}
});
});
document.getElementById('copyToken').addEventListener('click', async () => {
Swal.fire({
title: 'Are you sure?',
text: "You are proceeding to copy your token, make sure NO ONE sees it.",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, copy it!'
}).then((result) => {
if (result.value) {
copyText("<%= user.token %>");
Swal.fire(
'Copied!',
'Your API Token has been copied.',
'success'
);
}
});
});
document.getElementById('regenToken').addEventListener('click', async () => {
Swal.fire({
title: 'Are you sure?',
text: "You are proceeding to regenerate your token, remember all apps using your current one will stop working.",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, regenerate it!'
}).then(async (result) => {
if (result.value) {
const res = await fetch('/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
Swal.fire(
'Regenerated!',
'Your API Token has been regenerated.',
'success'
);
return window.location.href = '/'
}
} catch (e) {
console.error(e)
}
}
});
});
async function createUser() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (whitespace(username)) return showAlert('error', 'Please input a username.')
const res = await fetch('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
password,
administrator: document.getElementById('administrator').checked
})
});
try {
const json = await res.json();
if (json.error || json.code) return showAlert('error', json.error)
else {
$('#modal').modal('toggle');
showAlert('success', `Created user ${json.username} (${json.id})`)
return window.location.href = '/'
}
} catch (e) {
console.error(e)
}
}
document.getElementById('createUser').addEventListener('click', async () => {
if (document.getElementById('administrator').checked) {
Swal.fire({
title: 'Are you sure?',
text: "You are proceeding to create a user with administrator permissions, they can do whatever they want!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, create user!'
}).then(async (result) => {
if (result.value) {
createUser()
}
});
} else {
createUser();
}
})
document.getElementById('createUserButton').addEventListener("click", ()
=> { document.getElementById("modal-create-user").classList.add("active") })
</script>
</body>

53
views/login.ejs Normal file → Executable file
View File

@@ -2,49 +2,38 @@
<html lang="en">
<head>
<style>
body,
html {
height: 100%;
}
</style>
<%- include('./partials/head') %>
<link rel="icon" href="<%=config.meta.favicon%>" type="image/png">
<title><%= config.meta.title %> - Login</title>
</head>
<body>
<div id="particles-js"></div>
<div class="container h-100 d-flex justify-content-center">
<div class="jumbotron my-auto">
<h1 class="display-4" style="text-align: center;">Login</h1>
<form action="/login" method="post">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Your Username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password"
placeholder="Your Password">
</div>
<button type="submit" class="btn btn-primary" style="border-radius: 50px; width: 100%;">Login</button>
</form>
<div style="display:flex;align-items:center;justify-content:center;height:100%">
<div class="card bg-dark" style="border-color: #303742;">
<div class="card-header">
<div class="card-title h5">Login</div>
<div class="card-subtitle text-gray">Please enter in your username and password.</div>
</div>
<div class="card-body">
<form action="/login" method="post">
<label class="form-label" for="username">Username</label>
<input class="form-input" type="text" id="username" name="username" placeholder="Username">
<label class="form-label" for="username">Password</label>
<input class="form-input" type="password" id="password" name="password" placeholder="Password">
<button style="margin-top:12px;width:100%" type="submit"
class="btn btn-primary input-group-btn">Login</button>
<% if (config.core.public) { %>
<button style="margin-top:12px;width:100%" type="submit"
class="btn btn-primary input-group-btn" href="/register">Register (Beta)</button>
<% } %>
</form>
</div>
</div>
</div>
<p style="display:none" id="part"><%=JSON.stringify(config.particles.settings) %></p>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<script src="//cdn.jsdelivr.net/npm/sweetalert2@9/dist/sweetalert2.min.js"></script>
<script src="http://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
<script>if ("<%=config.particles.enabled%>" === "true") particlesJS("particles-js", JSON.parse(document.getElementById('part').innerText));</script>
<script>
if ("<%=failed%>" === 'true') {
Swal.fire({

43
views/partials/head.ejs Normal file → Executable file
View File

@@ -1,5 +1,42 @@
<meta charset="UTF-8">
<link rel="stylesheet" href="/public/css/bootstrap-dark.min.css" />
<link rel="stylesheet" href="/public/css/particles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/public/css/spectre.css" />
<link rel="stylesheet" href="/public/css/spectre-exp.css" />
<link rel="stylesheet" href="/public/css/spectre-icons.css" />
<link href="//cdn.jsdelivr.net/npm/@sweetalert2/theme-dark@3/dark.css" rel="stylesheet">
<link rel="icon" href="<%=config.meta.favicon%>" type="image/png">
<script src="https://kit.fontawesome.com/583a38381a.js" crossorigin="anonymous"></script>
<style>
body,
html {
height: 100%;
background: #21212c
}
.tab-item>label:hover {
cursor: pointer;
}
.tab-content {
display: none;
}
.tab-locator:nth-of-type(1):checked~.tab-block>.tab-item:nth-of-type(1)>label>a,
.tab-locator:nth-of-type(2):checked~.tab-block>.tab-item:nth-of-type(2)>label>a,
.tab-locator:nth-of-type(3):checked~.tab-block>.tab-item:nth-of-type(3)>label>a,
.tab-locator:nth-of-type(4):checked~.tab-block>.tab-item:nth-of-type(4)>label>a,
.tab-locator:nth-of-type(5):checked~.tab-block>.tab-item:nth-of-type(5)>label>a,
.tab-locator:nth-of-type(6):checked~.tab-block>.tab-item:nth-of-type(6)>label>a {
border-bottom-color: #5764c6;
color: #5764c6;
}
.tab-locator:nth-of-type(1):checked~.tabs>.tab-content:nth-of-type(1),
.tab-locator:nth-of-type(2):checked~.tabs>.tab-content:nth-of-type(2),
.tab-locator:nth-of-type(3):checked~.tabs>.tab-content:nth-of-type(3),
.tab-locator:nth-of-type(4):checked~.tabs>.tab-content:nth-of-type(4),
.tab-locator:nth-of-type(5):checked~.tabs>.tab-content:nth-of-type(5),
.tab-locator:nth-of-type(6):checked~.tabs>.tab-content:nth-of-type(6) {
display: block;
}
</style>

5
views/partials/meta.ejs Executable file
View File

@@ -0,0 +1,5 @@
<meta property="og:title" content="Zipline" />
<meta name="theme-color" content="#09122B" />
<meta property="og:description" content="Zipline is image/file uploading service.">
<meta property="og:image"
content="https://raw.githubusercontent.com/zipline-project/zipline/master/public/assets/typex_small_circle.png" />