## Update Info

🍆 **DICK** v1.1.0 🍆

Our number is getting bigger! Though, I hear size is not what matters
but how you use it. So, I am happy to introduce you to a bunch of new
stuff within the front end!

🔀 **UPDATING**

Updating your instance should be easy, unless you already edited the
code base, at that point your on your own.

If you have a direct clone of the master of the current 1.0.2(old
master) branch, then all you need to do is
1. Browse to your DICK folder
2. Run `git pull` to pull new changes
3. Run `npm i` to install new dependancies
4. Delete the `dist` folder
5. Start DICK using `npm start`
6. Enjoy

> **Note**
> If you load your instance and styling seems wrong, please clear your
browsers cache, and reload the page.

> **Note**
> The first user to log into your DICK WebUI will be marked as the
instance admin. You can change which users are admin by editing the user
database file located at `/src/database/users.json`. This file will only
appear once you've started your instance for the first time.

✏️ **CHANGELOG**

```diff
ADDITIONS
+ Admin Dashboard
  > This page will be where system administrators can view their syatem settings and stats!
+ Database
  > Added DICK database, inside JSON files with management utils.
+ Added new app settings page to Admin Dashboard
  > This will allow administrators to customize their instance on the fly without having to edit the codebase. White labeling!
+ Added user list to Admin Dashboard
  > This allows administrators to view which users are registered in their ASS currently, and their roles set. You can also create new users from this page. (There are a lot of hidden divs in this page so imstance admins can add extra code to dick to enable stuff like deletion of users)
+ Registrations
  > Administrators can toggle registrations into their ASS from their DICK UI via the /register page!
+ Captcha
  > By default, when a user gets login information wrong they will be forwarded to a Rick Roll. Now you can add a hCaptcha site key to DICK to enable hCaptcha for your login and register public pages!
+ Added a "default profile picture"
  > Every users default profile picture. This is planned to be able to be set per user in the future, so users can pick their own seperate from the default.

REMOVALS
- Removeed STAFF_IDs from codebase.
  > This means you can remove this CONSTANT from your instance CONSTANTS file, please see the repo's constants example file to see if yours matches it.
```
```fix
CHANGES
= Large codebase cleanup
= Seperated js for components into their own files based on job
= Redid some naming for tailwind colour theme classes to provide proper theming from the tailwind config file
= Cleaned up a lot of the utils
= Added embed gen page as a hidden extra for devs to add themselves in their own time if they wish (please PR if you achieve this 🤘)
= Fixed the flash message warning colours to actually be red or green depending on error/success
```

## Issues Resolved / Fixes In This Release

Resolves #17 , Resolves #14 , Resolves #10 , Resolves #7
pull/28/head v1.1
Facinorous 1 year ago committed by GitHub
parent b15442a49b
commit 2334a5e33c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

6
.gitignore vendored

@ -14,7 +14,8 @@ logs
results
tmp
# Constants
# Constants & Database
src/constants
src/constants.ts
# Dependency directory
@ -28,4 +29,5 @@ Thumbs.db
dist/**/*
package-lock.json
yarn.lock
yarn.lock

@ -49,16 +49,18 @@
</details>
## About The Project
Dick was created to be an easy to use front end for <a href="https://github.com/tycrek/ass">Ass</a> as there was no public option to allow users to view their saved images on their server. I decided to learn tailwind and also at the same time build this. I did this all in my spare time, and will keep updating as best as I can while I use it. I'm still learning all this so if anyone has suggestions on how to do things better, I am all ears! I love learning! :)
**Current Feautres:**
1. General statistics on your file uploads
2. File browser, allows you to see all your uploads on one webpage (plans to make it more powerful)
3. Deletion of items (currently one at a time, plans for multiple at once)
4. Copy link of items (currently one at a time, plans for multiple with spaces in between in pastebin)
2. File browser, allows you to see all your uploads on one webpage
3. Deletion of items
4. Copy link of items
5. Customize DICK (completely white label)
6. Register new ASS users
7. hCaptcha on login and register pages <a href="https://www.hcaptcha.com/">You can learn more here</a>.
**Planned Feautres:**
1. There are a lot of good ideas out there, to keep track of what is currently planned see <a href="https://github.com/Facinorous-420/dick/projects/2">the v1.1 project board</a>
@ -72,10 +74,11 @@ The back end is written in <a href="https://www.typescriptlang.org/">Typescript<
Running DICK is very simple, though there is no docker container.<br/>
You must have `Node >=v16.14.0`, which you should if you're running ASS.<br/>
**NOTE:** DICK requires you to use JSON for ASS' data storage method.
### Config
Inside of your dick root folder, you will see `src/CONSTANTS.ts.example`. Copy this to `CONSTANTS.ts`<br/>
Inside of your dick root folder, you will see `src/constants.ts.example`. Copy this to `constants.ts`<br/>
Inside this file, is some basic configuration you can change for your set up. There are only 5 variables you need to worry about in this file:
| Variable | Description |
@ -84,9 +87,11 @@ Inside this file, is some basic configuration you can change for your set up. Th
| `ASS_LOCATION = "../ass"` | If running DICK seperately, DICK will use this to find your ASS install folder |
| `ASS_SECURE = false` | Put this to true if you are running ASS behind a domain with HTTPS,. false if HTTP |
| `ASSDOMAIN = "127.0.0.1:40115"` | Put this to your ASS domain. Can be an ip, or domain for example `https://cdn.mydomain.com` |
| `STAFF_IDS = ["ass"]` | Change this to whatever your username is in your ASS `auth.json` file. Default user in ASS, is `ass` |
| `PORT = "3000"` | Change this number to the port you wish DICK to run on |
> **Note**
> If you want to set a user as admin, currently you must do it via the database file generated at `/src/database/users.json` and change the users role from `user` to `admin`. By default, the first user to login to your dick instance will be admin.
### Running
#### Development
@ -100,12 +105,10 @@ Inside this file, is some basic configuration you can change for your set up. Th
2. Install, and run ASS https://github.com/tycrek/ass#installation (This will create an `ass` folder)
3. Go back into the folder you created and clone this repo `git clone https://github.com/Facinorous-420/dick`
4. Go into the newly created `dick` folder `cd dick`
5. Go into `/src` and copy `CONSTANTS.ts.example` to `CONSTANTS.ts` and edit it as needed
5. Go into `/src` and copy `constants.ts.example` to `constants.ts` and edit it as needed
6. Go back to the root of `dick` and install the dependancies for the frontend, `npm i`
7. Run `npm run build:dev` to compile the code base in watch mode
8. In a new terminal, run `npm run serve:dev` to run DICK using nodemon
:warning:```ASS will be running under it's port of 40115 whereas the dashboard will be under the port 3000.```<br/>
</details>
#### Production
@ -118,49 +121,37 @@ Inside this file, is some basic configuration you can change for your set up. Th
2. Install, and run ASS https://github.com/tycrek/ass#installation (This will create an `ass` folder)
3. Go back into the folder you created and clone this repo `git clone https://github.com/Facinorous-420/dick.git`
4. Go into the newly created `dick` folder `cd dick`
5. Go into `/src` and copy `CONSTANTS.ts.example` to `CONSTANTS.ts` and edit it as needed
5. Go into `/src` and copy `constants.ts.example` to `constants.ts` and edit it as needed
6. Go back to the root of `dick` and install the dependancies for the frontend, `npm i`
7. Run `npm start` to compile the code base and run DICK
</details>
When you approach the login screen, your secret key is the key generated for your account. You should not share this with anyone.
When you approach the login screen, the login information is your ASS username, and the secret key generated by ASS is your password.
The first user to login will be added to the instance admin list.
:warning:```ASS and the dashboard will be under their own ports.```<br/>
<sub> They will have entirely different routing. This means you can use two different domains for each, such as `cdn.yourdomain.com` for ASS and `dashboard.yourdomain.com` for DICK. </sub>
> **Note**
> ```ASS and the dashboard will be under their own ports.```<br/>
> <sub> They will have entirely different routing, and ports. ASS will be running under it's port of `40115` whereas the dashboard will be under the port `3000`. This means you can use two different domains for each, such as `cdn.yourdomain.com` for ASS and `dashboard.yourdomain.com` for DICK. </sub>
<details>
<summary><sub>Open to view the set up steps to run this as a submodule to ass</sub></summary><br/>
### Customizing
**Preface:** You need to edit `/src/CONSTANTS.ts`'s variable of `DICK_SUBMODULE` to `true`
1. Setup ASS https://github.com/tycrek/ass#installation
For when it asks for name of front end, leave as `ass-x` (default) for now.
2. Add this repo as a submodule into ASS `git submodule add https://github.com/Facinorous-420/dick`
3. Go into the frontend's directory, `cd dick`, and run `git submodule update --init --recursive` to initiaze it
4. Go into `/src` and copy `CONSTANTS.ts.example` to `CONSTANTS.ts` and edit it as needed
5. Install the dependancies for the frontend, `npm i`
6. Run `npm run build` to compile the frontend and get it ready to run
7. Then move to the ASS directory and run the ASS setup again `npm run setup`
8. Leave everything as you did prior, but this time under `frontend name`, type `dick` and continue
9. Go into the `.gitmodules` file, and youll notice two submodules. Remove the
```
[submodule "ass-x"]
path = ass-x
url = git@github.com:tycrek/ass-x.git
```
submodule so only the
```
[submodule "dick"]
path = dick
url = https://github.com/Facinorous-420/dick
```
one is left
10. Run `npm run build` to recompile this change
11. You can run ASS, `npm start` or however you normally run your ass instance
</details>
App settings ware available through the admin page. This is where a user can whitelabel their DICK instance.
| Variable | Description |
| --------------------------------------------- | :---------------------: |
| `App Name` | This will replace all the **DICK** occurrences around the app |
| `App Emoji` | This will allow you to change the emoji you see around the instance, by default its an eggplant. 🍆 |
| `Site Title` | This is the text that shows up in browser tabs, as well as the title for the embed when you link the DICK dashboard **NOT** to be confused with ASS' picture embeds. |
| `Site Description` | This text is the description text on the embed when you link the DICK dashboard **NOT** to be confused with ASS' picture embeds. |
| `Login Text` | This text is the text on the login screen, above the form. |
| `hCaptcha Enabled` | If enabled, your instance will use hCaptcha for the login and register pages, if disabled it will only send failed logins to a Rick Roll. |
| `hCaptcha Site ID` | This is the hcaptcha site id if you plan to use hCaptcha as captcha to protect your DICK instance. |
| `hCaptcha Secret Key` | This is the hcaptcha secret key found in your hcaptcha account if you plan to use hCaptcha as captcha to protect your DICK instance. |
| `Private Mode` | If set, this will hide the global instance stats shown on the login page. |
| `Registrations Enabled` | If set, this will allow people to use the register page to create new accounts. |
| `App Logo` | This is the app image shown various places in DICK. |
| `Default Profile Picture` | This is the default image set as users profile pictures. |
## Contributing
@ -172,11 +163,3 @@ When you approach the login screen, your secret key is the key generated for you
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## Contact
| Developer | Job |
| --------------------------------------------- | :---------------------: |
| [Facinorous](https://github.com/facinorous-420) | Lead |
| [Sublime](https://github.com/senpaiSubby)#4233 | Helping hand, my sensei, created the back end |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@ -1,6 +1,6 @@
{
"name": "dick",
"version": "1.0.2",
"version": "1.1.0",
"description": "A frontend for ASS",
"main": "./dist/dashboard.js",
"repository": {
@ -13,50 +13,56 @@
"start": "npm run build && npm run serve",
"build": "tsc && mix --production && postcss ./src/public/css/tailwind.css -o ./dist/public/css/app.css",
"build:dev": "concurrently --kill-others \"tsc -w\" \"mix watch\" \"tailwindcss build -i ./src/public/css/tailwind.css -o ./dist/public/css/app.css --watch\" ",
"serve": "node dist/dashboard.js",
"serve": "cross-env NODE_ENV=production node dist/dashboard.js",
"serve:dev": "nodemon dist/dashboard.js"
},
"dependencies": {
"@callmekory/logger": "^1.1.1",
"async": "^3.2.3",
"bcrypt": "^5.0.1",
"async": "^3.2.4",
"bcrypt": "^5.1.0",
"compression": "^1.7.4",
"connect-flash": "^0.1.1",
"cookie-parser": "^1.4.6",
"cookie-session": "^2.0.0",
"ejs": "^3.1.6",
"ejs": "^3.1.8",
"errorhandler": "^1.5.1",
"express": "^4.17.3",
"lucide": "^0.17.13",
"express": "^4.18.2",
"fs-extra": "^10.1.0",
"lucide": "^0.101.0",
"multer": "^1.4.5-lts.1",
"node-fetch": "^2.6.7",
"passport": "^0.5.2",
"passport-local": "^1.0.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.0",
"@types/async": "^3.2.12",
"@tailwindcss/forms": "^0.5.3",
"@types/async": "^3.2.15",
"@types/body-parser": "^1.19.2",
"@types/compression": "^1.7.2",
"@types/connect-flash": "^0.0.37",
"@types/cookie-session": "^2.0.44",
"@types/errorhandler": "^1.5.0",
"@types/express": "^4.17.13",
"@types/express": "^4.17.14",
"@types/express-busboy": "^8.0.0",
"@types/express-session": "^1.17.4",
"@types/express-session": "^1.17.5",
"@types/fs-extra": "^9.0.13",
"@types/node": "^17.0.22",
"@types/passport": "^1.0.7",
"@types/multer": "^1.4.7",
"@types/node": "^18.11.9",
"@types/node-fetch": "^2.6.2",
"@types/passport": "^1.0.11",
"@types/passport-local": "^1.0.34",
"@types/request": "^2.48.8",
"autoprefixer": "^10.4.4",
"concurrently": "^7.1.0",
"cssnano": "5.1.5",
"laravel-mix": "^6.0.43",
"nodemon": "^2.0.15",
"autoprefixer": "^10.4.13",
"concurrently": "^7.5.0",
"cross-env": "^7.0.3",
"cssnano": "5.1.14",
"laravel-mix": "^6.0.49",
"nodemon": "^2.0.20",
"postcss-advanced-variables": "^3.0.1",
"postcss-cli": "^9.1.0",
"tailwindcss": "^3.0.23",
"ts-node": "^10.7.0",
"postcss-cli": "^10.0.0",
"tailwindcss": "^3.2.4",
"ts-node": "^10.9.1",
"tslint": "^5.20.1",
"typescript": "^4.6.2"
"typescript": "^4.9.3"
}
}

@ -1,10 +1,12 @@
import { Response } from "express"
import { parseAuthFile, parseDataFile } from "./utils/assJSONStructure"
import { RenderOptions } from "./typings/Pager"
import { ASS_DOMAIN, ASS_SECURE, STAFF_IDS } from "./constants"
import { convertTimestamp, convertToPaginatedArray, formatSize } from "./utils/utils"
import { ASSUser, ASSItem } from "./typings/ASSTypes"
import { ASS_DOMAIN, ASS_SECURE } from "./constants"
import { convertTimestamp, convertToPaginatedArray, formatSize, } from "./utils/utils"
import { getSettingsDatabase, getUserDatabase, getUserDatabaseObj } from "./utils/database"
import { ASSUser, ASSItem } from "./typings/ASS"
import { IExtendedRequest } from "./typings/express-ext"
import { ISettingsDatabase, IUsersDatabase, IUserSettings } from "./typings/database"
export class Pager {
/**
@ -21,26 +23,29 @@ export class Pager {
const data: Array<ASSItem> = parseDataFile()
const users: Array<ASSUser> = parseAuthFile()
const dickUsers: IUsersDatabase = getUserDatabase()
const database: ISettingsDatabase = getSettingsDatabase()
// If user is already authenticated load the authenticated data
if (req.isAuthenticated()) {
return this.renderAuthenticatedData(res, req, template, options, data, users)
return this.renderAuthenticatedData(res, req, template, options, data, users, database, dickUsers)
}
// If user is not authenticated only load guest data
return this.renderUnauthenticatedData(res, req, template, options, data, users)
return this.renderUnauthenticatedData(res, req, template, options, data, users, database)
}
/**
* Renders templates for unauthenticated templates
*/
static async renderUnauthenticatedData(
static async renderUnauthenticatedData(
res: Response,
req: IExtendedRequest,
template: string,
options: RenderOptions,
data: Array<ASSItem>,
users: Array<ASSUser>
users: Array<ASSUser>,
settingsDatabase: ISettingsDatabase
) {
const totalUsers = users.length
const totalData = data.length
@ -49,6 +54,7 @@ export class Pager {
const baseData = {
params: options.params,
path: req.path,
settingsDatabase,
totalUsers,
totalData,
totalSize
@ -68,38 +74,56 @@ export class Pager {
template: string,
options: RenderOptions,
data: Array<ASSItem>,
users: Array<ASSUser>
users: Array<ASSUser>,
settingsDatabase: ISettingsDatabase,
dickUsers: IUsersDatabase
) {
// * -------------------- BUILD DATA OBJECT FOR FRONTEND EJS VARIABLES ------------
const totalUsers = users.length
const allUsers = []
for (const user of users) {
allUsers.push(`${user.username}`)
}
const user: IUserSettings = getUserDatabaseObj(req.user.username)
const totalData = data.length
const totalSize = formatSize(data.map(item => item.size).reduce((prev, curr) => prev + curr, 0))
const hasRole = STAFF_IDS.indexOf(req.user.username) > -1
const hasRole = user.role == "admin" ? true : false
// Get all the specific users file information, using secret key to match
const usersData = data.filter(item => item.owner == req.user.password).map((item) => ({
...item,
...item,
timestamp: convertTimestamp(item.timestamp)
})).sort((a, b) => {
let da = new Date(a.timestamp),
db = new Date(b.timestamp)
return db.getTime() - da.getTime()
db = new Date(b.timestamp)
return db.getTime() - da.getTime()
})
// I feel like this could be done better, but I created an object filled with useful variables for the user data to be rendered on the pages
const appDataObj = {
allImages: data.filter(item => item.type.includes('image')),
allVideos: data.filter(item => item.type.includes('video')),
allAudio: data.filter(item => item.type.includes('audio')),
allOthers: data.filter(item => item.type.includes('other')),
totalSize: formatSize(data.map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalImageSize: formatSize(data.filter(item => item.type.includes('image')).map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalVideosSize: formatSize(data.filter(item => item.type.includes('video')).map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalAudioSize: formatSize(data.filter(item => item.type.includes('audio')).map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalOthersSize: formatSize(data.filter(item => item.type.includes('other')).map(item => item.size).reduce((prev, curr) => prev + curr, 0))
}
const usersDataObj = {
data: convertToPaginatedArray(usersData,50),
data: convertToPaginatedArray(usersData, 50),
totalFiles: usersData.length,
allImages: usersData.filter(item=> item.type.includes('image')),
allVideos: usersData.filter(item=> item.type.includes('video')),
allAudio: usersData.filter(item=> item.type.includes('audio')),
allOthers: usersData.filter(item=> item.type.includes('other')),
totalSize:formatSize(usersData.map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalImageSize: formatSize(usersData.filter(item=> item.type.includes('image')).map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalVideosSize: formatSize(usersData.filter(item=> item.type.includes('video')).map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalAudioSize: formatSize(usersData.filter(item=> item.type.includes('audio')).map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalOthersSize: formatSize(usersData.filter(item=> item.type.includes('other')).map(item => item.size).reduce((prev, curr) => prev + curr, 0))
allImages: usersData.filter(item => item.type.includes('image')),
allVideos: usersData.filter(item => item.type.includes('video')),
allAudio: usersData.filter(item => item.type.includes('audio')),
allOthers: usersData.filter(item => item.type.includes('other')),
totalSize: formatSize(usersData.map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalImageSize: formatSize(usersData.filter(item => item.type.includes('image')).map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalVideosSize: formatSize(usersData.filter(item => item.type.includes('video')).map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalAudioSize: formatSize(usersData.filter(item => item.type.includes('audio')).map(item => item.size).reduce((prev, curr) => prev + curr, 0)),
totalOthersSize: formatSize(usersData.filter(item => item.type.includes('other')).map(item => item.size).reduce((prev, curr) => prev + curr, 0))
}
let targetDataObj = {}
// If the user is staff and is trying to view another users information, we will grab it here to render that as well..
/*
@ -119,19 +143,23 @@ export class Pager {
}
}
*/
const baseData = {
assDomain: ASS_DOMAIN,
assSecure: ASS_SECURE,
user: req.user,
totalSize,
totalUsers,
totalData,
usersDataObj,
//targetDataObj: options.params.userID ? targetDataObj : null,
params: options.params,
path: req.path,
hasRole
assDomain: ASS_DOMAIN,
assSecure: ASS_SECURE,
user: req.user,
settingsDatabase,
totalSize,
totalUsers,
allUsers,
dickUsers,
totalData,
usersDataObj,
appDataObj,
//targetDataObj: options.params.userID ? targetDataObj : null,
params: options.params,
path: req.path,
hasRole
}
return res.render(template, Object.assign(baseData, options))

@ -1,6 +1,6 @@
import { templatePathBuilder } from "./utils/utils"
/* ------------------- ASS HOST INFORMATION ---------------------------- */
/* ------------------- ASS HOST INFORMATION ------------------------ */
// Boolean variable if your ASS is secured with HTTPS
// true = https | false = http
export const ASS_SECURE = false
@ -8,19 +8,11 @@ export const ASS_SECURE = false
// This is the domain your ASS is hosted at: "127.0.0.1" | "cdn.domain.com"
export const ASS_DOMAIN = "127.0.0.1:40115"
// If running DICK in separate mode, it will need to know where your ASS is installed
// do note that it must be a *relative* location.
// DICK will need to know where your ASS is installed
// Do note that it must be a *relative* location.
// For example "../ass" means ass is installed in the parent directory.
//NOTE: If you are using DICK as a submodule to ASS, you would simply use './' as ASS is running DICK and the folder structure starts in ASS' folder.
export const ASS_LOCATION = "../ass"
/* ------------------- STAFF ID CONSTANTS ---------------------------- */
// this will eventually be moved away from constants
// Array of all USERNAMES in your ASS auth.json file that will have admin access: ["ass", "dick", "frank"]
export const STAFF_IDS = ["ass"]
/* ------------------- SYSTEM CONSTANTS ------------------------ */
// Port to run the server on, change if you have something else running on that port
export const PORT = "3000"

@ -13,8 +13,8 @@ import { publicRoutes } from "./routes/route.public"
import { userRoutes } from "./routes/route.user"
import { adminRoutes } from "./routes/route.admin"
import { PORT } from "./constants"
import { syncAssUsersToDick } from "./utils/database"
// Add async into express cause async is megachad
const app = express()
// Setting up express
@ -33,18 +33,21 @@ app.use(cookieSession({
keys: ['_H-A*7LKy0ivJCc3JJ!p7GriVigPN+faeXKl3QS8tx)SRoJV6l6s2biA#BAR2a9siA=xfcXW(2D-Ig9J2eP83zeBC6Fc%BSvg+DQbeWljQ$ypx!dtJ(#VTu!Cu#hXQQoilz4-Mr33xz&#(PdRwuP1T'],
maxAge: 30 * 24 * 60 * 1000 // 30 days
}))
app.use(flash());
app.use(flash())
app.use(passport.initialize())
app.use(passport.session())
// Global variables
app.use((request, response, next) => {
response.locals.success_alert_message = request.flash('success_alert_message');
response.locals.error_message = request.flash('error_message');
response.locals.error = request.flash('error');
next();
response.locals.success_alert_message = request.flash('success_alert_message')
response.locals.error_message = request.flash('error_message')
response.locals.error = request.flash('error')
next()
})
// Database migrations
syncAssUsersToDick()
// Make the public folder available publically, you know, so the public can view the public files that should be freely open to the genreal public
app.use(
express.static(path.join(__dirname, "public/"), { maxAge: 31557600000 })

@ -0,0 +1,2 @@
users.json
settings.json

@ -16,15 +16,52 @@
.bg-tertiary {
@apply bg-lighttheme-tertiary dark:bg-darktheme-tertiary;
}
.bg-accent {
@apply bg-lighttheme-accent dark:bg-darktheme-accent;
}
.bg-accentsecondary {
@apply bg-lighttheme-accentSecondary dark:bg-darktheme-accentSecondary;
}
.bg-tertiary-hover {
@apply bg-lighttheme-tertiaryHover dark:bg-darktheme-tertiaryHover;
}
.bg-tooltip {
@apply bg-lighttheme-tooltip dark:bg-darktheme-tooltip;
}
/* Custom Form Colors */
.bg-forminput {
@apply bg-lightthemeForm-input dark:bg-darkthemeForm-input;
}
/* Custom Border Colors */
.border-tooltip {
@apply border-lightthemeBorder-tooltip dark:border-darkthemeBorder-tooltip;
}
.border-accent {
@apply border-lightthemeBorder-accent dark:border-darkthemeBorder-accent;
}
.border-accentsecondary {
@apply border-lightthemeBorder-accentSecondary dark:border-darkthemeBorder-accentSecondary;
}
.border-form {
@apply border-lightthemeBorder-form dark:border-darkthemeBorder-form;
}
.border-table {
@apply border-lightthemeBorder-table dark:border-darkthemeBorder-table;
}
/* Custom Text Colors */
.text-color-primary {
@apply text-lightthemeText-primary dark:text-darkthemeText-primary;
}
.text-color-secondary {
@apply text-lightthemeText-secondary dark:text-darkthemeText-secondary;
}
.text-color-tertiary {
@apply text-lightthemeText-tertiary dark:text-darkthemeText-tertiary;
}
.text-color-light {
@apply text-lightthemeText-light dark:text-darkthemeText-secondary;
}
.text-color-accent {
@apply text-lightthemeText-accentPrimary dark:text-darkthemeText-accentPrimary;
}
@ -49,4 +86,79 @@
.has-name-tooltip:hover .name-tooltip {
@apply hover:visible hover:z-50;
}
.switch {
@apply inline-flex items-center cursor-pointer relative;
}
.switch input[type="checkbox"] {
@apply absolute left-0 opacity-0 -z-10;
}
.switch input[type="checkbox"] + .check {
@apply border-gray-700 border transition-colors duration-200;
}
.switch input[type="checkbox"]:focus + .check {
@apply ring ring-lightthemeBorder-accent dark:ring-darkthemeBorder-accent;
}
.checkbox input[type="checkbox"] + .check {
@apply rounded;
}
.switch input[type="checkbox"] + .check {
@apply flex items-center shrink-0 w-12 h-6 p-0.5 bg-forminput;
}
/*bg-[#19253B]*/
.switch input[type="checkbox"] + .check,
.switch input[type="checkbox"] + .check:before {
@apply rounded-full;
}
.switch input[type="checkbox"]:checked + .check {
@apply bg-lighttheme-accent dark:bg-darktheme-accent border-lightthemeBorder-accent dark:border-darkthemeBorder-accent;
}
.switch input[type="checkbox"] + .check:before {
content: "";
@apply block w-5 h-5 bg-white border border-gray-700;
}
.switch input[type="checkbox"]:checked + .check:before {
transform: translate3d(110%, 0, 0);
@apply border-lightthemeBorder-accent dark:border-darkthemeBorder-accent;
}
}
@layer utilities {
.custom-color-picker-border {
--color: linear-gradient(
90deg,
Red,
Orange,
Yellow,
Green,
purple,
Indigo,
violet
);
width: 99%;
height: 1.57rem;
position: relative;
}
/* Create the pseudo class and add styling */
.custom-color-picker-border::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
width: 100%;
height: 150%;
background: var(--color);
border-radius: 5px;
}
}

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 70 KiB

@ -0,0 +1,18 @@
// DOM Ready Function
function ready(fn) {
if (document.readyState != 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
// DOM Ready Called
ready(function () {
const colorPicker = document.querySelector('.custom-color-picker-border');
colorPicker.addEventListener('input',()=>{
colorPicker.style.setProperty('--color', colorPicker.value)
})
});

@ -1,196 +0,0 @@
// DOM Ready Function
function ready(fn) {
if (document.readyState != 'loading') {
fn()
} else {
document.addEventListener('DOMContentLoaded', fn)
}
}
// DOM Ready Called
ready(function () {
const openFile = document.getElementById('openFile')
const showFile = document.getElementById('showFile')
const traMove = document.getElementsByClassName('traMove')
const tabsProfile = document.getElementsByClassName('tabsProfile')
const tabsProfileContent = document.getElementsByClassName('tabsProfileContent')
const dropdownNavBtn = document.getElementById('dropdownNavBtn')
const dropdownNav = document.getElementById('dropdownNav')
const dropdownProfileBtn = document.getElementsByClassName('dropdownProfileBtn')
const dropdownProfile = document.getElementsByClassName('dropdownProfile')
const dropdownSearchBtn = document.getElementsByClassName('dropdownSearchBtn')
const dropdownSearch = document.getElementsByClassName('dropdownSearch')
const dropdownFileBtn = document.getElementsByClassName('dropdownFileBtn')
// Open File Manager Event
/*
function fireFileManager(){
openFile.addEventListener('click', () => {
showFile.click();
});
}
fireFileManager();
*/
// Dropdown Navbar Event
function fireDropdownNav(){
dropdownNavBtn.addEventListener('click', () => {
if (dropdownNav.classList.contains('-translate-y-full')) {
dropdownNav.classList.remove('-translate-y-full')
dropdownNav.classList.add('translate-y-0', 'ease-linear')
// When the event get fired, every 'traMove' id attibute will move down
Array.prototype.forEach.call(traMove, (e) => {
e.classList.remove('-translate-y-32')
e.classList.add('translate-y-0', 'ease-linear')
})
} else {
dropdownNav.classList.add('-translate-y-full',)
dropdownNav.classList.remove('translate-y-0', 'ease-linear')
// Then if you hide the event, every 'traMove' id attribute will back to normal
Array.prototype.forEach.call(traMove, (e) => {
e.classList.add('-translate-y-32')
e.classList.remove('translate-y-0', 'ease-linear')
})
}
})
}
fireDropdownNav()
// Dropdown Profile Event
function fireDropdownProfile() {
Array.prototype.forEach.call(dropdownProfileBtn, function (e, index) {
e.addEventListener('click', () => {
var timer
if (dropdownProfile[index].classList.contains('opacity-0')) {
window.clearTimeout(timer)
dropdownProfile[index].classList.remove('opacity-0', 'translate-y-6', 'invisible')
dropdownProfile[index].classList.add('translate-y-0', 'opacity-100')
} else {
dropdownProfile[index].classList.add('opacity-0', 'translate-y-6')
dropdownProfile[index].classList.remove('translate-y-0', 'opacity-100')
//Set timer to hide the dropdown
//the value of timer '250' must be same as the tailwind class 'duration-250' in the class dropdownProfile attribute
timer = window.setTimeout( () => {
dropdownProfile[index].classList.add('invisible')
}, 250)
}
})
// Click outside event
window.addEventListener('click', (eve) => {
if (!dropdownProfileBtn[index].contains(eve.target) && !dropdownProfile[index].contains(eve.target)) {
dropdownProfile[index].classList.add('opacity-0', 'translate-y-6')
dropdownProfile[index].classList.remove('translate-y-0', 'opacity-100')
// Same as above
timer = window.setTimeout( () => {
dropdownProfile[index].classList.add('invisible')
}, 250)
}
})
})
}
fireDropdownProfile()
// Dropdown Search Event
function fireDropdownSearch() {
Array.prototype.forEach.call(dropdownSearchBtn, (e, index) => {
e.addEventListener('click', () => {
var timer
if (dropdownSearch[index].classList.contains('opacity-0')) {
window.clearTimeout(timer)
dropdownSearch[index].classList.remove('opacity-0', 'translate-y-6', 'invisible')
dropdownSearch[index].classList.add('translate-y-0', 'opacity-100')
} else {
dropdownSearch[index].classList.add('opacity-0', 'translate-y-6')
dropdownSearch[index].classList.remove('translate-y-0', 'opacity-100')
//Set timer to hide the dropdown
//the value of timer '250' must be same as the tailwind class 'duration-250' in the class dropdownSearch attribute
timer = window.setTimeout( () => {
dropdownSearch[index].classList.add('invisible')
}, 250)
}
})
// Click outside event
window.addEventListener('click', (eve) => {
if (!dropdownSearchBtn[index].contains(eve.target) && !dropdownSearch[index].contains(eve.target)) {
dropdownSearch[index].classList.add('opacity-0', 'translate-y-6')
dropdownSearch[index].classList.remove('translate-y-0', 'opacity-100')
// Same as above
timer = window.setTimeout( () => {
dropdownSearch[index].classList.add('invisible')
}, 250)
}
})
})
}
fireDropdownSearch()
// Dropdown File Event
function fireDropdownFile() {
Array.prototype.forEach.call(dropdownFileBtn, (e, index) => {
const findSibling = e.parentElement.children[1]
e.addEventListener('click', () => {
var timer
if (findSibling.classList.contains('opacity-0')) {
window.clearTimeout(timer)
findSibling.classList.remove('opacity-0', 'translate-y-6', 'invisible')
findSibling.classList.add('translate-y-0', 'opacity-100')
} else {
findSibling.classList.add('opacity-0', 'translate-y-6')
findSibling.classList.remove('translate-y-0', 'opacity-100')
//Set timer to hide the dropdown
//the value of timer '250' must be same as the tailwind class 'duration-250' in the class dropdownFile attribute
timer = window.setTimeout( () => {
findSibling.classList.add('invisible')
}, 250)
}
})
// Click outside event
window.addEventListener('click', (eve) => {
if (!dropdownFileBtn[index].contains(eve.target) && !findSibling.contains(eve.target)) {
findSibling.classList.add('opacity-0', 'translate-y-6')
findSibling.classList.remove('translate-y-0', 'opacity-100')
// Same as above
timer = window.setTimeout( () => {
findSibling.classList.add('invisible')
}, 250)
}
})
})
}
fireDropdownFile()
// Tabs Profile Event
function fireTabsProfile(){
//File Manager Tab = 0
tabsProfile[0].addEventListener('click', () => {
if (tabsProfile[1].classList.contains('border-b-2', 'border-purple-400', 'font-semibold')) {
tabsProfile[1].classList.remove('border-b-2', 'border-purple-400', 'font-semibold')
tabsProfile[0].classList.add('border-b-2', 'border-purple-400', 'font-semibold')
tabsProfileContent[1].classList.add('hidden')
tabsProfileContent[0].classList.remove('hidden')
}
});
//Config Gen Tab = 1
tabsProfile[1].addEventListener('click', () => {
if (tabsProfile[0].classList.contains('border-b-2', 'border-purple-400', 'font-semibold')) {
tabsProfile[0].classList.remove('border-b-2', 'border-purple-400', 'font-semibold')
tabsProfile[1].classList.add('border-b-2', 'border-purple-400', 'font-semibold')
tabsProfileContent[0].classList.add('hidden')
tabsProfileContent[1].classList.remove('hidden')
}
})
}
fireTabsProfile()
})

@ -0,0 +1,162 @@
// DOM Ready Function
function ready(fn) {
if (document.readyState != 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
// DOM Ready Called
ready(function () {
const traMove = document.getElementsByClassName('traMove');
const dropdownNavBtn = document.getElementById('dropdownNavBtn');
const dropdownNav = document.getElementById('dropdownNav');
const dropdownProfileBtn = document.getElementsByClassName('dropdownProfileBtn');
const dropdownProfile = document.getElementsByClassName('dropdownProfile');
const dropdownSearchBtn = document.getElementsByClassName('dropdownSearchBtn');
const dropdownSearch = document.getElementsByClassName('dropdownSearch');
const dropdownFileBtn = document.getElementsByClassName('dropdownFileBtn');
// Dropdown Navbar Event
function fireDropdownNav() {
dropdownNavBtn.addEventListener('click', () => {
if (dropdownNav.classList.contains('-translate-y-full')) {
dropdownNav.classList.remove('-translate-y-full');
dropdownNav.classList.add('translate-y-0', 'ease-linear');
// When the event get fired, every 'traMove' id attibute will move down
for (let index = 0; index < traMove.length; index++) {
traMove[index].classList.remove('-translate-y-32');
traMove[index].classList.add('translate-y-0', 'ease-linear');
}
} else {
dropdownNav.classList.add('-translate-y-full',);
dropdownNav.classList.remove('translate-y-0', 'ease-linear');
// Then if you hide the event, every 'traMove' id attribute will back to normal
for (let index = 0; index < traMove.length; index++) {
traMove[index].classList.add('-translate-y-32');
traMove[index].classList.remove('translate-y-0', 'ease-linear');
}
}
});
}
fireDropdownNav();
// Dropdown Profile Event
function fireDropdownProfile() {
for (let index = 0; index < dropdownProfileBtn.length; index++) {
dropdownProfileBtn[index].addEventListener('click', () => {
var timer;
if (dropdownProfile[index].classList.contains('opacity-0')) {
window.clearTimeout(timer);
dropdownProfile[index].classList.remove('opacity-0', 'translate-y-6', 'invisible');
dropdownProfile[index].classList.add('translate-y-0', 'opacity-100');
} else {
dropdownProfile[index].classList.add('opacity-0', 'translate-y-6');
dropdownProfile[index].classList.remove('translate-y-0', 'opacity-100');
//Set timer to hide the dropdown
//the value of timer '250' must be same as the tailwind class 'duration-250' in the class dropdownProfile attribute
timer = window.setTimeout(() => {
dropdownProfile[index].classList.add('invisible');
}, 250);
}
});
// Click outside event
window.addEventListener('click', (eve) => {
if (!dropdownProfileBtn[index].contains(eve.target) && !dropdownProfile[index].contains(eve.target)) {
dropdownProfile[index].classList.add('opacity-0', 'translate-y-6');
dropdownProfile[index].classList.remove('translate-y-0', 'opacity-100');
// Same as above
timer = window.setTimeout(() => {
dropdownProfile[index].classList.add('invisible');
}, 250);
}
});
}
}
fireDropdownProfile();
// Dropdown Search Event
function fireDropdownSearch() {
for (let index = 0; index < dropdownSearchBtn.length; index++) {
dropdownSearchBtn[index].addEventListener('click', () => {
var timer;
if (dropdownSearch[index].classList.contains('opacity-0')) {
window.clearTimeout(timer);
dropdownSearch[index].classList.remove('opacity-0', 'translate-y-6', 'invisible');
dropdownSearch[index].classList.add('translate-y-0', 'opacity-100');
} else {
dropdownSearch[index].classList.add('opacity-0', 'translate-y-6');
dropdownSearch[index].classList.remove('translate-y-0', 'opacity-100');
//Set timer to hide the dropdown
//the value of timer '250' must be same as the tailwind class 'duration-250' in the class dropdownSearch attribute
timer = window.setTimeout(() => {
dropdownSearch[index].classList.add('invisible');
}, 250);
}
});
// Click outside event
window.addEventListener('click', (eve) => {
if (!dropdownSearchBtn[index].contains(eve.target) && !dropdownSearch[index].contains(eve.target)) {
dropdownSearch[index].classList.add('opacity-0', 'translate-y-6');
dropdownSearch[index].classList.remove('translate-y-0', 'opacity-100');
// Same as above
timer = window.setTimeout(() => {
dropdownSearch[index].classList.add('invisible');
}, 250);
}
});
}
}
fireDropdownSearch();
// Dropdown File Event
function fireDropdownFile() {
for (let index = 0; index < dropdownFileBtn.length; index++) {
const findSibling = dropdownFileBtn[index].parentElement.children[1];
dropdownFileBtn[index].addEventListener('click', () => {
var timer;
if (findSibling.classList.contains('opacity-0')) {
window.clearTimeout(timer);
findSibling.classList.remove('opacity-0', 'translate-y-6', 'invisible');
findSibling.classList.add('translate-y-0', 'opacity-100');
} else {
findSibling.classList.add('opacity-0', 'translate-y-6');
findSibling.classList.remove('translate-y-0', 'opacity-100');
//Set timer to hide the dropdown
//the value of timer '250' must be same as the tailwind class 'duration-250' in the class dropdownFile attribute
timer = window.setTimeout(() => {
findSibling.classList.add('invisible');
}, 250);
}
});
// Click outside event
window.addEventListener('click', (eve) => {
if (!dropdownFileBtn[index].contains(eve.target) && !findSibling.contains(eve.target)) {
findSibling.classList.add('opacity-0', 'translate-y-6');
findSibling.classList.remove('translate-y-0', 'opacity-100');
// Same as above
timer = window.setTimeout(() => {
findSibling.classList.add('invisible');
}, 250);
}
});
}
}
fireDropdownFile();
});

@ -0,0 +1,30 @@
// DOM Ready Function
function ready(fn) {
if (document.readyState != 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
// DOM Ready Called
ready(function () {
const buttonModal = document.getElementsByClassName('buttonModal');
const showModal = document.getElementById('showModal');
function fireModal() {
for (let index = 0; index < buttonModal.length; index++) {
buttonModal[index].addEventListener('click', () => {
if(showModal.classList.contains('flex')){
showModal.classList.remove('flex')
showModal.classList.add('hidden')
}else{
showModal.classList.remove('hidden')
showModal.classList.add('flex')
}
});
}
}
fireModal();
});

@ -0,0 +1,28 @@
// DOM Ready Function
function ready(fn) {
if (document.readyState != 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
// DOM Ready Called
ready(function () {
const inputPasswordType = document.getElementById('inputPasswordType')
const checkboxPassword = document.getElementById('checkboxPassword')
function changeInputPasswordType() {
checkboxPassword.addEventListener('change', (eve) => {
const checked = eve.target.checked
if (checked) {
inputPasswordType.type = 'text'
} else {
inputPasswordType.type = 'password'
}
});
}
changeInputPasswordType();
});

@ -0,0 +1,50 @@
// DOM Ready Function
function ready(fn) {
if (document.readyState != 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
// DOM Ready Called
ready(function () {
const tabAdmin = document.getElementsByClassName('tabAdmin');
const changeTabAdmin = document.getElementsByClassName('changeTabAdmin');
// Tabs Admin Event
function fireTabsAdmin() {
//Dynamic tabs with loops
const loop = [
{ id: 1, contains: 1, remove: 1, add: 0 },
{ id: 2, contains: 0, remove: 0, add: 1 }
]
var currentTab = []
//index 0 is for tab app-setting and 1 is users
loop.forEach((e, index) => {
tabAdmin[index].addEventListener('click', () => {
if (tabAdmin[e.contains].classList.contains('border-b-2', 'border-accent', 'font-semibold')) {
tabAdmin[e.remove].classList.remove('border-b-2', 'border-accent', 'font-semibold');
tabAdmin[e.add].classList.add('border-b-2', 'border-accent', 'font-semibold');
}
currentTab.push(tabAdmin[index].dataset.id)
if(currentTab.slice(-2)[0] != tabAdmin[index].dataset.id || (currentTab.length <= 1 && tabAdmin[index].dataset.id == 2)){
for (let idx = 0; idx < changeTabAdmin.length; idx++) {
if (changeTabAdmin[idx].classList.contains('flex-1')) {
changeTabAdmin[idx].classList.remove('flex-1');
changeTabAdmin[idx].classList.add('hidden');
} else {
changeTabAdmin[idx].classList.remove('hidden');
changeTabAdmin[idx].classList.add('flex-1');
}
}
}
});
})
}
fireTabsAdmin();
});

@ -0,0 +1,50 @@
// DOM Ready Function
function ready(fn) {
if (document.readyState != 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
// DOM Ready Called
ready(function () {
const tabUser = document.getElementsByClassName('tabUser');
const showConfigGen = document.getElementsByClassName('changeTabUser');
function fireTabsProfile() {
const loop = [
{ id: 1, contains: 1, remove: 1, add: 0 },
{ id: 2, contains: 0, remove: 0, add: 1 }
]
var currentTab = []
//index 0 is for tab file manager and 1 is config-gen
loop.forEach((e, index) => {
tabUser[index].addEventListener('click', () => {
if (tabUser[e.contains].classList.contains('border-b-2', 'border-accent', 'font-semibold')) {
tabUser[e.remove].classList.remove('border-b-2', 'border-accent', 'font-semibold');
tabUser[e.add].classList.add('border-b-2', 'border-accent', 'font-semibold');
}
currentTab.push(tabUser[index].dataset.id)
if(currentTab.slice(-2)[0] != tabUser[index].dataset.id || (currentTab.length <= 1 && tabUser[index].dataset.id == 2)){
for (let idx = 0; idx < showConfigGen.length; idx++) {
if (showConfigGen[idx].classList.contains('flex-1')) {
showConfigGen[idx].classList.remove('flex-1');
showConfigGen[idx].classList.add('hidden');
} else {
showConfigGen[idx].classList.remove('hidden');
showConfigGen[idx].classList.add('flex-1');
}
}
}
});
})
}
fireTabsProfile();
});

@ -1,16 +1,180 @@
import { Request, Response, Router } from "express"
import { authCheck, adminCheck, wrap } from "../utils/utils"
import path from "path"
import fs from "fs-extra"
import multer from "multer"
import { authCheck, adminCheck, wrap } from "../utils/middleware"
import { checkIfUserExistInASS, checkIfUserExistInDICK, createUserInASS, createUserInDICK, getSettingsDatabase } from "../utils/database"
import { TEMPLATE } from "../constants"
import { Pager } from "../Pager"
import { defaultPPStorage, defaultPPStorageDist, imageFileFilter, logoStorage, logoStorageDist } from "../utils/uploads"
const settingsDatabaseLocation = path.resolve(`./src/database/settings.json`)
export const adminRoutes = (app: Router) => {
app.get(
"/admin",
authCheck,
adminCheck,
wrap,
async (req: Request, res: Response) => {
return Pager.render(res, req, TEMPLATE.USER, {})
app.get(
"/admin",
authCheck,
adminCheck,
wrap,
async (req: Request, res: Response) => {
return Pager.render(res, req, TEMPLATE.USER, {})
}
)
// Save button on app settings page
app.post(
"/admin/save/settings",
authCheck,
adminCheck,
(req: Request, res: Response) => {
const settingsDatabase = getSettingsDatabase()
const { name, appEmoji, siteTitle, siteDescription, loginText, captchaCheckbox, captchaSiteID, captchaSecretKey, privateModeEnabled, registrationEnabled } = req.body
/*
* This code is for if I ever decide to add changing the location of the image urls (such as calling an external URL from local files)
\
if (logo) {
if (!/\.(jpg|jpeg|png|webp|avif|gif|svg)$/.test(logo)) {
req.flash('error_message', 'Logo URL is not a valid picture.')
return res.redirect('/admin')
}
settingsDatabase.logo = logo
}
if (defaultProfilePicture) {
if (!/\.(jpg|jpeg|png|webp|avif|gif|svg)$/.test(logo)) {
req.flash('error_message', 'Default profile picture is not a valid picture.')
return res.redirect('/admin')
}
settingsDatabase.defaultProfilePicture = defaultProfilePicture
}
*/
if (captchaCheckbox) {
// If they do not have a capatcha site id set, they can not enable and save capatcha preventing it not working.
if (!settingsDatabase.captchaSiteID) {
if (!captchaSiteID) {
req.flash('error_message', 'You must include a captcha site ID to enable captcha.')
return res.redirect('/admin')
}
}
)
if (!settingsDatabase.captchaSecretKey) {
if (!captchaSecretKey) {
req.flash('error_message', 'You must include a captcha secret key to enable captcha.')
return res.redirect('/admin')
}
}
settingsDatabase.captchaEnabled = true
} else {
settingsDatabase.captchaEnabled = false
}
name ? settingsDatabase.name = name : null
appEmoji ? settingsDatabase.appEmoji = appEmoji : null
siteTitle ? settingsDatabase.siteTitle = siteTitle : null
siteDescription ? settingsDatabase.siteDescription = siteDescription : null
loginText ? settingsDatabase.loginText = loginText : null
captchaSiteID ? settingsDatabase.captchaSiteID = captchaSiteID : null
captchaSecretKey ? settingsDatabase.captchaSecretKey = captchaSecretKey : null
privateModeEnabled ? settingsDatabase.privateModeEnabled = true : settingsDatabase.privateModeEnabled = false
registrationEnabled ? settingsDatabase.registrationEnabled = true : settingsDatabase.registrationEnabled = false
fs.writeJsonSync(settingsDatabaseLocation, settingsDatabase, { spaces: 4 })
req.flash('success_alert_message', 'Settings successfully saved')
return res.redirect('/admin')
}
)
// App logo upload on app settings page
app.post(
"/admin/upload/logo",
authCheck,
adminCheck,
(req: Request, res: Response) => {
const uploadLogo = multer({ storage: logoStorage, fileFilter: imageFileFilter }).fields([{ name: 'app-logo', maxCount: 1 }])
const uploadLogoDist = multer({ storage: logoStorageDist, fileFilter: imageFileFilter }).fields([{ name: 'app-logo', maxCount: 1 }])
uploadLogo(req, res, (err) => {
if (err) {
console.log(err)
req.flash('error_message', 'Logo failed to upload')
return res.redirect('/admin')
}
})
uploadLogoDist(req, res, (err) => {
if (err) {
console.log(err)
req.flash('error_message', 'Logo failed to upload')
return res.redirect('/admin')
}
})
req.flash('success_alert_message', 'Logo successfully uploaded and saved. Please clear cache to see the new change!')
return res.redirect('/admin')
}
)
// Default profile picture upload on app settings page
app.post(
"/admin/upload/default-pp",
authCheck,
adminCheck,
(req: Request, res: Response) => {
const uploadDefaultPP = multer({ storage: defaultPPStorage, fileFilter: imageFileFilter }).fields([{ name: 'default-pp', maxCount: 1 }])
const uploadDefaultPPDist = multer({ storage: defaultPPStorageDist, fileFilter: imageFileFilter }).fields([{ name: 'default-pp', maxCount: 1 }])
uploadDefaultPP(req, res, (err) => {
if (err) {
console.log(err)
req.flash('error_message', 'Profile picture failed to upload. Please clear cache to see the new change!')
return res.redirect('/admin')
}
})
uploadDefaultPPDist(req, res, (err) => {
if (err) {
console.log(err)
req.flash('error_message', 'Logo failed to upload')
return res.redirect('/admin')
}
})
req.flash('success_alert_message', 'Logo successfully uploaded and saved')
return res.redirect('/admin')
}
)
// Add new user via add user modal
app.post('/admin/add/user', async (req, res) => {
// Check if the form is filled our properly
if (!req.body.username) {
req.flash('error_message', 'You did not include a username!')
return res.redirect("/admin")
}
if (!req.body.password) {
req.flash('error_message', 'You did not include a password!')
return res.redirect("/admin")
}
if (req.body.username > 20) {
req.flash('error_messge', 'Username can not be more than 20 characters!')
return res.redirect("/admin")
}
if (req.body.password < 5) {
req.flash('error_messge', 'Secret key can not be less than 5 characters!')
return res.redirect("/admin")
}
// Check if user exists in ass or dick, if it does then we throw error
if (await checkIfUserExistInASS(req.body.username, req.body.password) || await checkIfUserExistInDICK(req.body.username)) {
req.flash('error_message', 'User already exists!')
return res.redirect("/admin")
}
// Create the user
await createUserInASS(req.body.username, req.body.password)
await createUserInDICK(req.body.username)
// Redirect them to the login screen
req.flash('success_alert_message', `You have sucesfully created a user with the name ${req.body.username}. They can now log in with the token you provided.`)
return res.redirect("/admin")
})
}

@ -1,5 +1,6 @@
import { Request, Response, NextFunction, Router } from "express"
import { authCheck } from "../utils/utils"
import { authCheck, checkCaptcha } from "../utils/middleware"
import { checkIfUserExistInASS, checkIfUserExistInDICK, createUserInASS, createUserInDICK } from "../utils/database"
const { passport } = require("../utils/passport")
@ -36,30 +37,26 @@ export const authRoutes = (app: Router) => {
})
})
// When logout, redirect to client
app.get("/auth/logout", (req: Request, res: Response) => {
const user = req.user
req.logout({ keepSessionInfo: false }, null)
req.flash('success_alert_message', 'You have been succesfully logged out')
return res.redirect("/login")
})
// Auth with local passport, send them to ricky boy to prevent brute forcing 'cause Im too lazy to add proper captcha rn
app.post(
"/auth/login",
checkCaptcha,
passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
failureFlash: true
}),
(req: Request, res: Response, next: NextFunction) => {
(next: NextFunction) => {
next()
}
)
// Redirect to home page after successfully login
app.get(
"/auth/callback",
@ -68,8 +65,43 @@ export const authRoutes = (app: Router) => {
if (req.user) {
return res.redirect("/")
}
console.log('we hit here')
return res.redirect("/login")
}
)
// Register page post request on button submit
app.post('/auth/register', checkCaptcha, async (req, res) => {
// Check if the form is filled our properly
if (!req.body.username) {
req.flash('error_message', 'You did not include a username!')
return res.redirect("/register")
}
if (!req.body.password) {
req.flash('error_message', 'You did not include a password!')
return res.redirect("/register")
}
if (req.body.username > 20) {
req.flash('error_messge', 'Username can not be more than 20 characters!')
return res.redirect("/register")
}
if (req.body.password < 5) {
req.flash('error_messge', 'Secret key can not be less than 5 characters!')
return res.redirect("/register")
}
// Check if user exists in ass or dick, if it does then we throw error
if (await checkIfUserExistInASS(req.body.username, req.body.password) || await checkIfUserExistInDICK(req.body.username)) {
req.flash('error_message', 'User already exists!')
return res.redirect("/register")
}
// Create the user
await createUserInASS(req.body.username, req.body.password)
await createUserInDICK(req.body.username)
// Redirect them to the login screen
req.flash('success_alert_message', `You have sucesfully created a user with the name ${req.body.username}. You can now log in.`)
return res.redirect("/login")
})
}

@ -1,4 +1,5 @@
import { Request, Response, Router } from "express"
import { getSettingsDatabase } from "../utils/database"
import { TEMPLATE } from "../constants"
import { Pager } from "../Pager"
@ -8,6 +9,22 @@ export const publicRoutes = (app: Router) => {
if (req.user) {
return res.redirect('/')
}
await Pager.render(res, req, TEMPLATE.PUBLIC)
await Pager.render(res, req, TEMPLATE.PUBLIC, {})
})
app.get("/register", async (req: Request, res: Response) => {
// If the user is already logged in via cookies, redirect them to the dashboard
if (req.user) {
return res.redirect('/')
}
const database = getSettingsDatabase()
if (!database.registrationEnabled){
req.flash('error_message', 'Registration is not enabled!')
return res.redirect("/login")
}
await Pager.render(res, req, TEMPLATE.PUBLIC, {})
})
}

@ -1,5 +1,5 @@
import { Request, Response, Router } from "express"
import { adminCheck, authCheck, wrap } from "../utils/utils"
import { adminCheck, authCheck, wrap } from "../utils/middleware"
import { TEMPLATE } from "../constants"
import { Pager } from "../Pager"
import { parseAuthFile } from "../utils/assJSONStructure"

@ -0,0 +1,22 @@
export interface ISettingsDatabase {
name: string
logo: string
siteTitle: string
siteDescription: string
loginText: string
appEmoji: string
captchaEnabled: boolean
captchaSiteID: string | null
captchaSecretKey: string | null
defaultProfilePicture: string
registrationEnabled: boolean
privateModeEnabled: boolean
}
export interface IUsersDatabase extends Array<IUserSettings>{}
export interface IUserSettings {
username: string
role: "admin" | "user"
profilePicture: string | null
}

@ -1,7 +1,7 @@
import { ASS_LOCATION } from "../constants"
import fs from "fs"
import path from "path"
import { ASSItem } from "typings/ASSTypes"
import { ASSItem } from "typings/ASS"
const DATA_FILE_PATH = path.resolve(`${ASS_LOCATION}/data.json`)
const AUTH_FILE_PATH = path.resolve(`${ASS_LOCATION}/auth.json`)

@ -0,0 +1,202 @@
import path from "path"
import fs from "fs-extra"
import { ISettingsDatabase, IUsersDatabase, IUserSettings } from "typings/database"
import { ASS_LOCATION } from "../constants"
import { parseAuthFile } from "./assJSONStructure"
import { Log } from "@callmekory/logger/lib"
const settingsDatabaseLocation = path.resolve(`./src/database/settings.json`)
const userDatabaseLocation = path.resolve(`./src/database/users.json`)
/////////////////////////////
//
// Get Database Files
//
/////////////////////////////
// Get settings database, creating a default one if it doesnt exist
export const getSettingsDatabase = (): ISettingsDatabase => {
try {
const databaseFile = fs.readFileSync(settingsDatabaseLocation).toString()
const database: ISettingsDatabase = JSON.parse(databaseFile)
return database
} catch (error) {
const defaultDatabase: ISettingsDatabase = {
name: "dick",
appEmoji: "🍆",
siteTitle: "DICK (Directly Integrated Client for Keisters)",
siteDescription: "The frontend for your backend",
loginText: "Sign in to easily manage your nudes.",
captchaEnabled: false,
captchaSiteID: null,
captchaSecretKey: null,
registrationEnabled: false,
privateModeEnabled: false,
logo: "./images/logo.png",
defaultProfilePicture: "./images/profile.png"
}
fs.writeJsonSync(settingsDatabaseLocation, defaultDatabase, { spaces: 4 })
return defaultDatabase
}
}
// Get users database, creating a default one if it doesnt exist
export const getUserDatabase = (): IUsersDatabase => {
try {
const databaseFile = fs.readFileSync(userDatabaseLocation).toString()
const database: IUsersDatabase = JSON.parse(databaseFile)
return database
} catch (error) {
const defaultDatabase: IUsersDatabase = []
fs.writeJsonSync(userDatabaseLocation, defaultDatabase, { spaces: 4 })
return defaultDatabase
}
}
// Get users database object, creating a default one if it doesnt exist
export const getUserDatabaseObj = (username: string): IUserSettings | null => {
try {
const database = getUserDatabase()
const user = database.find((e: IUserSettings) => e.username === username)
if (!user) {
let newUser: IUserSettings
// If there are no users in the database yet, we will make this user the admin (first user to login will always be admin)
if (database.length == 0) {
newUser = {
username: username,
role: "admin",
profilePicture: null
}
} else {
// Else we add the user to the database as a regular user
newUser = {
username: username,
role: "user",
profilePicture: null
}
}
database.push(newUser)
fs.writeJsonSync(userDatabaseLocation, database, { spaces: 4 })
return user
}
return user
} catch (error) {
Log.error(error)
return null
}
}
/////////////////////////////
//
// MISC
//
/////////////////////////////
/**
* Checks if the user is in ASS database, returns true if the user exists, or false if it does not
* @param username Username we are checking exists
* @param token? If passed, will also check ASS token
*/
export const checkIfUserExistInASS = async (username: string, token?: string): Promise<boolean> => {
const assUserDatabase = parseAuthFile()
let result = false
// Check if the user exists in the ASS Database
const assUsernameResult = assUserDatabase.find((user) => user.username === username) ? true : false
if (token) {
const assTokenResult = assUserDatabase.find((user) => user.password === token) ? true : false
if (assUsernameResult || assTokenResult) {
result = true
}
}
if (assUsernameResult) {
result = true
}
// Return true if any of the checks above found the user, else return false
return result
}
/**
* Checks if the user is in our local JSON database, returns true if the user exists, or false if it does not
* @param username Username we are checking exists
*/
export const checkIfUserExistInDICK = async (username: string): Promise<boolean> => {
const dickUserDatabase = getUserDatabase()
let result = false
// Check if the user exists in the DICK Database
if (dickUserDatabase.find((e: IUserSettings) => e.username === username)) {
result = true
}
// Return true if any of the checks above found the user, else return false
return result
}
/**
* Registers a new user to ASS
* @param username Username we will create
* @param password Password we will use
*/
export const createUserInASS = async (username: string, password: string) => {
const AUTH_FILE_PATH = path.resolve(`${ASS_LOCATION}/auth.json`)
fs.readJson(AUTH_FILE_PATH)
.then((auth) => {
auth.users[password] = { username, count: 0 }
fs.writeJsonSync(AUTH_FILE_PATH, auth, { spaces: 4 })
})
}
/**
* Registers a new user to DICK
* @param username Username we will create
*/
export const createUserInDICK = async (username: string) => {
const userDatabase = getUserDatabase()
// If user does not exist in our database, we create it
if (!userDatabase.find((e: IUserSettings) => e.username === username)) {
let newUser: IUserSettings
// If there are no users in the database yet, we will make this user the admin (first user to login will always be admin)
if (userDatabase.length == 0) {
newUser = {
username: username,
role: "admin",
profilePicture: null
}
} else {
// Else we add the user to the database as a regular user
newUser = {
username: username,
role: "user",
profilePicture: null
}
}
userDatabase.push(newUser)
fs.writeJsonSync(userDatabaseLocation, userDatabase, { spaces: 4 })
}
}
/**
* Cycles through all users in the ASS auth database and adds them to the DICk user database
* if they are not already in it.
*/
export const syncAssUsersToDick = () => {
const assUserDatabase = parseAuthFile()
const dickUserDatabase = getUserDatabase()
if(dickUserDatabase.length !== 0){
for (const user of assUserDatabase) {
if (!dickUserDatabase.find((e: IUserSettings) => e.username === user.username)) {
createUserInDICK(user.username)
}
}
}
}

@ -0,0 +1,100 @@
import { Response, NextFunction } from "express"
import { Log } from "@callmekory/logger"
import fetch from 'node-fetch'
import { IExtendedRequest } from "../typings/express-ext"
import { checkIfUserExistInDICK, createUserInDICK, getSettingsDatabase, getUserDatabase, getUserDatabaseObj } from "./database"
import { parseAuthFile } from "./assJSONStructure"
import { IUserSettings } from "typings/database"
/**
* Wraps the express route in a function that passes the
* `next` method from the route to the promise's catch
* statement which allows the middleware to catch the
* exception.
*/
export const wrap = async (req: IExtendedRequest, res: Response, next: NextFunction) => {
if (req.user) {
// If the user does not exist in DICKs database yet, we add them
if (await checkIfUserExistInDICK(req.user.username) == false) {
createUserInDICK(req.user.username)
}
// Make sure ASS users stay synced with DICK database
const assUserDatabase = parseAuthFile()
const dickUserDatabase = getUserDatabase()
if(dickUserDatabase.length !== 0){
for (const user of assUserDatabase) {
if (!dickUserDatabase.find((e: IUserSettings) => e.username === user.username)) {
createUserInDICK(user.username)
}
}
}
// Log the page the user navigated to
Log.info(`${req.user.username} navigated to page ${req.path}`)
}
return next()
}
//Express middleware to check captcha token
export const checkCaptcha = async (req: IExtendedRequest, res: Response, next: NextFunction) => {
const database = getSettingsDatabase()
// If captcha is enabled, we verify the captcha
if (database.captchaEnabled) {
// If there is no response for some reason
if(!req.body['h-captcha-response']){
Log.info(`A user submitted a form on the endpoint ${req.path} and failed captcha due to not being able to reach hCaptcha, redirecting back to login page`)
req.flash('error_message', `You failed the captcha due to not being able to reach hCaptcha. Please reach an admin.`)
return res.redirect("/login")
}
// Build payload with secret key and captcha response token from form data with key 'h-captcha-response'
const params = new URLSearchParams()
params.append('secret', database.captchaSecretKey)
params.append('response', req.body['h-captcha-response'])
// Make POST request with data payload to hCaptcha API endpoint
const response = await fetch("https://hcaptcha.com/siteverify", { method: 'POST', body: params })
const data = await response.json()
// Parse JSON from response. Check for success or error codes.
// If not correct, send back to login screen with error
// A missing-input-response means its not getting info from hcaptcha
if (data.success == false){
Log.info(`A user submitted a form on the endpoint ${req.path} and failed captcha due to: ${data['error-codes']}, redirecting back to login page`)
req.flash('error_message', `You failed the captcha due to ${data['error-codes']}. Please try again.`)
return res.redirect("/login")
}
// Else continue as they are verified as human
next()
} else next()
}
// Express middleware to check if username/password match one of the users
// in auth.json
export const authCheck = (req: IExtendedRequest, res: Response, next: NextFunction) => {
if (!req.user) {
Log.info(`A user navigated to page ${req.path} and is not logged in, redirecting to login page`)
req.flash('error_message', 'Please log in to access the requested page')
res.redirect('/login')
} else next()
}
// Express middleware to check if username trying to access the page matches the users
// in CONSTANTS
export const adminCheck = (req: IExtendedRequest, res: Response, next: NextFunction) => {
const user = getUserDatabaseObj(req.user.username)
if (!user) {
Log.info(`A user navigated to page ${req.path} and is not logged in, redirecting to login page`)
req.flash('error_message', 'Please log in to access the requested page')
res.redirect('/login')
}
if (user.role !== 'admin') {
Log.info(`${req.user.username} navigated to page ${req.path} and is not an admin, redirecting to users dashboard`)
res.redirect('/')
} else next()
}

@ -0,0 +1,51 @@
import { Request } from 'express'
import multer, { FileFilterCallback } from 'multer'
type DestinationCallback = (error: Error | null, destination: string) => void
type FileNameCallback = (error: Error | null, filename: string) => void
export const logoStorage = multer.diskStorage({
destination: (request: Request, file: Express.Multer.File, callback: DestinationCallback): void => {
callback(null, 'src/public/images')
},
filename: (req: Request, file: Express.Multer.File, callback: FileNameCallback): void => {
callback(null,'logo.png')
}
})
export const logoStorageDist = multer.diskStorage({
destination: (request: Request, file: Express.Multer.File, callback: DestinationCallback): void => {
callback(null, 'dist/public/images')
},
filename: (req: Request, file: Express.Multer.File, callback: FileNameCallback): void => {
callback(null,'logo.png')
}
})
export const defaultPPStorage = multer.diskStorage({
destination: (request: Request, file: Express.Multer.File, callback: DestinationCallback): void => {
callback(null, 'src/public/images')
},
filename: (req: Request, file: Express.Multer.File, callback: FileNameCallback): void => {
callback(null,'profile.png')
}
})
export const defaultPPStorageDist = multer.diskStorage({
destination: (request: Request, file: Express.Multer.File, callback: DestinationCallback): void => {
callback(null, 'dist/public/images')
},
filename: (req: Request, file: Express.Multer.File, callback: FileNameCallback): void => {
callback(null,'profile.png')
}
})
export const imageFileFilter = (request: Request, file: Express.Multer.File, callback: FileFilterCallback): void => {
if (
file.mimetype === 'image/png' ||
file.mimetype === 'image/jpg' ||
file.mimetype === 'image/jpeg'
) {
callback(null, true)
} else {
callback(null, false)
}
}

@ -1,8 +1,5 @@
import { Log } from "@callmekory/logger"
import { STAFF_IDS } from "../constants"
import { Response, NextFunction } from "express"
import { join, normalize } from "path"
import { IExtendedRequest } from "../typings/express-ext"
/**
* Generate the appropriate pathing for the a template to be rendered
*/
@ -12,39 +9,6 @@ export const templatePathBuilder = (templatePath: string) => {
return normalize(join(templateDir, "templates", templatePath))
}
/**
* Wraps the express route in a function that passes the
* `next` method from the route to the promise's catch
* statement which allows the middleware to catch the
* exception.
*/
export const wrap = (req: IExtendedRequest, res: Response, next: NextFunction) => {
if (req.user) {
Log.info(`${req.user.username} navigated to page ${req.path}`)
}
return next()
}
// Express middleware to check if username/password match one of the users
// in auth.json
export const authCheck = (req: IExtendedRequest, res: Response, next: NextFunction) => {
if (!req.user) {
Log.info(`A user navigated to page ${req.path} and is not logged in, redirecting to login page`)
req.flash('error_message', 'Please log in to access the requested page')
res.redirect('/login')
} else next()
}
// Express middleware to check if username trying to access the page matches the users
// in CONSTANTS
export const adminCheck = (req: IExtendedRequest, res: Response, next: NextFunction) => {
if ((STAFF_IDS.indexOf(req.user.username) > -1) == false) {
Log.info(`${req.user.username} navigated to page ${req.path} and is not an admin, redirecting to users dashboard`)
res.redirect('/')
} else next()
}
/**
*
* @param obj Object to be checked if empty
@ -75,7 +39,7 @@ export const formatSize = (kb: number, decimals = 2) => {
*
* @param unixTimestamp Unix timestamp you wish to convert to a Date object
*/
export const convertTimestamp = (unixTimestamp: number) => {
export const convertTimestamp = (unixTimestamp: number) => {
return new Date(unixTimestamp)
}
@ -84,10 +48,12 @@ export const formatSize = (kb: number, decimals = 2) => {
* @param data Original array of many objects
* @param itemsPerPage The amount of objects you wish you wish to have in each array in the new array
*/
export const convertToPaginatedArray = (data: Array<any>, itemsPerPage: number) => {
export const convertToPaginatedArray = (data: Array<any>, itemsPerPage: number) => {
let paginatedArray: Array<any> = []
for (let i=0; i < data.length; i += itemsPerPage) {
paginatedArray.push(data.slice(i,i + itemsPerPage))
for (let i = 0; i < data.length; i += itemsPerPage) {
paginatedArray.push(data.slice(i, i + itemsPerPage))
}
return paginatedArray
}
}

@ -1,6 +1,14 @@
const defaultTheme = require('tailwindcss/defaultTheme')
const colors = require('tailwindcss/colors')
// If you are adding a colour scheme to the themes below, be sure to add the combo to ./src/public/css/tailwind.css as a css element
// EG:
/*
* .bg-primary {
* @apply bg-lighttheme-primary dark:bg-darktheme-primary;
* }
*/
module.exports = {
darkMode: 'class',
content: [
@ -11,30 +19,59 @@ module.exports = {
extend: {
colors: {
darktheme: {
primary: '#1b253b', //bg-primary
secondary: '#232d45', //bg-secondary
secondaryHover: '#222f4d', //bg-secondary-hover
tertiary: '#28334e', //bg-tertiary
tertiaryHover: '#2d3c5d' //bg-tertiary-hover
primary: '#1B253B', //bg-primary
secondary: '#232D45', //bg-secondary
secondaryHover: '#222F4D', //bg-secondary-hover
tertiary: '#28334E', //bg-tertiary
accent: colors.purple[400], //bg-accent
accentSecondary: colors.purple[700], //bg-accentsecondary
tertiaryHover: '#2D3C5D', //bg-tertiary-hover
tooltip: '#354567' //bg-toolip
},
darkthemeBorder: {
tooltip: '#354567', // border-tooltip
accent: colors.purple[400], //border-accent
accentSecondary: colors.purple[700], //border-accentsecondary
form: colors.gray[600], //border-form
table: '#232D45' //border-table
},
darkthemeForm: {
input: colors.gray[700] //bg-forminput
},
darkthemeText: {
primary: colors.white, //text-color-primary
secondary: colors.slate[400], //text-color-secondary
tertiary: colors.gray[500], //text-color-tertiary
accentPrimary: colors.purple[400], //text-color-accent
accentSecondary: colors.purple[800] //text-color-accentsecondary
accentSecondary: colors.purple[800], //text-color-accentsecondary
},
lighttheme: {
primary: '#2b910e', //bg-primary
secondary: '#f1f5f9', //bg-secondary
secondaryHover: '#294ab3', //bg-secondary-hover
primary: '#2596BE', //bg-primary
secondary: '#F1F5F9', //bg-secondary
secondaryHover: '#3FB0D9', //bg-secondary-hover
tertiary: colors.white, //bg-tertiary
tertiaryHover: '#eef1f6' //bg-tertiary-hover
accent: colors.purple[200], //bg-accent
accentSecondary: colors.purple[400], //bg-accentsecondary
tertiaryHover: '#EEF1F6', //bg-tertiary-hover
tooltip: '#7393B3' //bg-tooltip
},
lightthemeBorder: {
tooltip: '#7393B3', //border-tooltip
accent: colors.purple[400], //border-accent
accentSecondary: colors.purple[700], //border-accentsecondary
form: colors.gray[600], //border-form
table: '#C4CAD7' //border-table
},
lightthemeForm: {
input: colors.white //bg-forminput
},
lightthemeText: {
primary: colors.slate[800], //text-color-primary
secondary: colors.slate[400], //text-color-secondary
secondary: colors.slate[600], //text-color-secondary
tertiary: colors.gray[700], //text-color-tertiary
light: colors.gray[300], //text-color-light
accentPrimary: colors.purple[600], //text-color-accent
accentSecondary: colors.purple[800] //text-color-accentsecondary
accentSecondary: colors.purple[800], //text-color-accentsecondary
}
},
maxHeight: {

@ -0,0 +1,74 @@
<div id="showModal" class="hidden z-50 items-center flex-col justify-center overflow-hidden fixed inset-0">
<!-- Modal Outscreen -->
<div class="buttonModal absolute inset-0 bg-gradient-to-tr opacity-90 from-gray-700 via-gray-900 to-gray-700">
</div>
<!-- Modal Outscreen -->
<form action="/admin/add/user" method="POST"
class="rounded-2xl flex-col bg-tertiary flex shadow-lg max-h-modal w-11/12 md:w-3/5 lg:w-2/5 xl:w-4/12 z-50">
<div class="flex-1 p-6">
<!--Modal Header -->
<div class="flex items-center justify-between mb-3 border-b border-slate-700/50">
<!-- Modal Title -->
<h1 class="text-2xl text-color-primary font-semibold">
Add New User
</h1>
<!-- End Modal Title -->
<!-- Modal Close Button -->
<button type="button" class="buttonModal inline-flex justify-center items-center text-color-secondary"
type="button">
<span class="inline-flex justify-center items-center w-6 h-6">
<svg viewBox="0 0 24 24" width="25" height="25" class="inline-block">
<path fill="currentColor"
d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z">
</path>
</svg>
</span>
</button>
<!-- End Modal Close Button -->
</div>
<!-- End Modal Header -->
<!-- Modal Body -->
<div class="mt-6">
<div class="grid grid-cols-2 gap-5 text-sm md:text-base">
<div class="md:col-span-1 col-span-2">
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
Username
</div>
</label>
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="Imposter" name="username" type="text" />
</div>
<div class="md:col-span-1 col-span-2">
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
Secret Key
</div>
</label>
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="***************" name="password" type="password" />
</div>
</div>
</div>
<!-- End Modal Body -->
</div>
<!-- Modal Footer -->
<div class="p-6">
<div class="flex items-center justify-end flex-wrap -mb-3">
<button type="submit"
class="bg-accentsecondary w-auto hover:bg-accent rounded-md px-5 py-2 text-color-primary">
Add User
</button>
</div>
<div>
<!-- End Modal Footer -->
</form>
</div>

@ -1,2 +1,146 @@
<p class="text-orange-700 dark:text-orange-400">I already said this page was TBA, what more do you want from me? To
actually code it? 😜 </p>
<!-- Content Title -->
<span class="text-lg font-semibold text-color-secondary md:text-xl lg:text-xl">
Admin Settings
</span>
<!-- End Content Title -->
<div class="bg-tertiary mt-6 h-auto w-full rounded-lg">
<div
class="flex w-full flex-row flex-wrap justify-center space-x-3 border-b border-purple-400/25 py-3 px-6 md:space-x-6 md:px-5 md:py-5 lg:space-x-6 lg:px-5 lg:py-5">
<!-- System Stat Cards -->
<div class="flex flex-wrap lg:flex-row gap-6 pb-5">
<div class="col-span-12 sm:col-span-6 xl:col-span-2 min-w-30">
<div class="bg-secondary shadow-xl rounded-lg p-5 hover:bg-secondary-hover">
<div class="flex">
<i data-lucide="users" class="text-color-accent mr-2"></i>
</div>
<div class="text-color-primary sm:text-lg md:text-3xl font-medium leading-8 mt-6">
<%= totalUsers %>
</div>
<div class="sm:text-sm md:text-base text-base text-slate-500 mt-1">
Total Users
</div>
</div>
</div>
<div class="col-span-12 sm:col-span-6 xl:col-span-2 min-w-30">
<div class="bg-secondary shadow-xl rounded-lg p-5 hover:bg-secondary-hover">
<div class="flex">
<i data-lucide="files" class="text-color-accent mr-2"></i>
<p class="ml-auto text-slate-500 text-sm">
<%= totalSize %>
</p>
</div>
<div class="text-color-primary sm:text-lg md:text-3xl font-medium leading-8 mt-6">
<%= usersDataObj.totalFiles %>
</div>
<div class="sm:text-sm md:text-base text-base text-slate-500 mt-1">
Total Files
</div>
</div>
</div>
<div
class="col-span-6 sm:col-span-3 xl:col-span-1 w-px h-40 border border-r border-accent border-dashed mx-4 xl:mx-6 hidden md:table-cell">
</div>
<div class="col-span-12 sm:col-span-6 xl:col-span-2 min-w-30">
<div class="bg-secondary shadow-xl rounded-lg p-5 hover:bg-secondary-hover ">
<div class="flex">
<i data-lucide="image" class="text-blue-400 mr-2"></i>
<p class="ml-auto text-slate-500 text-sm">
<%= appDataObj.totalImageSize %>
</p>
</div>
<div class="text-color-primary sm:text-lg md:text-3xl font-medium leading-8 mt-6">
<%= appDataObj.allImages.length %>
</div>
<div class="sm:text-sm md:text-base text-base text-slate-500 mt-1">
<% if(appDataObj.allImages.length==1){ %> Image <% } else { %> Images <% } %>
</div>
</div>
</div>
<div class="col-span-12 sm:col-span-6 xl:col-span-2 min-w-30">
<div class="bg-secondary shadow-xl rounded-lg p-5 hover:bg-secondary-hover">
<div class="flex">
<i data-lucide="video" class="text-yellow-400 mr-2"></i>
<p class="ml-auto text-slate-500 text-sm">
<%= appDataObj.totalVideosSize %>
</p>
</div>
<div class="text-color-primary sm:text-lg md:text-3xl font-medium leading-8 mt-6">
<%= appDataObj.allVideos.length %>
</div>
<div class="sm:text-sm md:text-base text-base text-slate-500 mt-1">
<% if(appDataObj.allVideos.length==1){ %> Video <% } else { %> Videos <% } %>
</div>
</div>
</div>
<div class="col-span-12 sm:col-span-6 xl:col-span-2">
<div class="bg-secondary shadow-xl rounded-lg p-5 hover:bg-secondary-hover">
<div class="flex">
<i data-lucide="headphones" class="text-orange-400 mr-2"></i>
<p class="ml-auto text-slate-500 text-sm">
<%= appDataObj.totalAudioSize %>
</p>
</div>
<div class="text-color-primary sm:text-lg md:text-3xl font-medium leading-8 mt-6">
<%= appDataObj.allAudio.length %>
</div>
<div class="sm:text-sm md:text-base text-base text-slate-500 mt-1">
<% if(appDataObj.allAudio.length==1){ %> Audio File <% } else { %> Audio Files <% } %>
</div>
</div>
</div>
<div class="col-span-12 sm:col-span-6 xl:col-span-2 min-w-30">
<div class="bg-secondary shadow-xl rounded-lg p-5 hover:bg-secondary-hover">
<div class="flex">
<i data-lucide="ghost" class="text-green-400 mr-2"></i>
<p class="ml-auto text-slate-500 text-sm">
<%= appDataObj.totalOthersSize %>
</p>
</div>
<div class="text-color-primary sm:text-lg md:text-3xl font-medium leading-8 mt-6">
<%= appDataObj.allOthers.length %>
</div>
<div class="sm:text-sm md:text-base text-slate-500 mt-1">
<% if(appDataObj.allOthers.length==1){ %> Other File <% } else { %> Other Files <% } %>
</div>
</div>
</div>
</div>
<!-- End System Stat Cards -->
</div>
<!-- Admin Tabs -->
<div
class="flex flex-col flex-wrap items-center space-y-4 px-5 pt-5 md:flex-row md:space-y-0 lg:flex-row lg:space-y-0">
<button data-id="1"
class="tabAdmin inline-flex w-full items-center space-x-2 border-b-2 border-accent px-6 pb-5 text-sm font-semibold text-color-primary md:w-auto md:text-base lg:w-auto lg:text-base">
<i data-lucide="Settings" class="text-color-secondary"></i>
<span class="truncate"> App Settings </span>
</button>
<button data-id="2"
class="tabAdmin inline-flex w-full items-center space-x-2 px-6 pb-5 text-sm text-color-primary md:w-auto md:text-base lg:w-auto lg:text-base">
<i data-lucide="Users" class="text-color-secondary"></i>
<span> Users </span>
</button>
</div>
<!-- End Admin Tabs -->
</div>
<div class="mt-10 flex flex-col flex-wrap">
<div class="changeTabAdmin flex-1 basis-full">
<%- include("./tabs/appsettings") %>
</div>
<div class="changeTabAdmin hidden basis-full">
<%- include("./tabs/users") %>
</div>
</div>

@ -0,0 +1,470 @@
<div class="flex lg:flex-row lg:justify-center lg:items-center flex-col flex-wrap">
<div class="flex-none basis-1/2">
<div class="flex flex-col flex-wrap">
<div class="flex flex-col lg:gap-8 gap-3 ">
<% if(success_alert_message !='' ){ %>
<div class="flex-1 bg-tertiary p-6 rounded-md">
<p class="text-green-600 pb-2 text-center">
<%= success_alert_message %>
</p>
</div>
<% } %>
<% if(error_message !='' ){ %>
<div class="flex-1 bg-tertiary p-6 rounded-md">
<p class="text-red-600 pb-2 text-center">
<%= error_message %>
</p>
</div>
<% } %>
<form method="post" action="/admin/save/settings">
<!-- Main Setting -->
<div class="flex-1 bg-tertiary p-6 rounded-md">
<div class="py-9">
<div class="border-b w-full border-gray-300/10 pb-3">
<span class="text-lg text-color-primary font-semibold">
Main Settings
</span>
</div>
</div>
<div class="grid grid-cols-2 gap-5 text-sm md:text-base">
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
App Name
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
This is the title shown in the top left of the app, as well as other various places.
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid"></div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="<%= settingsDatabase.name ? settingsDatabase.name : "DICK" %>" name="name" type="text" />
</div>
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
App Emoji
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
This is the emoji shown in the breadcrumps at the top of the screen, as well as on each file.
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid">
</div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="<%= settingsDatabase.appEmoji ? settingsDatabase.appEmoji : "🍆" %>" name="appEmoji" type="text" />
</div>
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
Site Name
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
This is the text shown on the browser tab, and title in the embed when directly linking the <%= settingsDatabase.name ? settingsDatabase.name.toUpperCase() : "DICK" %> dashboard.
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid"></div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="<%= settingsDatabase.siteTitle ? settingsDatabase.siteTitle : "DICK (Directly Integrated Client for Keisters)" %>" name="siteTitle" type="text" />
</div>
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
Site Description
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
This is the description text shown in the embed when directly linking the <%= settingsDatabase.name ? settingsDatabase.name.toUpperCase() : "DICK" %> dashboard.
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid"></div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="<%= settingsDatabase.siteDescription ? settingsDatabase.siteDescription : "The frontend for your backend" %>" name="siteDescription" type="text" />
</div>
<div class="col-span-2">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
Login Text
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
This is the text shown directly under the app title in the login and register pages!
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid"></div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="<%= settingsDatabase.loginText ? settingsDatabase.loginText : "Sign in to easily manage your nudes." %>" name="loginText" type="text" />
</div>
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
Captcha Site ID
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
If set, this will enable hCaptcha. If configured wrong, your users wont be able to log in. Proceed with caution. <a href="https://www.hcaptcha.com/">learn more here.</a>
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid"></div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<!-- Input -->
<div class="relative">
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="<%= settingsDatabase.captchaSiteID ? settingsDatabase.captchaSiteID : "No site ID entered. Defaulting to vanilla captcha." %>" name="captchaSiteID" type="text" />
<div class="absolute top-0 right-3 h-full">
<div class="flex items-center justify-center space-x-2 h-full">
<!-- Rounded Checkbox -->
<div class="relative">
<input id="captchaCheckbox" name="captchaCheckbox" class="hidden peer"
<%= settingsDatabase.captchaEnabled ? 'checked' : null %> type="checkbox" />
<button type="button"
class="peer border-2 focus:border-accent text-transparent peer-checked:text-color-primary border-[#354567] flex items-center justify-center peer-checked:bg-accent relative h-5 w-5 md:h-6 md:w-6 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg"
width="14" height="14" viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<polyline points="20 6 9 17 4 12">
</polyline>
</svg>
<label class="absolute inset-0 cursor-pointer"
for="captchaCheckbox">
</label>
</button>
</div>
<!-- End Rounded Checkbox -->
<small class="text-color-tertiary text-xs">
Enable
</small>
</div>
</div>
</div>
<!-- End Input -->
</div>
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
Captcha Secret Key
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
This is found in your hCaptcha profile and used to verify captcha checks.
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid"></div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="<%= settingsDatabase.captchaSecretKey ? settingsDatabase.captchaSecretKey : "No site ID entered. Defaulting to vanilla captcha." %>" name="captchaSecretKey" type="text" />
</div>
<div class="col-span-1">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
Private Mode
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
This will remove the app statisics from the login page.
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid"></div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<!-- Switch -->
<label class="switch">
<!-- <span class="pr-2 text-color-secondary">
Off
</span> -->
<input type="checkbox" name="privateModeEnabled" value="privateModeEnabled" <%= settingsDatabase.privateModeEnabled ? 'checked' : null %>>
<span class="check"></span>
<!-- <span class="pl-2 text-color-secondary">
On
</span> -->
</label>
<!-- End Switch -->
</div>
<div class="col-span-1">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
Registration Enabled
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
This allows people to create accounts in your ASS instance.
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid"></div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<label class="switch">
<!-- <span class="pr-2 text-color-primary">
Off
</span> -->
<input type="checkbox" name="registrationEnabled" value="registrationEnabled" <%= settingsDatabase.registrationEnabled ? 'checked' : null %>>
<span class="check"></span>
<!-- <span class="pl-2 text-color-primary">
On
</span> -->
</label>
</div>
<div class="col-span-2">
<div class="flex w-full justify-center items-center bg-tertiary p-6 rounded-md">
<button type="submit" class="bg-accentsecondary w-full hover:bg-accent rounded-md px-8 py-3 text-color-primary">
Save Settings
</button>
</div>
</div>
</div>
</div>
<!-- End Main Setting -->
</form>
<!-- Image Setting -->
<div class="flex-1 bg-tertiary p-6 rounded-md">
<div class="py-9">
<div class="border-b w-full border-gray-300/10 pb-3">
<span class="text-lg text-color-primary font-semibold">
Image Settings
</span>
</div>
</div>
<div class="grid grid-cols-2 gap-5 text-sm md:text-base">
<div class="col-span-2">
<form method="post" enctype="multipart/form-data" action="/admin/upload/logo">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
App Logo
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
This is the logo shown in various areas of the app!
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid"></div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<!-- Drag Drop File -->
<label for="app-logo" class="cursor-pointer flex w-full h-40 border-2 border-form border-dashed rounded-md p-5">
<div class="w-full flex flex-col justify-center items-center h-ful">
<span class="font-semibold text-color-primary text-lg">
Drop files here or click to upload.
</span>
<p class="text-sm text-color-secondary text-center">
All files uploaded are stored directly in the <%= settingsDatabase.name ? settingsDatabase.name.toUpperCase() : "DICK" %> app folder.
</p>
</div>
<input id="app-logo" name="app-logo" class="hidden" type="file" onchange="form.submit()" accept=".jpg, .jpeg, .png">
</label>
<!-- End Drag Drop File -->
</form>
</div>
<div class="col-span-2">
<form method="post" enctype="multipart/form-data" action="/admin/upload/default-pp">
<!-- Label with tooltip -->
<label class="mb-2 block text-color-tertiary text-sm">
<div class="relative w-max">
Default Profile Picture
<div class="group z-20 absolute -right-8 top-0 cursor-help">
<div>
<i data-lucide="help-circle" class="text-color-secondary"></i>
</div>
<div class="flex justify-center items-center">
<div class="lg:w-52 w-48 group-hover:block text-color-light text-sm hidden absolute bottom-10 bg-tooltip p-2 rounded-md">
<div class="break-words">
This is the default profile picture all new users have.
</div>
<div class="absolute -left-[0.10rem] flex justify-center items-center w-full">
<div class="w-0 h-0 border-t-[20px] border-l-[10px] border-r-[10px] border-tooltip border-l-transparent border-r-transparent border-solid"></div>
</div>
</div>
</div>
</div>
</div>
</label>
<!-- End Label with tooltip -->
<!-- Drag Drop File -->
<label for="default-pp" class="cursor-pointer flex w-full h-40 border-2 border-form border-dashed rounded-md p-5">
<div class="w-full flex flex-col justify-center items-center h-ful">
<span class="font-semibold text-color-primary text-lg">
Drop files here or click to upload.
</span>
<p class="text-sm text-color-secondary text-center">
All files uploaded are stored directly in the <%= settingsDatabase.name ? settingsDatabase.name.toUpperCase() : "DICK" %> app folder.
</p>
</div>
<input id="default-pp" name="default-pp" class="hidden" type="file" onchange="form.submit()" accept=".jpg, .jpeg, .png">
</label>
<!-- End Drag Drop File -->
</form>
</div>
</div>
</div>
<!-- End Image Setting -->
</div>
</div>
</div>
<!-- EMPTY CARD ON THE RIGHT -->
<!-- <div class="flex-none lg:basis-1/2 basis-full self-auto lg:self-start lg:pl-3 pl-0 lg:mt-0 mt-3">
<div class="flex flex-col flex-wrap">
<div class="flex flex-col lg:gap-8 gap-3 ">
<div class="flex-1 bg-tertiary p-6 rounded-md">
<div class="py-9">
<div class="border-b w-full border-gray-300/10 pb-3">
<span class="text-lg text-color-primary font-semibold">
Title here
</span>
</div>
</div>
</div>
</div>
</div>
</div> -->
</div>

@ -0,0 +1,121 @@
<div class="flex flex-col items-center justify-center w-full">
<div class="flex-1 lg:w-4/5 w-full">
<div class="flex flex-row flex-wrap w-full items-center mt-4 gap-5">
<!-- Add user -->
<div class="flex-none order-1">
<div class="inline-flex space-x-3">
<button type="button"
class="buttonModal rounded-md bg-accentsecondary py-2 px-2.5 font-medium text-color-primary hover:bg-accent">
Add New user
</button>
<!--
<button
class="custom-bg-button hover:custom-bg-button-hover rounded-md p-3 text-color-secondary">
<i data-lucide="download" class="text-color-primary"></i>
</button>
-->
</div>
</div>
<!-- End Add User -->
<!-- Total User -->
<div class="flex-1 lg:order-2 order-3 lg:basis-auto basis-full">
<div class="text-sm text-color-secondary lg:text-center text-right">
Showing <%= totalUsers %> users
</div>
</div>
<!-- End Total User -->
<!-- Search User -->
<!--
<div
class="lg:flex-none flex-1 lg:basis-80 sm:basis-auto basis-full lg:order-3 order-2">
<div class="sm:flex sm:justify-end">
<form action="#" class="relative h-auto lg:w-full w-full sm:w-3/5">
<input
class="bg-tertiary relative z-10 w-full rounded-md pl-4 pr-10 py-3 text-color-primary shadow-lg placeholder:text-color-secondary focus:border focus:border-gray-300/25 focus:outline-none md:w-full lg:w-full"
value="" placeholder="Search Users" />
<div
class="absolute inset-0 flex flex-row flex-nowrap items-center justify-end px-4">
<button type="button" class="absolute z-10">
<i data-lucide="search" class="text-color-secondary"></i>
</button>
</div>
</form>
</div>
</div>
-->
<!-- End Search user -->
</div>
<div class="mt-8">
<!-- Table User -->
<table class="table-fixed w-full md:text-base text-sm text-black">
<thead class="text-color-primary font-bold">
<tr>
<th class="p-3 text-left">
PROFILE PICTURE
</th>
<th class="p-3 text-left">
USERNAME
</th>
<!--
<th class="p-3 text-center">
UPLOADS
</th>
<th class="p-3 text-center">
DATA USED
</th>
<th class="p-3 text-center">
ACTIONS
</th>
-->
</tr>
</thead>
<tbody>
<% for(const user of dickUsers) {%>
<tr class="text-sm border-b-2 border-table">
<td class="p-4 bg-tertiary rounded-tl-2xl rounded-bl-2xl text-left ">
<img class="relative h-10 w-10 rounded-full object-cover object-center"
src=<%=settingsDatabase.defaultProfilePicture ?
settingsDatabase.defaultProfilePicture : "./images/profile.png" %> alt="Profile" />
</td>
<td class="p-4 bg-tertiary">
<span class="text-color-secondary font-semibold">
<%= user.username %>
</span>
<div class="text-color-tertiary text-xs">
<%= user.role %>
</div>
</td>
<!--
<td class="p-4 bg-tertiary text-color-secondary text-center">
66
</td>
<td class="p-4 bg-tertiary text-color-secondary text-center ">
<div class="sm:border-r border-slate-700 border-r-0">
65GB
</div>
</td>
-->
<!--
<td class="p-4 bg-tertiary rounded-br-2xl rounded-tr-2xl text-center">
<div class="flex justify-center">
<button class="text-red-500 flex sm:flex-row flex-col justify-center items-center space-x-1 ">
<i data-lucide="trash-2" class="text-color-red"></i>
<div>
Delete
</div>
</button>
</div>
</td>
-->
</tr>
<% } %>
</tbody>
</table>
<!-- End Table User -->
</div>
</div>
</div>

@ -1,76 +1,83 @@
<div class="bg-secondary max-h-1/2 max-w-md mx-auto overflow-hidden rounded-lg shadow-xl">
<div class="flex flex-col items-center justify-center overflow-y-auto md:flex-row">
<div class="flex items-center justify-center p-6 sm:p-12">
<div class="w-full">
<h3 class="text-gray-700 dark:text-gray-400 font-bold text-2xl text-center">DICK</h3>
<p class="text-purple-400 text-xs pt-2 pb-5 text-center">Sign in to easily manage your nudes.</p>
<div class="flex flex-col items-center justify-center overflow-y-auto md:flex-row">
<div class="flex items-center justify-center p-6 sm:p-12">
<div class="w-full">
<h3 class="text-color-tertiary font-bold text-2xl text-center">
<%= settingsDatabase.name ? settingsDatabase.name.toUpperCase() : "DICK" %>
</h3>
<p class="text-color-accent text-xs pt-2 pb-5 text-center">
<%= settingsDatabase.loginText ? settingsDatabase.loginText : "Sign in to easily manage your nudes." %>
</p>
<% if(success_alert_message !='' ){ %>
<p class="text-green-600 pb-2 text-center">
<%= success_alert_message %>
</p>
<% } %>
<% if(success_alert_message != ''){ %>
<p class="text-color-primary pb-2"><%= success_alert_message %></p>
<% } %>
<% if(error_message != ''){ %>
<p class="text-color-primary pb-2"><%= error_message %></p>
<% } %>
<% if(error_message !='' ){ %>
<p class="text-red-600 pb-2 text-center">
<%= error_message %>
</p>
<% } %>
<form class="flex flex-col" method="post" action="/auth/login">
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Username</span>
<input
class="form-input block w-full mt-1 rounded text-sm dark:text-gray-400 dark:border-gray-600 dark:bg-gray-700 transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-purple-600 focus:text-purple-400"
placeholder="Imposter" name="username" type="text" />
</label>
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">Secret Key</span>
<input
class="form-input block w-full mt-1 rounded text-sm dark:text-gray-400 dark:border-gray-600 dark:bg-gray-700 transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-purple-600 focus:text-purple-400"
placeholder="***************" name="password" type="password" />
</label>
<form class="flex flex-col" method="post" action="/auth/login">
<label class="block text-sm">
<span class="text-color-tertiary">Username</span>
<input class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="Imposter" name="username" type="text" />
</label>
<label class="block mt-4 text-sm">
<span class="text-color-tertiary">Secret Key</span>
<input class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="***************" name="password" type="password" />
</label>
<% if(settingsDatabase.captchaEnabled) { %>
<div class="h-captcha mt-4" data-theme="dark" data-sitekey=<%= settingsDatabase.captchaSiteID %>></div>
<% } %>
<button
class="block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-color-primary transition-colors duration-150 bg-accentsecondary border border-transparent rounded-lg active:bg-accentsecondary hover:bg-accent focus:outline-none focus:shadow-outline-purple"
type="submit">
Sign In
</button>
</form>
<button
class="block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-color-primary transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple"
type="submit">
Sign In
</button>
</form>
<!-- Add this in when we get system settings that can enable/disable registration
<p class="mt-4 text-center">
<a
class="text-sm font-medium text-purple-600 dark:text-purple-400 hover:underline"
href="3"
>
Create account
</a>
</p>
-->
</div>
</div>
</div>
<% if(settingsDatabase.registrationEnabled) { %>
<p class="mt-4 text-center">
<a class="text-sm font-medium text-color-accent hover:underline" href="/register">
Create account
</a>
</p>
<% } %>
</div>
</div>
</div>
</div>
<div class="bg-secondary h-1/2 max-w-md mt-5 mx-auto rounded-lg shadow-xl">
<div class="flex justify-around p-3">
<div class="text-center">
<i data-lucide="files" class="text-purple-400 mx-auto"></i>
<p class="text-gray-700 dark:text-gray-400">Total Files Saved </p>
<p class="text-gray-700 dark:text-gray-400">
<%= totalData %>
</p>
</div>
<div class="text-center px-5">
<i data-lucide="hard-drive" class="text-purple-400 mx-auto"></i>
<p class="text-gray-700 dark:text-gray-400">Total Space Used </p>
<p class="text-gray-700 dark:text-gray-400">
<%= totalSize %>
</p>
</div>
<div class="text-center">
<i data-lucide="users" class="text-purple-400 mx-auto"></i>
<p class="text-gray-700 dark:text-gray-400">Total Users </p>
<p class="text-gray-700 dark:text-gray-400">
<%= totalUsers %>
</p>
</div>
</div>
</div>
<% if(settingsDatabase.privateModeEnabled == false) { %>
<div class="bg-secondary h-1/2 max-w-md mt-5 mx-auto rounded-lg shadow-xl">
<div class="flex justify-around p-3">
<div class="text-center">
<i data-lucide="files" class="text-color-accent mx-auto"></i>
<p class="text-color-tertiary">Total Files Saved </p>
<p class="text-color-tertiary">
<%= totalData %>
</p>
</div>
<div class="text-center px-5">
<i data-lucide="hard-drive" class="text-color-accent mx-auto"></i>
<p class="text-color-tertiary">Total Space Used </p>
<p class="text-color-tertiary">
<%= totalSize %>
</p>
</div>
<div class="text-center">
<i data-lucide="users" class="text-color-accent mx-auto"></i>
<p class="text-color-tertiary">Total Users </p>
<p class="text-color-tertiary">
<%= totalUsers %>
</p>
</div>
</div>
</div>
<% } %>

@ -0,0 +1,54 @@
<div class="bg-secondary max-h-1/2 max-w-md mx-auto overflow-hidden rounded-lg shadow-xl">
<div class="flex flex-col items-center justify-center overflow-y-auto md:flex-row">
<div class="flex items-center justify-center p-6 sm:p-12">
<div class="w-full">
<h3 class="text-color-tertiary font-bold text-2xl text-center">
<%= settingsDatabase.name ? settingsDatabase.name.toUpperCase() : "DICK" %>
</h3>
<p class="text-color-accent text-xs pt-2 pb-5 text-center">
Please enter a username and desired secret key.
</p>
<% if(success_alert_message !='' ){ %>
<p class="text-green-600 pb-2 text-center">
<%= success_alert_message %>
</p>
<% } %>
<% if(error_message !='' ){ %>
<p class="text-red-600 pb-2 text-center">
<%= error_message %>
</p>
<% } %>
<form class="flex flex-col" method="post" action="/auth/register">
<label class="block text-sm">
<span class="text-color-tertiary">Username</span>
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="Imposter" name="username" type="text" />
</label>
<label class="block mt-4 text-sm">
<span class="text-color-tertiary">Secret Key</span>
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="***************" name="password" type="password" />
</label>
<% if(settingsDatabase.captchaEnabled) { %>
<div class="h-captcha mt-4" data-theme="dark" data-sitekey=<%= settingsDatabase.captchaSiteID %>></div>
<% } %>
<button class="block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-color-primary transition-colors duration-150 bg-accentsecondary border border-transparent rounded-lg active:bg-accentsecondary hover:bg-accent focus:outline-none focus:shadow-outline-purple"
type="submit">
Register
</button>
</form>
<p class="mt-4 text-center">
<a class="text-sm font-medium text-color-accent hover:underline" href="/login">
Login
</a>
</p>
</div>
</div>
</div>
</div>

@ -1,18 +1,13 @@
<!-- Content Title -->
<span class="text-lg font-semibold text-color-secondary md:text-xl lg:text-xl">
Your Profile
</span>
<!-- End Content Title -->
<div class="bg-tertiary mt-6 h-auto w-full rounded-lg">
<!-- Content Profile -->
<div
class="flex w-full flex-row flex-wrap items-center space-x-3 border-b border-purple-400/25 py-3 px-6 md:space-x-6 md:px-5 md:py-5 lg:space-x-6 lg:px-5 lg:py-5">
<div class="flex w-full flex-row flex-wrap items-center space-x-3 border-b border-purple-400/25 py-3 px-6 md:space-x-6 md:px-5 md:py-5 lg:space-x-6 lg:px-5 lg:py-5">
<div class="flex flex-1 px-5 items-center justify-center lg:justify-start pb-5 xl:pb-0">
<div class="flex-none lg:custom-size-profile md:h-40 md:w-40 sm:h-32 sm:w-32 h-20 w-20 relative">
<img class="rounded-full object-cover object-center"
src="/images/profile.png" alt="Profile" />
src=<%= settingsDatabase.defaultProfilePicture ? settingsDatabase.defaultProfilePicture : "./images/profile.png" %> alt="Profile" />
</div>
<div class="ml-5">
<div class="truncate text-base text-color-primary sm:text-xl md:text-2xl lg:text-2xl">
@ -30,7 +25,7 @@
<div class="col-span-12 sm:col-span-6 xl:col-span-2 min-w-30">
<div class="bg-secondary shadow-xl rounded-lg p-5 hover:bg-secondary-hover">
<div class="flex">
<i data-lucide="files" class="text-purple-400 mr-2"></i>
<i data-lucide="files" class="text-color-accent mr-2"></i>
<p class="ml-auto text-slate-500 text-sm">
<%= totalSize %>
</p>
@ -44,7 +39,7 @@
</div>
</div>
<div class="col-span-6 sm:col-span-3 xl:col-span-1 w-px h-40 border border-r border-purple-400 border-dashed mx-4 xl:mx-6 hidden md:table-cell" ></div>
<div class="col-span-6 sm:col-span-3 xl:col-span-1 w-px h-40 border border-r border-accent border-dashed mx-4 xl:mx-6 hidden md:table-cell" ></div>
<div class="col-span-12 sm:col-span-6 xl:col-span-2 min-w-30">
<div class="bg-secondary shadow-xl rounded-lg p-5 hover:bg-secondary-hover ">
@ -113,24 +108,21 @@
</div>
</div>
</div>
</div>
<!-- EndProfile Stat Cards -->
<!-- End Profile Stat Cards -->
</div>
<!-- EndContent Profile -->
<!-- End Profile Header -->
<!-- Content Profile Tabs -->
<div
class="flex flex-col flex-wrap items-center space-y-4 px-5 pt-5 md:flex-row md:space-y-0 lg:flex-row lg:space-y-0">
<button data-id="file-manager"
class="tabsProfile inline-flex w-full items-center space-x-2 border-b-2 border-purple-400 px-6 pb-5 text-sm font-semibold text-color-primary md:w-auto md:text-base lg:w-auto lg:text-base">
<!-- Profile Tabs -->
<div class="flex flex-col flex-wrap items-center space-y-4 px-5 pt-5 md:flex-row md:space-y-0 lg:flex-row lg:space-y-0">
<button data-id="1"
class="tabUser inline-flex w-full items-center space-x-2 border-b-2 border-accent px-6 pb-5 text-sm font-semibold text-color-primary md:w-auto md:text-base lg:w-auto lg:text-base">
<i data-lucide="Files" class="text-color-secondary"></i>
<span class="truncate"> File Manager </span>
</button>
<button data-id="config-gen"
class="tabsProfile inline-flex w-full items-center space-x-2 px-6 pb-5 text-sm text-color-primary md:w-auto md:text-base lg:w-auto lg:text-base">
<button data-id="2"
class="tabUser inline-flex w-full items-center space-x-2 px-6 pb-5 text-sm text-color-primary md:w-auto md:text-base lg:w-auto lg:text-base">
<i data-lucide="Settings" class="text-color-secondary"></i>
<span> Config Gen </span>
<span class="text-xs inline-block py-1 px-2.5 leading-none text-center whitespace-nowrap align-baseline font-bold bg-yellow-600 text-black rounded-full">
@ -138,14 +130,14 @@
</span>
</button>
</div>
<!-- EndContent Profile Tabs -->
<!-- End Profile Tabs -->
</div>
<div class="mt-10 flex flex-col flex-wrap space-x-0 space-y-6 md:flex-row md:flex-nowrap md:space-x-6 md:space-y-0 lg:flex-row lg:flex-nowrap lg:space-x-6 lg:space-y-0">
<div class="tabsProfileContent">
<div class="changeTabUser flex-1 basis-full">
<%- include("./tabs/filemanager") %>
</div>
<div class="tabsProfileContent hidden">
<div class="changeTabUser hidden basis-full">
<%- include("./tabs/configgen") %>
</div>
</div>

@ -0,0 +1,114 @@
<div class=" flex-col flex-wrap mt-6">
<div class="flex xl:flex-row lg:flex-row flex-col lg:gap-8 gap-3 ">
<div class="flex-1 bg-tertiary p-6 rounded-md lg:order-1 md:order-2 order-2">
<div class="py-9">
<div class="border-b w-full border-gray-300/10 pb-3">
<span class="text-lg text-color-primary font-semibold">
Config Setting
</span>
</div>
</div>
<form class="grid grid-cols-2 gap-5 text-sm md:text-base lg:text-base">
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<label class="mb-2 block text-color-tertiary text-sm">Provider</label>
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
value="" placeholder="Some text" type="text" />
</div>
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<label class="mb-2 block text-color-tertiary text-sm">Provider Url</label>
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
value="" placeholder="Some text" type="text" />
</div>
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<label class="mb-2 block text-color-tertiary text-sm">Author</label>
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
value="" placeholder="Some text" type="text" />
</div>
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<label class="mb-2 block text-color-tertiary text-sm">Author Url</label>
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
value="" placeholder="Some text" type="text" />
</div>
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<label class="mb-2 block text-color-tertiary text-sm">Title</label>
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
value="" placeholder="Some text" type="text" />
</div>
<div class="xl:col-span-1 lg:col-span-2 col-span-1">
<label class="mb-2 block text-color-tertiary text-sm">Description</label>
<input
class="form-input block w-full mt-1 rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
value="" placeholder="Some text" type="text" />
</div>
<div>
<label class="mb-3 block text-color-tertiary text-sm">Colour</label>
<input type="color" class="mt-1 custom-color-picker-border">
</div>
<div class="relative">
<label class="relative mb-2 block text-color-tertiary text-sm">Url Type</label>
<select
class="default:bg-gray-30 custom-bg-darker relative block w-full rounded text-sm text-color-tertiary border-form bg-forminput transition duration-500 focus:border-transparent border-transparent focus:ring-0 focus:border-formaccent focus:text-color-accent"
placeholder="Zero Width Space">
<option value="0">Zero Width Space</option>
<option value="1">Mixed-Case Alphas</option>
<option value="2">Gfycat</option>
<option value="3">Original</option>
<option value="3">Timestamp</option>
</select>
</div>
<div class="col-span-2 pt-5">
<div class="flex justify-end">
<button
class="bg-accentsecondary md:w-auto w-full hover:bg-accent rounded-md px-5 py-2 text-color-primary">
Export Config
</button>
</div>
</div>
</form>
</div>
<div
class="place-self-center flex-none h-80 bg-slate-600/25 w-[1px] lg:order-2 md:order-3 lg:block md:hidden hidden">
</div>
<div
class="flex-none bg-tertiary xl:basis-[38rem] lg:basis-[28rem] p-6 rounded-md lg:order-3 md:order-1 order-1 lg:-mt-0 md:-mt-0 -mt-7">
<div class="py-9">
<div class="border-b w-full border-gray-300/10 pb-3">
<span class="text-lg text-color-primary font-semibold">
Example Embed
</span>
</div>
</div>
<div class="xl:px-12 lg:px-0 md:px-0 px-0">
<div
class="p-3 space-y-4 flex flex-col bg-slate-800 rounded-md border-l-8 border-purple-600">
<a href ="/" class="text-color-secondary">
Provider
</a>
<span class="font-semibold text-gray-300 text-lg">
Author
</span>
<a href ="/" class="font-semibold text-cyan-500 text-lg">
Title
</a>
<p class="text-gray-300">
Description
</p>
<img class="w-full h-64 object-contain"
src="./images/profile.png" alt="">
</div>
</div>
</div>
</div>
</div>

@ -1,3 +1,3 @@
<div class="flex-1 basis-full space-y-3 md:basis-2/3 lg:basis-3/4">
<p class="text-orange-700 dark:text-orange-400">This tab is coming to a DICK near you soon </p>
<p class="text-orange-700 dark:text-orange-400">This tab is coming to a <%= settingsDatabase.name ? settingsDatabase.name.toUpperCase() : "DICK" %> near you soon </p>
</div>

@ -4,11 +4,11 @@
<!--We will add this and style it when we add multi-delete, for now eggplant
<input class="accent-blue-700 rounded-md h-3 w-4 md:h-4 md:w-5 lg:h-4 lg:w-4" type="checkbox" />
-->
<span>🍆</span>
<span><%= settingsDatabase.appEmoji ? settingsDatabase.appEmoji : '🍆' %></span>
<div class="relative">
<!-- File Dropdown Button -->
<button class="dropdownFileBtn relative text-gray-400">
<button class="dropdownFileBtn relative text-color-secondary">
<svg class="relative h-3 w-3 md:h-4 md:w-4 lg:h-4 lg:w-4" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
@ -27,7 +27,7 @@
<i data-lucide="link" class="w-4 h-4 mr-2 inline"></i>
Copy Link
</button>
<div id="${id}-copy" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
<div id="${id}-copy" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-color-primary bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip bg-forminput">
Link copied to clipboard!
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
@ -37,7 +37,7 @@
<i data-lucide="trash" class="w-4 h-4 mr-2 inline"></i>
Delete
</button>
<div id="${id}-delete" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
<div id="${id}-delete" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-color-primary bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip bg-forminput">
Item is being deleted, page will refresh when finished.
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
@ -54,7 +54,7 @@
<a target="_blank" href='${itemURL}'>
<div class="mt-2 flex flex-wrap items-center justify-center">
<img class="h-20 w-auto rounded-md object-cover object-center md:h-24 lg:h-24"
src='${itemURL}/direct' alt=${originalName} />
src='${itemURL}/thumbnail' alt=${originalName} />
</div>
</a>
@ -63,10 +63,10 @@
<span class='name-tooltip rounded shadow-lg p-2 bg-primary text-color-primary -mt-8'>${originalName}</span>
<p class="truncate">${originalName}</p>
</a>
<div class="text-xs text-gray-400 md:text-sm lg:text-sm">
<div class="text-xs text-color-secondary md:text-sm lg:text-sm">
${timeStamp}
</div>
<div class="text-xs text-gray-400">
<div class="text-xs text-color-secondary">
${size}
</div>
</div>

@ -1,12 +1,12 @@
<div class="flex-1">
<div class="flex justify-center flex-col items-center space-y-2">
<svg class="animate-spin text-purple-400 w-10 h-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
<svg class="animate-spin text-color-accent w-10 h-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 2v6h-6"></path>
<path d="M3 12a9 9 0 0 1 15-6.7L21 8"></path>
<path d="M3 22v-6h6"></path>
<path d="M21 12a9 9 0 0 1-15 6.7L3 16"></path>
</svg>
<span class="font-semibold text-white">Loading more files...</span>
<span class="font-semibold text-color-primary">Loading more files...</span>
</div>
</div>

@ -1,11 +1,11 @@
<div class="flex-1">
<div class="flex justify-center flex-col items-center space-y-2">
<svg class="text-purple-400 w-10 h-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="text-color-accent w-10 h-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M16 16s-1.5-2-4-2-4 2-4 2"></path>
<line x1="9" y1="9" x2="9.01" y2="9"></line>
<line x1="15" y1="9" x2="15.01" y2="9"></line>
</svg>
<span class="font-semibold text-white">You have no more files to load!</span>
<span class="font-semibold text-color-primary">You have no more files to load! <%= settingsDatabase.appEmoji ? settingsDatabase.appEmoji : '🍆' %></span>
</div>
</div>

@ -4,11 +4,11 @@
<!--We will add this and style it when we add multi-delete, for now eggplant
<input class="accent-blue-700 rounded-md h-3 w-4 md:h-4 md:w-5 lg:h-4 lg:w-4" type="checkbox" />
-->
<span>🍆</span>
<span><%= settingsDatabase.appEmoji ? settingsDatabase.appEmoji : '🍆' %></span>
<div class="relative">
<!-- File Dropdown Button -->
<button class="dropdownFileBtn relative text-gray-400">
<button class="dropdownFileBtn relative text-color-secondary">
<svg class="relative h-3 w-3 md:h-4 md:w-4 lg:h-4 lg:w-4" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
@ -27,7 +27,7 @@
<i data-lucide="link" class="w-4 h-4 mr-2 inline"></i>
Copy Link
</button>
<div id="${id}-copy" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
<div id="${id}-copy" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-color-primary bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip bg-forminput">
Link copied to clipboard!
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
@ -37,7 +37,7 @@
<i data-lucide="trash" class="w-4 h-4 mr-2 inline"></i>
Delete
</button>
<div id="${id}-delete" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
<div id="${id}-delete" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-color-primary bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip bg-forminput">
Item is being deleted, page will refresh when finished.
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
@ -70,10 +70,10 @@
<span class='name-tooltip rounded shadow-lg p-2 bg-primary text-color-primary -mt-8'>${originalName}</span>
<p class="truncate">${originalName}</p>
</a>
<div class="text-xs text-gray-400 md:text-sm lg:text-sm">
<div class="text-xs text-color-secondary md:text-sm lg:text-sm">
${timeStamp}
</div>
<div class="text-xs text-gray-400">
<div class="text-xs text-color-secondary">
${size}
</div>
</div>

@ -3,16 +3,16 @@
<!-- Content Search Bar -->
<form action="#" class="relative mt-3 h-auto w-auto lg:mt-0">
<input
class="bg-tertiary relative z-10 w-full rounded-md px-12 py-2 text-color-primary shadow-lg placeholder:text-gray-400 focus:border focus:border-gray-300/25 focus:outline-none md:w-full lg:w-auto"
class="bg-tertiary relative z-10 w-full rounded-md px-12 py-2 text-color-primary shadow-lg placeholder:text-color-secondary focus:border focus:border-gray-300/25 focus:outline-none md:w-full lg:w-auto"
value="" placeholder="Search Files" />
<div class="absolute inset-0 flex flex-row flex-nowrap items-center justify-between px-4">
<svg class="z-20 flex-none text-gray-400" xmlns="http://www.w3.org/2000/svg" width="20"
<svg class="z-20 flex-none text-color-secondary" xmlns="http://www.w3.org/2000/svg" width="20"
height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<svg class="dropdownSearchBtn z-20 flex-none cursor-pointer text-gray-400"
<svg class="dropdownSearchBtn z-20 flex-none cursor-pointer text-color-secondary"
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
@ -29,19 +29,19 @@
<div>
<label class="mb-2 block text-color-primary">File Name (Fuzzy Search)</label>
<input
class="bg-primary block w-full rounded-md py-2 px-2 text-color-primary placeholder:text-gray-400 focus:outline-none focus:ring focus:ring-gray-300/20"
class="bg-primary block w-full rounded-md py-2 px-2 text-color-primary placeholder:text-color-secondary focus:outline-none focus:ring focus:ring-gray-300/20"
value="" placeholder="nudes" />
</div>
<div>
<label class="mb-2 block text-color-primary">Extension</label>
<input
class="bg-primary block w-full rounded-md py-2 px-2 text-color-primary placeholder:text-gray-400 focus:outline-none focus:ring focus:ring-gray-300/20"
class="bg-primary block w-full rounded-md py-2 px-2 text-color-primary placeholder:text-color-secondary focus:outline-none focus:ring focus:ring-gray-300/20"
value="" placeholder="png" />
</div>
<div>
<label class="mb-2 block text-color-primary">Created At</label>
<input
class="bg-primary block w-full rounded-md py-2 px-2 text-color-primary placeholder:text-gray-400 focus:outline-none focus:ring focus:ring-gray-300/20"
class="bg-primary block w-full rounded-md py-2 px-2 text-color-primary placeholder:text-color-secondary focus:outline-none focus:ring focus:ring-gray-300/20"
value="" placeholder="Make/get a date picker for this slot + extend it to full width" />
</div>
</div>
@ -63,7 +63,7 @@
<div class="flex flex-row flex-nowrap items-center space-x-2">
<!-- Content Upload Form -->
<form action="">
<button id="openFile"
<button id="showFile"
class="rounded-md bg-blue-700 py-2 px-2.5 font-medium text-color-primary hover:bg-blue-600">
Upload New File
</button>

@ -6,7 +6,7 @@
<div class="text-color-primary mt-10 lg:mt-0">
<div class="text-8xl font-medium">404</div>
<div class="text-xl lg:text-3xl font-medium mt-5">Oops. This page does not exist.</div>
<div class="text-lg mt-3">You may have mistyped the address or DICK seems to be malfunctioning</div>
<div class="text-lg mt-3">You may have mistyped the address or <%= settingsDatabase.name ? settingsDatabase.name.toUpperCase() : "DICK" %> seems to be malfunctioning</div>
<a href="/login" class="py-3 px-4 text-color-primary border-white dark:border-darkmode-400 dark:text-slate-200 mt-10">
Back to Home
</a>

@ -1,13 +1,27 @@
<% if(path=='/' ){ %>
<% if(path=='/register' || path=='/login'){ %>
<script src="/js/app.js"></script>
<script src="/js/components.js"></script>
<script src="/js/theme/theme-switcher.js"></script>
<% if( settingsDatabase.captchaEnabled ){ %>
<script src="https://js.hcaptcha.com/1/api.js" async defer>
console.log('test')
</script>
<% } %>
<% } %>
<% if(path=='/login' ){ %>
<% if(path=='/' ){ %>
<script src="/js/app.js"></script>
<script src="/js/dropdowns.js"></script>
<script src="/js/change-color.js"></script>
<!-- <script src="/js/show-password.js"></script>-->
<script src="/js/tabs-user.js"></script>
<script src="/js/theme/theme-switcher.js"></script>
<% } %>
<% if(path=='/admin' ){ %>
<script src="/js/app.js"></script>
<script src="/js/dropdowns.js"></script>
<script src="/js/open-modal.js"></script>
<!-- <script src="/js/show-password.js"></script>-->
<script src="/js/tabs-admin.js"></script>
<script src="/js/theme/theme-switcher.js"></script>
<% } %>

@ -2,25 +2,24 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="The frontend for your backend" />
<meta name="keywords" content="your, keywords" />
<meta name="description" content=<%= settingsDatabase.description ? settingsDatabase.description : "DICK, the frontend for your ASS" %> />
<meta name="author" content="Facinorous-420" />
<meta property="og:description" content="DICK, the frontend for your ASS" />
<meta property="og:description" content=<%= settingsDatabase.description ? settingsDatabase.description : "DICK, the frontend for your ASS" %> />
<meta property="og:url" content="https://github.com/facinorous-420/dick" />
<meta property="og:site_name" content="DICK (Directly Integrated Client for Keisters)" />
<meta property="og:image" content="/images/dick-logo.png" />
<meta property="og:site_name" content=<%= settingsDatabase.siteTitle ? settingsDatabase.siteTitle : "DICK (Directly Integrated Client for Keisters)" %> />
<meta property="og:image" content=<%= settingsDatabase.logo ? settingsDatabase.logo : "/images/logo.png" %> />
<!--meta twitter-->
<meta name="twitter:card" content="summary" />
<meta property="twitter:image" content="/images/dick-logo.png" />
<meta property="twitter:title" content="DICK (Directly Integrated Client for Keisters)" />
<meta property="twitter:description" content="DICK, the frontend for your ASS" />
<meta property="twitter:image" content=<%= settingsDatabase.logo ? settingsDatabase.logo : "/images/logo.png" %> />
<meta property="twitter:title" content=<%= settingsDatabase.siteTitle ? settingsDatabase.siteTitle : "DICK (Directly Integrated Client for Keisters)" %> />
<meta property="twitter:description" content=<%= settingsDatabase.description ? settingsDatabase.description : "DICK, the frontend for your ASS" %> />
<!-- end meta twitter-->
<title>DICK (Directly Intregrated Client for Keisters)</title>
<title><%= settingsDatabase.siteTitle ? settingsDatabase.siteTitle : "DICK (Directly Integrated Client for Keisters)" %></title>
<link rel="stylesheet" href="/css/app.css">
<script src="/js/theme/theme-set.js"></script>
<link rel="icon" type="image/png" href="/images/dick-logo.png">
<link rel="icon" type="image/png" href=<%= settingsDatabase.logo ? settingsDatabase.logo : "/images/logo.png" %>>
</head>

@ -1,9 +1,9 @@
<div
class="traMove order-3 w-full flex-1 basis-1/3 -translate-y-32 shadow-2xl border-b border-gray-300/25 md:border-t lg:border-t transition duration-300 sm:basis-1/2 md:order-2 md:flex-1 md:basis-2/3 md:-translate-y-0 lg:order-2 lg:flex-1 lg:basis-2/3 lg:-translate-y-0">
<div class="flex h-16 w-full flex-row flex-nowrap items-center space-x-2 pl-3 text-color-primary md:pl-7 lg:pl-7">
<a href="#" class="inine-block">🍆</a>
<a href="#" class="inine-block"><%= settingsDatabase.appEmoji ? settingsDatabase.appEmoji : '🍆' %></a>
<svg class="inline-block text-gray-400" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
<svg class="inline-block text-color-secondary" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>

@ -1,4 +1,4 @@
<div class="flex h-0 pl-3 md:h-8 md:pl-7 lg:h-8 lg:pl-7">
<img alt="DICK" class="w-6" src="/images/dick-logo.png">
<span class="text-white ml-3 hidden text-lg font-medium md:block lg:block">DICK</span>
<img alt=<%= settingsDatabase.name ? settingsDatabase.name.toUpperCase() : "DICK" %> class="w-6" src=<%= settingsDatabase.logo ? settingsDatabase.logo : "./images/logo.png" %>>
<span class="text-color-primary ml-3 hidden text-lg font-medium md:block lg:block"><%= settingsDatabase.name ? settingsDatabase.name.toUpperCase() : "DICK" %></span>
</div>

@ -8,9 +8,9 @@
class="<% if(path == '/') { %>bg-secondary <% } else { %> z-20 lg:group-hover:bg-secondary-hover <% } %>
relative inline-flex w-full items-center justify-start space-x-2 rounded-none px-6 pt-3 pb-3 text-color-primary md:w-auto md:justify-center md:rounded-t-2xl lg:w-auto lg:justify-center lg:rounded-t-2xl">
<!-- Navbar List Menu Icon -->
<i data-lucide="home" class="<% if(path == '/') { %> text-color-accent <% } else { %> text-color-secondary <% } %>"></i>
<i data-lucide="home" class="<% if(path == '/') { %> text-color-accent <% } else { %> text-color-light <% } %>"></i>
<!-- EndNavbar List Menu Icon -->
<div class="<% if(path == '/') { %> text-color-primary font-semibold <% } else { %> text-color-secondary <% } %>
<div class="<% if(path == '/') { %> text-color-primary font-semibold <% } else { %> text-color-light <% } %>
relative truncate ">Dashboard</div>
<!-- Rounding To Tab Corner -->
@ -43,14 +43,11 @@
class="<% if(path == '/admin') { %> bg-secondary <% } else { %> lg:group-hover:bg-secondary-hover <% } %>
relative inline-flex w-full items-center justify-start space-x-2 rounded-none px-6 pt-3 pb-3 text-color-primary md:w-auto md:justify-center md:rounded-tr-2xl md:rounded-tl-2xl lg:w-auto lg:justify-center lg:rounded-tr-2xl lg:rounded-tl-2xl">
<!-- Navbar List Menu Icon -->
<i data-lucide="settings" class="<% if(path == '/admin') { %> text-color-accent <% } else { %> text-color-secondary <% } %>"></i>
<i data-lucide="settings" class="<% if(path == '/admin') { %> text-color-accent <% } else { %> text-color-light <% } %>"></i>
<!-- End Navbar List Menu Icon -->
<div class="<% if(path == '/admin') { %> text-color-primary font-semibold <% } else { %> text-color-secondary <% } %>
<div class="<% if(path == '/admin') { %> text-color-primary font-semibold <% } else { %> text-color-light <% } %>
relative truncate">
Admin
<span class="text-xs inline-block py-1 px-2.5 leading-none text-center whitespace-nowrap align-baseline font-bold bg-yellow-600 text-black rounded-full">
TBA
</span>
</div>
<!-- Rounding To Tab Corner -->

@ -4,7 +4,7 @@
<div class="flex flex-row flex-nowrap items-center space-x-6">
<!-- Theme Toggler -->
<button id="theme-toggle" data-tooltip-target="themeTogglerToolTip" type="button"
class="relative text-gray-400 hover:bg-gray-800 dark:hover:bg-gray-300 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
class="relative text-color-secondary hover:bg-gray-800 dark:hover:bg-gray-300 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
@ -17,7 +17,7 @@
</svg>
</button>
<div id="themeTogglerToolTip" role="tooltip"
class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-color-primary bg-gray-900 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
Toggle theme
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
@ -25,7 +25,7 @@
<!-- Navbar Profile Image -->
<div class="dropdownProfileBtn relative z-[100] cursor-pointer hover:scale-110">
<img class="relative h-10 w-10 rounded-full object-cover object-center" src="./images/profile.png"
<img class="relative h-10 w-10 rounded-full object-cover object-center" src=<%= settingsDatabase.defaultProfilePicture ? settingsDatabase.defaultProfilePicture : "./images/profile.png" %>
alt="Profile" />
</div>
<!-- End Navbar Profile Image -->

@ -4,14 +4,14 @@
<span class="w-44 flex-none truncate font-semibold text-color-primary">
<%= user.username %>
</span>
<div class="w-44 flex-none truncate text-xs text-gray-400">
<div class="w-44 flex-none truncate text-xs text-color-secondary">
<% if(hasRole){ %> admin <% } else { %> user <% } %>
</div>
</div>
<div class="grid grid-cols-1">
<!-- This is if we ever decide to add more dropdown options here
<a href="#" class="hover:bg-tertiary-hover inline-flex items-center space-x-2 py-2 px-3">
<svg class="flex-none text-gray-400" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
<svg class="flex-none text-color-secondary" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"></path>
@ -24,11 +24,7 @@
<div class="border-t border-gray-300/25">
<a href="/auth/logout"
class="hover:bg-tertiary-hover inline-flex w-full items-center space-x-2 rounded-b-lg px-3 pt-3 pb-1">
<svg class="text-gray-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"></rect>
<circle cx="16" cy="12" r="3"></circle>
</svg>
<i data-lucide="log-out" class="text-color-secondary"></i>
<div class="text-sm text-color-primary">Logout</div>
</a>
</div>

@ -1,10 +1,13 @@
<%- include("./partials/head") %>
<section class="bg-primary">
<div class="flex flex-col items-center justify-center min-h-screen p-6">
<!-- Page Content -->
<% if(path == '/login'){ %> <%- include("../pages/public/login") %> <% } %>
<%//We will enable this when we add the server logic to toggle registration if(path == '/register'){ %> <%-// include("../pages/public/register") %> <%// } %>
<!-- /Page Content -->
</div>
</section>
<section class="bg-primary">
<div class="flex flex-col items-center justify-center min-h-screen p-6">
<!-- Page Content -->
<% if(path == '/login'){ %> <%- include("../pages/public/login") %> <% } %>
<% if(settingsDatabase.registrationEnabled) { %>
<% if(path == '/register'){ %> <%- include("../pages/public/register") %> <% } %>
<% } %>
<!-- /Page Content -->
</div>
</section>
<%- include("./partials/footer") %>

@ -9,16 +9,20 @@
<div class="bg-secondary my-10 mx-2 max-w-3xl flex-1 rounded-2xl py-6 px-3 md:my-0 md:mx-3 md:max-w-3xl md:px-5 lg:my-0 lg:mx-3 lg:max-w-7xl lg:px-5 xl:mx-3 xl:max-w-full 2xl:mx-3 2xl:max-w-full">
<!-- Page Content -->
<% if(path=='/' ){ %>
<%- include("../pages/user/main") %>
<%- include("../pages/user/main") %>
<% } %>
<% if(path=='/admin' ){ %>
<%- include("../pages/admin/main") %>
<%- include("../pages/admin/main") %>
<% } %>
<!-- /Page Content -->
</div>
</section>
</main>
<% if(path=='/admin' ){ %>
<%- include("../modals/admin/create-user") %>
<% } %>
<%- include("./partials/footer") %>
</body>

@ -1,5 +1,7 @@
const mix = require('laravel-mix')
mix.disableNotifications()
// I'm using laravel-mix to copy all the images, and js to the public folder then transpile the EJSmodules in src/public/modules/app.js
mix.setPublicPath('dist/public')
.copyDirectory("src/public/js", "dist/public/js")

Loading…
Cancel
Save