diff --git a/.gitignore b/.gitignore index 3b06183..d3d26d5 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file +yarn.lock + diff --git a/README.md b/README.md index 60a7ca4..eefdf87 100644 --- a/README.md +++ b/README.md @@ -49,16 +49,18 @@ - ## About The Project Dick was created to be an easy to use front end for Ass 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 You can learn more here. **Planned Feautres:** 1. There are a lot of good ideas out there, to keep track of what is currently planned see the v1.1 project board @@ -72,10 +74,11 @@ The back end is written in Typescript< Running DICK is very simple, though there is no docker container.
You must have `Node >=v16.14.0`, which you should if you're running ASS.
+**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`
+Inside of your dick root folder, you will see `src/constants.ts.example`. Copy this to `constants.ts`
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.```
#### 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 -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.```
- 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. +> **Note** +> ```ASS and the dashboard will be under their own ports.```
+> 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. -
- Open to view the set up steps to run this as a submodule to ass
+### 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 - -
+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 | diff --git a/assets/dick_example_1.png b/assets/dick_example_1.png index 6cb5dd3..deb70b3 100644 Binary files a/assets/dick_example_1.png and b/assets/dick_example_1.png differ diff --git a/assets/dick_example_2.png b/assets/dick_example_2.png index 49cd619..7f7738b 100644 Binary files a/assets/dick_example_2.png and b/assets/dick_example_2.png differ diff --git a/package.json b/package.json index 246e317..6ffd11c 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/Pager.ts b/src/Pager.ts index 8dd9fb0..ed770b9 100644 --- a/src/Pager.ts +++ b/src/Pager.ts @@ -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 = parseDataFile() const users: Array = 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, - users: Array + users: Array, + 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, - users: Array + users: Array, + 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)) diff --git a/src/constants.ts.example b/src/constants.ts.example index c431929..9c05c9b 100644 --- a/src/constants.ts.example +++ b/src/constants.ts.example @@ -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" diff --git a/src/dashboard.ts b/src/dashboard.ts index cef7c45..969cb1a 100644 --- a/src/dashboard.ts +++ b/src/dashboard.ts @@ -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 }) diff --git a/src/database/.gitignore b/src/database/.gitignore new file mode 100644 index 0000000..ec557c4 --- /dev/null +++ b/src/database/.gitignore @@ -0,0 +1,2 @@ +users.json +settings.json \ No newline at end of file diff --git a/src/public/css/tailwind.css b/src/public/css/tailwind.css index 62ac5ee..2139caa 100644 --- a/src/public/css/tailwind.css +++ b/src/public/css/tailwind.css @@ -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; + } } diff --git a/src/public/images/dick-logo.png b/src/public/images/logo.png similarity index 100% rename from src/public/images/dick-logo.png rename to src/public/images/logo.png diff --git a/src/public/images/profile-2.png b/src/public/images/profile-2.png deleted file mode 100644 index 16bc8fe..0000000 Binary files a/src/public/images/profile-2.png and /dev/null differ diff --git a/src/public/images/profile.png b/src/public/images/profile.png index 6ddc51a..1106a50 100644 Binary files a/src/public/images/profile.png and b/src/public/images/profile.png differ diff --git a/src/public/js/change-color.js b/src/public/js/change-color.js new file mode 100644 index 0000000..59c5c54 --- /dev/null +++ b/src/public/js/change-color.js @@ -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) + }) + +}); \ No newline at end of file diff --git a/src/public/js/components.js b/src/public/js/components.js deleted file mode 100644 index 4d86bf9..0000000 --- a/src/public/js/components.js +++ /dev/null @@ -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() - }) \ No newline at end of file diff --git a/src/public/js/dropdowns.js b/src/public/js/dropdowns.js new file mode 100644 index 0000000..7156bc5 --- /dev/null +++ b/src/public/js/dropdowns.js @@ -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(); +}); \ No newline at end of file diff --git a/src/public/js/open-modal.js b/src/public/js/open-modal.js new file mode 100644 index 0000000..30cfd17 --- /dev/null +++ b/src/public/js/open-modal.js @@ -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(); +}); \ No newline at end of file diff --git a/src/public/js/show-password.js b/src/public/js/show-password.js new file mode 100644 index 0000000..792b62e --- /dev/null +++ b/src/public/js/show-password.js @@ -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(); +}); \ No newline at end of file diff --git a/src/public/js/tabs-admin.js b/src/public/js/tabs-admin.js new file mode 100644 index 0000000..89ddc9c --- /dev/null +++ b/src/public/js/tabs-admin.js @@ -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(); +}); + diff --git a/src/public/js/tabs-user.js b/src/public/js/tabs-user.js new file mode 100644 index 0000000..558c8fd --- /dev/null +++ b/src/public/js/tabs-user.js @@ -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(); +}); \ No newline at end of file diff --git a/src/routes/route.admin.ts b/src/routes/route.admin.ts index 6674c5d..11561b1 100644 --- a/src/routes/route.admin.ts +++ b/src/routes/route.admin.ts @@ -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") + }) } diff --git a/src/routes/route.auth.ts b/src/routes/route.auth.ts index 26350cd..7e6a5c5 100644 --- a/src/routes/route.auth.ts +++ b/src/routes/route.auth.ts @@ -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") + }) + } diff --git a/src/routes/route.public.ts b/src/routes/route.public.ts index cca7765..9424cd4 100644 --- a/src/routes/route.public.ts +++ b/src/routes/route.public.ts @@ -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, {}) }) } diff --git a/src/routes/route.user.ts b/src/routes/route.user.ts index 8dd1539..594f102 100644 --- a/src/routes/route.user.ts +++ b/src/routes/route.user.ts @@ -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" diff --git a/src/typings/ASSTypes.d.ts b/src/typings/ASS.d.ts similarity index 100% rename from src/typings/ASSTypes.d.ts rename to src/typings/ASS.d.ts diff --git a/src/typings/database.d.ts b/src/typings/database.d.ts new file mode 100644 index 0000000..d6f0415 --- /dev/null +++ b/src/typings/database.d.ts @@ -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{} + +export interface IUserSettings { + username: string + role: "admin" | "user" + profilePicture: string | null +} \ No newline at end of file diff --git a/src/utils/assJSONStructure.ts b/src/utils/assJSONStructure.ts index c416896..84d7e7a 100644 --- a/src/utils/assJSONStructure.ts +++ b/src/utils/assJSONStructure.ts @@ -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`) diff --git a/src/utils/database.ts b/src/utils/database.ts new file mode 100644 index 0000000..9467a9a --- /dev/null +++ b/src/utils/database.ts @@ -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 => { + 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 => { + 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) + } + } + } + +} diff --git a/src/utils/middleware.ts b/src/utils/middleware.ts new file mode 100644 index 0000000..b36770a --- /dev/null +++ b/src/utils/middleware.ts @@ -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() +} + diff --git a/src/utils/uploads.ts b/src/utils/uploads.ts new file mode 100644 index 0000000..1279a3c --- /dev/null +++ b/src/utils/uploads.ts @@ -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) + } + } \ No newline at end of file diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 8da3b73..e326812 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -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, itemsPerPage: number) => { +export const convertToPaginatedArray = (data: Array, itemsPerPage: number) => { let paginatedArray: Array = [] - 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 -} \ No newline at end of file +} + + diff --git a/tailwind.config.js b/tailwind.config.js index 2796970..a62e0aa 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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: { diff --git a/views/modals/admin/create-user.ejs b/views/modals/admin/create-user.ejs new file mode 100644 index 0000000..e77be84 --- /dev/null +++ b/views/modals/admin/create-user.ejs @@ -0,0 +1,74 @@ +