├── client ├── .depcheckrc ├── public │ ├── otp.png │ ├── apps.png │ ├── cctv.png │ ├── code.png │ ├── home.png │ ├── link.png │ ├── live.png │ ├── robots.txt │ ├── todo.png │ ├── avatar.png │ ├── computer.png │ ├── default.png │ ├── folder.png │ ├── footage.png │ ├── logo192.png │ ├── logo512.png │ ├── snippet.png │ ├── nopreview.png │ ├── not-found.png │ ├── authenticator.png │ ├── network-error.png │ ├── .htaccess │ ├── vite.svg │ └── index.html ├── postcss.config.cjs ├── src │ ├── utils │ │ ├── Constants.js │ │ ├── ToastUtils.js │ │ ├── Helper.js │ │ └── SystemThemes.js │ ├── events.js │ ├── main.jsx │ ├── hooks │ │ ├── useDynamicFilter.jsx │ │ ├── useDetectProtocol.jsx │ │ ├── useCurrentRoute.jsx │ │ └── useSecurityCheck.jsx │ ├── components │ │ ├── NiceViews │ │ │ ├── NicePreferenceHeader.jsx │ │ │ ├── NiceTip.jsx │ │ │ ├── NiceLoader.jsx │ │ │ ├── NiceForm.jsx │ │ │ ├── NiceDrag.jsx │ │ │ ├── NiceBack.jsx │ │ │ ├── NiceClose.jsx │ │ │ ├── NiceLink.jsx │ │ │ ├── NiceCheckbox.jsx │ │ │ ├── NiceButton.jsx │ │ │ ├── NiceModal.jsx │ │ │ ├── NiceInput.jsx │ │ │ └── NiceUploader.jsx │ │ ├── Sidebar │ │ │ ├── SidebarLinkGroup.jsx │ │ │ ├── SidebarButtonItem.jsx │ │ │ └── SidebarLinkItem.jsx │ │ ├── Header │ │ │ └── WelcomeHeader.jsx │ │ ├── Misc │ │ │ ├── SelectList.jsx │ │ │ ├── NetworkError.jsx │ │ │ ├── ServerError.jsx │ │ │ ├── NotFound.jsx │ │ │ ├── ClientError.jsx │ │ │ ├── WelcomeUser.jsx │ │ │ └── NoListing.jsx │ │ ├── Networkdevice │ │ │ └── DeviceStatus.jsx │ │ ├── BuyMeACoffee.jsx │ │ ├── Page │ │ │ ├── ViewPage.css │ │ │ ├── Jodit.css │ │ │ ├── SinglePageItem.jsx │ │ │ └── ViewPage.jsx │ │ ├── Layout │ │ │ ├── Loader.jsx │ │ │ ├── ContentLoader.jsx │ │ │ ├── Layout.jsx │ │ │ └── PrivateRoute.jsx │ │ ├── Settings │ │ │ ├── SingleSettingsItem.jsx │ │ │ └── ThemeSettings.jsx │ │ ├── ClickOutside.jsx │ │ ├── Modals │ │ │ ├── DeletePageModal.jsx │ │ │ ├── DeleteUserModal.jsx │ │ │ ├── DeleteCodeItemModal.jsx │ │ │ ├── DeleteTodoModal.jsx │ │ │ ├── DeleteSnippetItemModal.jsx │ │ │ ├── RemoveInstalledIntegration.jsx │ │ │ ├── ConfirmPacketSendModal.jsx │ │ │ ├── ImageSelectorModal.jsx │ │ │ ├── ManagePublishPageModal.jsx │ │ │ ├── UpdateAvatarModal.jsx │ │ │ ├── UpdatePasswordModal.jsx │ │ │ └── BrandingRemovalModal.jsx │ │ └── Theme │ │ │ └── SingleThemeItem.jsx │ └── App.css ├── .gitignore ├── index.html ├── README.md ├── vite.config.js ├── .eslintrc.cjs └── package.json ├── server ├── apps │ ├── com.html │ │ ├── templates │ │ │ └── response.tpl │ │ ├── public │ │ │ └── logo.png │ │ ├── package.json │ │ ├── manifest.json │ │ └── app.js │ ├── com.github │ │ ├── public │ │ │ └── logo.png │ │ ├── package.json │ │ ├── manifest.json │ │ ├── templates │ │ │ ├── response.tpl │ │ │ └── m-response.tpl │ │ └── app.js │ ├── com.heimdall │ │ ├── public │ │ │ └── logo.png │ │ ├── package.json │ │ ├── manifest.json │ │ ├── templates │ │ │ ├── response.tpl │ │ │ └── m-response.tpl │ │ └── app.js │ ├── com.portainer │ │ ├── public │ │ │ └── logo.png │ │ ├── package.json │ │ ├── manifest.json │ │ └── app.js │ ├── com.proxmox │ │ ├── public │ │ │ └── logo.png │ │ ├── package.json │ │ └── manifest.json │ ├── com.proxyman │ │ ├── public │ │ │ └── logo.png │ │ ├── package.json │ │ ├── manifest.json │ │ ├── templates │ │ │ └── response.tpl │ │ └── app.js │ ├── com.youtube │ │ ├── public │ │ │ └── logo.png │ │ ├── package.json │ │ ├── manifest.json │ │ ├── templates │ │ │ ├── response.tpl │ │ │ └── m-response.tpl │ │ └── app.js │ └── com.truenas.scale │ │ ├── public │ │ └── logo.png │ │ ├── package.json │ │ └── manifest.json ├── public │ └── images │ │ └── integration.png ├── controllers │ ├── home.js │ ├── image.js │ └── auth.js ├── routes │ ├── home.js │ ├── auth.js │ ├── iconpack.js │ ├── totp.js │ ├── todo.js │ ├── page.js │ ├── networkdevice.js │ ├── manage.js │ ├── accounts.js │ ├── snippet.js │ ├── listing.js │ ├── image.js │ └── app.js ├── start-server.js ├── models │ ├── Icon.js │ ├── Page.js │ ├── App.js │ ├── Todo.js │ ├── Snippet.js │ ├── IconPack.js │ ├── NetworkDevice.js │ ├── Authenticator.js │ └── User.js ├── migrations │ ├── 20250118163157-add_avatar_in_existing_users.js │ ├── 20250108055715-encrypt_totp_secrets.js │ ├── 20250119183234-add_site_logo_debranding_info.js │ └── 20250108064334-migrate_old_icons.js ├── package.json ├── migrate-mongo-config.js ├── middlewares │ ├── uploadToLocalMiddleware.js │ └── auth.js ├── utils │ ├── allowedModules.js │ └── apiutils.js └── install-app-dependencies.js ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── package.json ├── Dockerfile ├── SECURITY.md ├── docker-compose.yml └── .gitignore /client/.depcheckrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignores": ["buffer", "postcss"] 3 | } -------------------------------------------------------------------------------- /client/public/otp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/otp.png -------------------------------------------------------------------------------- /server/apps/com.html/templates/response.tpl: -------------------------------------------------------------------------------- 1 |
2 | {{html}} 3 |
-------------------------------------------------------------------------------- /client/public/apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/apps.png -------------------------------------------------------------------------------- /client/public/cctv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/cctv.png -------------------------------------------------------------------------------- /client/public/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/code.png -------------------------------------------------------------------------------- /client/public/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/home.png -------------------------------------------------------------------------------- /client/public/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/link.png -------------------------------------------------------------------------------- /client/public/live.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/live.png -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/todo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/todo.png -------------------------------------------------------------------------------- /client/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /client/public/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/avatar.png -------------------------------------------------------------------------------- /client/public/computer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/computer.png -------------------------------------------------------------------------------- /client/public/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/default.png -------------------------------------------------------------------------------- /client/public/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/folder.png -------------------------------------------------------------------------------- /client/public/footage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/footage.png -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/snippet.png -------------------------------------------------------------------------------- /client/public/nopreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/nopreview.png -------------------------------------------------------------------------------- /client/public/not-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/not-found.png -------------------------------------------------------------------------------- /client/public/authenticator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/authenticator.png -------------------------------------------------------------------------------- /client/public/network-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/client/public/network-error.png -------------------------------------------------------------------------------- /server/apps/com.html/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/server/apps/com.html/public/logo.png -------------------------------------------------------------------------------- /server/public/images/integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/server/public/images/integration.png -------------------------------------------------------------------------------- /server/apps/com.github/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/server/apps/com.github/public/logo.png -------------------------------------------------------------------------------- /server/apps/com.heimdall/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/server/apps/com.heimdall/public/logo.png -------------------------------------------------------------------------------- /server/apps/com.portainer/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/server/apps/com.portainer/public/logo.png -------------------------------------------------------------------------------- /server/apps/com.proxmox/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/server/apps/com.proxmox/public/logo.png -------------------------------------------------------------------------------- /server/apps/com.proxyman/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/server/apps/com.proxyman/public/logo.png -------------------------------------------------------------------------------- /server/apps/com.youtube/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/server/apps/com.youtube/public/logo.png -------------------------------------------------------------------------------- /server/apps/com.truenas.scale/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanjeet990/Astroluma/HEAD/server/apps/com.truenas.scale/public/logo.png -------------------------------------------------------------------------------- /client/src/utils/Constants.js: -------------------------------------------------------------------------------- 1 | //exports CONSTANTS from here 2 | export const CONSTANTS = { 3 | BuyMeACoffee: "https://www.buymeacoffee.com/sanjeet990", 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /server/controllers/home.js: -------------------------------------------------------------------------------- 1 | 2 | exports.home = (req, res) => { 3 | return res.status(200).json({ 4 | error: false, 5 | message: "API running fine." 6 | }); 7 | } -------------------------------------------------------------------------------- /server/routes/home.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { home } = require('../controllers/home'); 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/home', home); 7 | 8 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { doLogin } = require('../controllers/auth'); 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/login', doLogin); 7 | 8 | module.exports = router; -------------------------------------------------------------------------------- /client/public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteBase / 4 | RewriteRule ^index\.html$ - [L] 5 | RewriteCond %{REQUEST_FILENAME} !-f 6 | RewriteCond %{REQUEST_FILENAME} !-d 7 | RewriteCond %{REQUEST_FILENAME} !-l 8 | RewriteRule . /index.html [L] 9 | -------------------------------------------------------------------------------- /server/apps/com.html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.html", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/apps/com.proxmox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.proxmox", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/apps/com.youtube/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.youtube", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/events.js: -------------------------------------------------------------------------------- 1 | // events.js 2 | import mitt from 'mitt'; 3 | 4 | const emitter = mitt(); 5 | 6 | export const PAGE_BOTTOM_EVENT = 'PAGE_BOTTOM_EVENT'; 7 | export const RELOAD_CODE_SNIPPET = 'RELOAD_CODE_SNIPPET'; 8 | export const RELOAD_INSTALLED_APPS = 'RELOAD_INSTALLED_APPS'; 9 | 10 | export default emitter; 11 | -------------------------------------------------------------------------------- /server/apps/com.heimdall/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.heimdall", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/apps/com.portainer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.portainer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/apps/com.proxyman/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.proxyman", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/main.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import './index.css'; 3 | import App from './App'; 4 | import { 5 | RecoilRoot, 6 | } from 'recoil'; 7 | 8 | 9 | const root = ReactDOM.createRoot(document.getElementById('root')); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /server/apps/com.truenas.scale/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.truenas.scale", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/apps/com.github/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.github", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "moment": "^2.30.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /server/start-server.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | 3 | exec('migrate-mongo up', (err, stdout, stderr) => { 4 | if (err) { 5 | console.error("Migration failed: ", stderr); 6 | } else { 7 | console.log("Migration completed.", stdout); 8 | } 9 | 10 | // Start the server regardless of migration result 11 | require('./server.js'); 12 | }); 13 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Dashboard 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/models/Icon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | const { Schema } = mongoose; 4 | 5 | const iconSchema = new Schema({ 6 | iconPath: { 7 | type: String, 8 | required: true, 9 | }, 10 | userId: { 11 | type: Schema.Types.ObjectId, 12 | ref: 'User', 13 | required: false, 14 | } 15 | }, { 16 | timestamps: true, 17 | }); 18 | 19 | const Icon = mongoose.model('Icon', iconSchema); 20 | 21 | module.exports = Icon; -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /server/apps/com.html/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "HTML Code", 3 | "appId": "com.html", 4 | "appIcon": "logo.png", 5 | "description": "", 6 | "alwaysShowDetailedView": false, 7 | "autoRefreshAfterSeconds": 0, 8 | "config": [ 9 | { 10 | "name": "htmlcode", 11 | "label": "HTML Code", 12 | "type": "string", 13 | "required": false, 14 | "placeholder": "Enter HTML Code" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /server/routes/iconpack.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { verifyToken } = require('../middlewares/auth'); 3 | const { create, listIconPacks, addiconpack, deleteIconPack } = require('../controllers/iconpack'); 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/iconpack/list', verifyToken, listIconPacks); 8 | router.post('/iconpack/add', verifyToken, addiconpack); 9 | router.get('/iconpack/delete/:id', verifyToken, deleteIconPack); 10 | 11 | 12 | module.exports = router; -------------------------------------------------------------------------------- /server/apps/com.youtube/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Youtube", 3 | "appId": "com.youtube", 4 | "appIcon": "logo.png", 5 | "description": "", 6 | "autoRefreshAfterSeconds": 0, 7 | "alwaysShowDetailedView": true, 8 | "config": [ 9 | { 10 | "name": "apiKey", 11 | "label": "Youtube API v3 Key", 12 | "type": "password", 13 | "required": true, 14 | "placeholder": "Enter your Youtube API v3 Key" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /client/src/hooks/useDynamicFilter.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useSetRecoilState } from 'recoil'; 3 | import { isFilterVisibleState } from '../atoms'; 4 | 5 | const useDynamicFilter = (enabled) => { 6 | const setFilterEnabled = useSetRecoilState(isFilterVisibleState); 7 | 8 | useEffect(() => { 9 | setFilterEnabled(enabled); 10 | return () => { 11 | setFilterEnabled(enabled); 12 | }; 13 | }, [enabled, setFilterEnabled]); 14 | }; 15 | 16 | export default useDynamicFilter; -------------------------------------------------------------------------------- /client/src/components/NiceViews/NicePreferenceHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const NicePreferenceHeader = ({ title }) => { 5 | 6 | return ( 7 |
8 |
{title}
9 |
10 | ) 11 | } 12 | 13 | NicePreferenceHeader.propTypes = { 14 | title: PropTypes.string 15 | } 16 | 17 | const MemoizedComponent = React.memo(NicePreferenceHeader); 18 | export default MemoizedComponent; -------------------------------------------------------------------------------- /server/apps/com.heimdall/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Heimdall", 3 | "appId": "com.heimdall", 4 | "appIcon": "logo.png", 5 | "description": "Displays statistics from Heimdall.", 6 | "alwaysShowDetailedView": true, 7 | "autoRefreshAfterSeconds": 60, 8 | "config": [ 9 | { 10 | "name": "overrideurl", 11 | "label": "Heimdall URL", 12 | "type": "string", 13 | "required": false, 14 | "placeholder": "If different from the Link URL, enter URL" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /client/src/components/Sidebar/SidebarLinkGroup.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const SidebarLinkGroup = ({ children, activeCondition }) => { 5 | const [open, setOpen] = useState(activeCondition); 6 | 7 | const handleClick = () => { 8 | setOpen(!open); 9 | }; 10 | 11 | return
  • {children(handleClick, open)}
  • ; 12 | }; 13 | 14 | SidebarLinkGroup.propTypes = { 15 | children: PropTypes.func.isRequired, 16 | activeCondition: PropTypes.bool, 17 | }; 18 | 19 | export default SidebarLinkGroup; 20 | -------------------------------------------------------------------------------- /client/src/hooks/useDetectProtocol.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useDetectProtocol = () => { 4 | const [protocol, setProtocol] = useState(''); 5 | 6 | useEffect(() => { 7 | // Get the protocol from the window.location object 8 | const protocol = window.location.protocol?.replace(':', '') || 'http'; 9 | 10 | // Update the state with the protocol 11 | setProtocol(() => protocol); 12 | }, []); // Empty dependency array ensures this effect runs once on mount 13 | 14 | return protocol; 15 | }; 16 | 17 | export default useDetectProtocol; -------------------------------------------------------------------------------- /server/routes/totp.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { verifyToken } = require('../middlewares/auth'); 3 | const { listTotp, deleteTotp, saveTotp, totpDetails, reorderTotp } = require('../controllers/totp'); 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/totp/save', verifyToken, saveTotp); 8 | router.get('/totp/list', verifyToken, listTotp); 9 | router.get('/totp/delete/:authId', verifyToken, deleteTotp); 10 | router.get('/totp/:authId', verifyToken, totpDetails); 11 | router.post('/totp/reorder', verifyToken, reorderTotp); 12 | 13 | module.exports = router; -------------------------------------------------------------------------------- /client/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | server: { 7 | host: '0.0.0.0', 8 | port: 3000 9 | }, 10 | plugins: [ 11 | react() 12 | ], 13 | build: { 14 | rollupOptions: { 15 | output: { 16 | manualChunks(id) { 17 | if (id.includes('node_modules')) { 18 | return id.toString().split('node_modules/')[1].split('/')[0].toString(); 19 | } 20 | } 21 | } 22 | } 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /client/src/hooks/useCurrentRoute.jsx: -------------------------------------------------------------------------------- 1 | // useCurrentRoute.js 2 | 3 | import { useEffect } from 'react'; 4 | import { useSetRecoilState } from 'recoil'; 5 | import { activeRouteState } from '../atoms'; 6 | 7 | const useCurrentRoute = (initialRoute = "/") => { 8 | const setActiveRoute = useSetRecoilState(activeRouteState); 9 | 10 | useEffect(() => { 11 | setActiveRoute(initialRoute); 12 | return () => { 13 | setActiveRoute("/"); 14 | }; 15 | }, [initialRoute, setActiveRoute]); 16 | 17 | return (currentRoute) => { 18 | setActiveRoute(currentRoute); 19 | }; 20 | }; 21 | 22 | export default useCurrentRoute; 23 | -------------------------------------------------------------------------------- /client/src/utils/ToastUtils.js: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | 3 | const makeToast = (type, message) => { 4 | 5 | if (!message || message === "undefined" || message === "null" || message === "") return; 6 | 7 | const customId = `t: ${type}, m: ${message}`; 8 | 9 | if (type === 'error') { 10 | toast.error(message, { toastId: customId }); 11 | } else if (type === 'info') { 12 | toast.info(message, { toastId: customId }); 13 | } else if (type === 'warning') { 14 | toast.warning(message, { toastId: customId }); 15 | } else { 16 | toast.success(message, { toastId: customId }); 17 | } 18 | }; 19 | 20 | export default makeToast; -------------------------------------------------------------------------------- /server/models/Page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | const { Schema } = mongoose; 4 | 5 | const pageSchema = new Schema({ 6 | pageTitle: { 7 | type: String, 8 | required: true, 9 | default: "", 10 | }, 11 | pageContent: { 12 | type: String, 13 | required: false, 14 | default: null, 15 | }, 16 | isPublished: { 17 | type: Boolean, 18 | required: true, 19 | default: true, 20 | }, 21 | userId: { 22 | type: Schema.Types.ObjectId, 23 | ref: 'User', 24 | required: true, 25 | } 26 | }, { 27 | timestamps: true, 28 | }); 29 | 30 | const Page = mongoose.model('Page', pageSchema); 31 | 32 | module.exports = Page; -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /server/routes/todo.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { verifyToken } = require('../middlewares/auth'); 3 | const { saveTodo, listTodo, completeTodo, deleteTodo } = require('../controllers/todo'); 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/todo/item', verifyToken, saveTodo); 8 | router.get('/todo/completion/:todoId', verifyToken, completeTodo); 9 | router.get('/todo/:todoId/delete/:itemId', verifyToken, deleteTodo); 10 | router.get('/todo/:todoId/items', verifyToken, listTodo); 11 | router.get('/todo/all/items/:completion/:filter/:page', verifyToken, listTodo); 12 | router.get('/todo/:todoId/items/:completion/:filter/:page', verifyToken, listTodo); 13 | 14 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/page.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { verifyToken } = require('../middlewares/auth'); 3 | const { savePage, pageList, pageInfo, deletePage, managePage } = require('../controllers/page'); 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/page/save', verifyToken, savePage); 8 | router.get('/page/list', verifyToken, pageList); 9 | router.get('/page/list/:active', verifyToken, pageList); 10 | router.get('/page/info/:pageId', verifyToken, pageInfo); 11 | router.get('/page/info/:pageId/:active', verifyToken, pageInfo); 12 | router.get('/page/delete/:pageId', verifyToken, deletePage); 13 | router.get('/page/action/:pageId/:action', verifyToken, managePage); 14 | 15 | module.exports = router; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astroluma", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server/server.js", 6 | "dependencies": { 7 | "sharp": "*", 8 | "nodemon": "^3.1.4" 9 | }, 10 | "optionalDependencies": { 11 | "@rollup/rollup-linux-x64-gnu": "*" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "postinstall": "cd server && npm install && cd ../client && npm install", 16 | "dev:be": "cd server && nodemon server.js", 17 | "dev:fe": "npm run dev --prefix client", 18 | "setup": "cd server && npm run install-app-deps", 19 | "server": "cd server && npm run server" 20 | }, 21 | "author": "Sanjeet990", 22 | "license": "GNU" 23 | } 24 | -------------------------------------------------------------------------------- /server/apps/com.github/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Github", 3 | "appId": "com.github", 4 | "appIcon": "logo.png", 5 | "description": "", 6 | "autoRefreshAfterSeconds": 300, 7 | "alwaysShowDetailedView": true, 8 | "config": [ 9 | { 10 | "name": "username", 11 | "label": "Username", 12 | "type": "string", 13 | "required": true, 14 | "placeholder": "Github username or email" 15 | }, 16 | { 17 | "name": "password", 18 | "label": "Personal access token", 19 | "type": "password", 20 | "required": true, 21 | "placeholder": "Github personal access token" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /server/apps/com.html/app.js: -------------------------------------------------------------------------------- 1 | const connectionTest = async (testerInstance) => { 2 | //No need to connect to third party services, 3 | //so just report as connected 4 | 5 | await testerInstance.connectionSuccess(); 6 | } 7 | 8 | const initialize = async (application) => { 9 | 10 | const htmlcode = application?.config?.htmlcode; 11 | 12 | try { 13 | 14 | const variables = [ 15 | { key: '{{html}}', value: htmlcode } 16 | ]; 17 | 18 | await application.sendResponse('response.tpl', 200, variables); 19 | 20 | } catch (error) { 21 | await application.sendError(error); 22 | } 23 | } 24 | 25 | global.initialize = initialize; 26 | global.connectionTest = connectionTest; 27 | -------------------------------------------------------------------------------- /server/apps/com.truenas.scale/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "TrueNas Scale", 3 | "appId": "com.truenas.scale", 4 | "appIcon": "logo.png", 5 | "description": "", 6 | "autoRefreshAfterSeconds": 300, 7 | "alwaysShowDetailedView": true, 8 | "config": [ 9 | { 10 | "name": "appurl", 11 | "label": "App Url", 12 | "type": "string", 13 | "required": false, 14 | "placeholder": "Enter TrueNas Scale URL" 15 | }, 16 | { 17 | "name": "apiKey", 18 | "label": "API Key", 19 | "type": "password", 20 | "required": true, 21 | "placeholder": "Enter TrueNas Scale API Key" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceTip.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const NiceTip = ({ title, children }) => { 5 | 6 | return ( 7 |
    8 | { 9 | title &&
    10 | {title} 11 |
    12 | } 13 |
    14 | {children} 15 |
    16 |
    17 | ) 18 | } 19 | 20 | NiceTip.propTypes = { 21 | children: PropTypes.node 22 | } 23 | 24 | const MemoizedComponent = React.memo(NiceTip); 25 | export default MemoizedComponent; -------------------------------------------------------------------------------- /client/src/hooks/useSecurityCheck.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useSecurityCheck = () => { 4 | const [isSecure, setIsSecure] = useState(false); 5 | 6 | useEffect(() => { 7 | // Get the hostname and protocol from the window.location object 8 | const hostname = window.location.hostname; 9 | const protocol = window.location.protocol; 10 | 11 | // Check if the hostname is localhost 12 | const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1'; 13 | 14 | // Check if the protocol is HTTPS 15 | const isHTTPS = protocol === 'https:'; 16 | 17 | // Update the state based on security checks 18 | setIsSecure(isLocalhost || isHTTPS); 19 | }, []); // Empty dependency array ensures this effect runs once on mount 20 | 21 | return isSecure; 22 | }; 23 | 24 | export default useSecurityCheck; -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceLoader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const NiceLoader = ({className = "text-loaderColor"}) => { 5 | 6 | return ( 7 |
    10 | Loading... 13 |
    14 | ) 15 | } 16 | 17 | NiceLoader.propTypes = { 18 | className: PropTypes.string 19 | } 20 | 21 | const MemoizedComponent = React.memo(NiceLoader); 22 | export default MemoizedComponent; -------------------------------------------------------------------------------- /server/routes/networkdevice.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { verifyToken } = require('../middlewares/auth'); 3 | const { saveDevice, deleteDevice, deviceDetails, reorderDevices, wakeDevice, listDevices, listDbDevices } = require('../controllers/networkdevice'); 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/networkdevices/save/device', verifyToken, saveDevice); 8 | router.get('/networkdevices/devices', verifyToken, listDevices); 9 | router.get('/networkdevices/db/devices', verifyToken, listDbDevices); 10 | router.get('/networkdevices/delete/:deviceId', verifyToken, deleteDevice); 11 | router.get('/networkdevices/device/:deviceId', verifyToken, deviceDetails); 12 | router.post('/networkdevices/device/reorder', verifyToken, reorderDevices); 13 | router.get('/networkdevices/wake/:deviceId', verifyToken, wakeDevice); 14 | 15 | module.exports = router; -------------------------------------------------------------------------------- /server/migrations/20250118163157-add_avatar_in_existing_users.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(db, client) { 3 | 4 | const usersCollection = db.collection('users'); 5 | 6 | const allUsers = await usersCollection.find({}).toArray(); 7 | 8 | allUsers.forEach(async (user) => { 9 | if (!user.userAvatar) { 10 | const avatar = { 11 | iconUrl: "defaultuser", 12 | iconUrlLight: null, 13 | iconProvider: 'com.astroluma.self' 14 | }; 15 | await usersCollection.updateOne({ _id: user._id }, { $set: { userAvatar: avatar } }); 16 | } 17 | }); 18 | }, 19 | 20 | async down(db, client) { 21 | // TODO write the statements to rollback your migration (if possible) 22 | // Example: 23 | // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}}); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /client/src/components/Header/WelcomeHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import WelcomeUser from '../Misc/WelcomeUser'; 3 | import WeatherWidget from './WeatherWidget'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const WelcomeHeader = () => { 7 | return ( 8 |
    9 |
    10 |
    11 |
    12 | 13 | 14 | 15 |
    16 |
    17 |
    18 |
    19 | ); 20 | }; 21 | 22 | const MemoizedComponent = React.memo(WelcomeHeader); 23 | export default MemoizedComponent; -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const NiceForm = ({ onSubmit, children }) => { 5 | const containerRef = useRef(null); 6 | 7 | const handleKeyDown = (event) => { 8 | if (event.key === 'Enter') { 9 | const activeElement = document.activeElement; 10 | if (containerRef.current?.contains(activeElement)) { 11 | onSubmit(); 12 | } 13 | } 14 | }; 15 | 16 | return ( 17 |
    18 | {children} 19 |
    20 | ); 21 | }; 22 | 23 | NiceForm.propTypes = { 24 | onSubmit: PropTypes.func.isRequired, 25 | children: PropTypes.node.isRequired, 26 | }; 27 | 28 | const MemoizedComponent = React.memo(NiceForm); 29 | export default MemoizedComponent; 30 | -------------------------------------------------------------------------------- /server/routes/manage.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { 3 | dashboard, 4 | saveSettings, 5 | getSetting, 6 | saveThemeSettings, 7 | saveWeatherSettings, 8 | weatherData 9 | } = require('../controllers/manage'); 10 | const { verifyToken } = require('../middlewares/auth'); 11 | 12 | const router = express.Router(); 13 | 14 | // Dashboard route 15 | router.get('/dashboard', verifyToken, dashboard); 16 | 17 | // Weather data route 18 | router.get('/weather', verifyToken, weatherData); 19 | 20 | // Settings routes 21 | router.post('/settings', verifyToken, saveSettings); // Save all settings 22 | router.post('/settings/theme', verifyToken, saveThemeSettings); // Save theme settings 23 | router.post('/settings/weather', verifyToken, saveWeatherSettings); // Save weather settings 24 | router.get('/settings', verifyToken, getSetting); // Get current settings 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /server/routes/accounts.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { verifyToken } = require('../middlewares/auth'); 3 | const { userList, accountInfo, saveAccount, deleteUser, changePassword, updateAvatar, updateOwnAvatar, doDebrand, doRebrand } = require('../controllers/accounts'); 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/accounts/save', verifyToken, saveAccount); 8 | router.get('/accounts/list', verifyToken, userList); 9 | router.get('/accounts/info/:userId', verifyToken, accountInfo); 10 | router.get('/accounts/delete/:userId', verifyToken, deleteUser); 11 | router.post('/accounts/password/:userId', verifyToken, changePassword); 12 | router.post('/accounts/avatar/:userId', verifyToken, updateAvatar); 13 | router.post('/accounts/avatar', verifyToken, updateOwnAvatar); 14 | router.get('/accounts/debrand', verifyToken, doDebrand); 15 | router.get('/accounts/rebrand', verifyToken, doRebrand); 16 | 17 | module.exports = router; -------------------------------------------------------------------------------- /server/models/App.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | const { Schema } = mongoose; 4 | 5 | const appsSchema = new Schema({ 6 | appName: { 7 | type: String, 8 | required: true, 9 | }, 10 | appId: { 11 | type: String, 12 | required: true, 13 | }, 14 | version: { 15 | type: String, 16 | required: true, 17 | }, 18 | description: { 19 | type: String, 20 | required: true, 21 | }, 22 | appIcon: { 23 | type: String, 24 | required: true, 25 | }, 26 | npmInstalled:{ 27 | type: Number, 28 | require: false, 29 | default: 0, 30 | }, 31 | coreSettings:{ 32 | type: Boolean, 33 | require: false, 34 | default: false, 35 | }, 36 | configured:{ 37 | type: Boolean, 38 | require: false, 39 | default: true, 40 | } 41 | }, { 42 | timestamps: true, 43 | }); 44 | 45 | const Icon = mongoose.model('App', appsSchema); 46 | 47 | module.exports = Icon; -------------------------------------------------------------------------------- /client/src/components/Misc/SelectList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const SelectList = ({listItems, selected, onChange}) => { 5 | return ( 6 | 17 | ); 18 | }; 19 | 20 | SelectList.propTypes = { 21 | listItems: PropTypes.array.isRequired, 22 | selected: PropTypes.string.isRequired, 23 | onChange: PropTypes.func.isRequired 24 | }; 25 | 26 | const MemoizedComponent = React.memo(SelectList); 27 | export default MemoizedComponent; 28 | -------------------------------------------------------------------------------- /client/src/components/Networkdevice/DeviceStatus.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const DeviceStatus = ({ status, instantFetch, loading, motionId }) => { 6 | // Determine the color based on the status prop 7 | let colorClass = status ? 'bg-green-500' : 'bg-red-500'; 8 | 9 | if (loading) { 10 | colorClass = 'fadeYellowAmber'; 11 | } 12 | 13 | return ( 14 | instantFetch(e)} 17 | className={`w-4 h-4 rounded-full ${colorClass}`} /> 18 | 19 | ); 20 | }; 21 | 22 | DeviceStatus.propTypes = { 23 | status: PropTypes.bool.isRequired, 24 | instantFetch: PropTypes.func.isRequired, 25 | loading: PropTypes.bool.isRequired, 26 | motionId: PropTypes.string 27 | } 28 | 29 | const MemoizedComponent = React.memo(DeviceStatus); 30 | export default MemoizedComponent; -------------------------------------------------------------------------------- /server/models/Todo.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const todoSchema = new mongoose.Schema({ 4 | todoItem: { 5 | type: String, 6 | required: true, 7 | }, 8 | completed: { 9 | type: Boolean, 10 | required: false, 11 | default: false 12 | }, 13 | dueDate: { 14 | type: Date, 15 | required: false, 16 | default: null 17 | }, 18 | priority: { 19 | type: Number, 20 | enum: [1, 2, 3], 21 | required: false, 22 | default: 3 23 | }, 24 | sortOrder: { 25 | type: Number, 26 | required: false, 27 | default: 9999 28 | }, 29 | parent: { 30 | type: mongoose.Schema.Types.ObjectId, 31 | ref: 'Listing', 32 | required: false, 33 | default: null 34 | }, 35 | userId: { 36 | type: mongoose.Schema.Types.ObjectId, 37 | ref: 'User', 38 | required: false, 39 | default: null 40 | } 41 | }, { 42 | timestamps: true, 43 | }); 44 | 45 | const Todo = mongoose.model('Todo', todoSchema); 46 | 47 | module.exports = Todo; -------------------------------------------------------------------------------- /server/apps/com.portainer/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Portainer", 3 | "appId": "com.portainer", 4 | "appIcon": "logo.png", 5 | "description": "Displays statistics from Portainer.", 6 | "alwaysShowDetailedView": true, 7 | "autoRefreshAfterSeconds": 60, 8 | "config": [ 9 | { 10 | "name": "overrideurl", 11 | "label": "Portainer URL", 12 | "type": "string", 13 | "required": false, 14 | "placeholder": "If different from the Link URL, enter URL" 15 | }, 16 | { 17 | "name": "username", 18 | "label": "Username", 19 | "type": "string", 20 | "required": true, 21 | "placeholder": "Enter portainer username" 22 | }, 23 | { 24 | "name": "password", 25 | "label": "Password", 26 | "type": "password", 27 | "required": true, 28 | "placeholder": "Enter portainer password" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /server/models/Snippet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | const { Schema } = mongoose; 4 | 5 | const snippetItemSchema = new Schema({ 6 | snippetFilename: { 7 | type: String, 8 | required: false, 9 | }, 10 | snippetCode: { 11 | type: String, 12 | required: false, 13 | }, 14 | snippetLanguage: { 15 | type: String, 16 | required: false, 17 | } 18 | }); 19 | 20 | const snippetSchema = new Schema({ 21 | snippetTitle: { 22 | type: String, 23 | required: true, 24 | }, 25 | snippetLanguage: { 26 | type: String, 27 | required: false, 28 | }, 29 | parent: { 30 | type: Schema.Types.ObjectId, 31 | ref: 'Listing', 32 | required: false, 33 | }, 34 | userId: { 35 | type: Schema.Types.ObjectId, 36 | ref: 'User', 37 | required: true, 38 | }, 39 | snippetItems: [snippetItemSchema], 40 | }, { 41 | timestamps: true, 42 | }); 43 | 44 | const Snippet = mongoose.model('Snippet', snippetSchema); 45 | 46 | module.exports = Snippet; 47 | -------------------------------------------------------------------------------- /server/apps/com.proxyman/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Nginx Proxy Manager", 3 | "appId": "com.proxyman", 4 | "appIcon": "logo.png", 5 | "description": "Displays statistics from the Nginx Proxy Manager", 6 | "autoRefreshAfterSeconds": 300, 7 | "alwaysShowDetailedView": true, 8 | "config": [ 9 | { 10 | "name": "overrideurl", 11 | "label": "NGINX Proxy Manager URL", 12 | "type": "string", 13 | "required": false, 14 | "placeholder": "If different from the Link URL, enter URL" 15 | }, 16 | { 17 | "name": "email", 18 | "label": "Email", 19 | "type": "string", 20 | "required": true, 21 | "placeholder": "Enter your email" 22 | }, 23 | { 24 | "name": "password", 25 | "label": "Password", 26 | "type": "password", 27 | "required": true, 28 | "placeholder": "Enter your password" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /server/routes/snippet.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { verifyToken } = require('../middlewares/auth'); 3 | const { saveSnippet, listSnippet, listFilesInSnippet, saveCodeToSnippet, deleteCodeFromSnippet, deleteSnippet } = require('../controllers/snippet'); 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/snippet/item', verifyToken, saveSnippet); 8 | router.get('/snippet/list/:snippetId', verifyToken, listFilesInSnippet); 9 | router.post('/snippet/save/:snippetId', verifyToken, saveCodeToSnippet); 10 | router.get('/snippet/:snippetId/delete', verifyToken, deleteSnippet); 11 | router.get('/snippet/:snippetId/delete/:codeId', verifyToken, deleteCodeFromSnippet); 12 | router.get('/snippet/:snippetId/items/search/:query', verifyToken, listSnippet); 13 | router.get('/snippet/:snippetId/items/:page/search/:query', verifyToken, listSnippet); 14 | router.get('/snippet/:snippetId/items', verifyToken, listSnippet); 15 | router.get('/snippet/:snippetId/items/:page', verifyToken, listSnippet); 16 | 17 | module.exports = router; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: Sanjeet990 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Container logs:** 27 | If applicable, add your container logs to better understand the problem. 28 | 29 | **Your docker-compose.yml content:** 30 | If applicable, add your docker compose to better understand the problem. 31 | ```yaml 32 | // Put docker-compose.yml code here 33 | ``` 34 | 35 | **Other details:** 36 | - Host CPU Type [e.g. x64]: 37 | - Host OS [e.g. Ubuntu 22.04]: 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrolumaserver", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "prestart": "echo \"No Prestart\"", 8 | "predev": "echo \"No predev\"", 9 | "install-app-deps": "node install-app-dependencies.js", 10 | "start": "cd client && npm run build && cd .. && nodemon server.js", 11 | "dev": "nodemon server.js", 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "server": "node start-server.js" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "axios": "^1.7.4", 19 | "cors": "^2.8.5", 20 | "crypto-js": "^4.2.0", 21 | "dotenv": "^16.3.1", 22 | "express": "^4.21.2", 23 | "extract-zip": "^2.0.1", 24 | "jsonwebtoken": "^9.0.2", 25 | "md5": "^2.3.0", 26 | "migrate-mongo": "^11.0.0", 27 | "mongoose": "^8.8.3", 28 | "multer": "^1.4.5-lts.1", 29 | "nodemon": "^3.1.4", 30 | "ping": "^0.4.4", 31 | "sharp": "^0.33.5", 32 | "wake_on_lan": "^1.0.0", 33 | "ws": "^8.17.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/migrations/20250108055715-encrypt_totp_secrets.js: -------------------------------------------------------------------------------- 1 | const { getSecretKey } = require("../utils/apiutils"); 2 | const CryptoJS = require('crypto-js'); 3 | 4 | 5 | module.exports = { 6 | async up(db, client) { 7 | 8 | const secret = getSecretKey(); 9 | 10 | const authenticatorCollection = db.collection('authenticators'); 11 | 12 | const allAuthenticators = await authenticatorCollection.find({}); 13 | 14 | allAuthenticators.forEach(async (singleAuth) => { 15 | //ENcrypt the TOTP secrets 16 | const secretKey = singleAuth.secretKey; 17 | const encryptedSecret = CryptoJS.AES.encrypt(secretKey, secret).toString(); 18 | 19 | //Update the listing 20 | await authenticatorCollection.updateOne({ _id: singleAuth._id }, { $set: { secretKey: encryptedSecret } }); 21 | }); 22 | 23 | }, 24 | 25 | async down(db, client) { 26 | // TODO write the statements to rollback your migration (if possible) 27 | // Example: 28 | // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}}); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /server/migrations/20250119183234-add_site_logo_debranding_info.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(db, client) { 3 | 4 | const usersCollection = db.collection('users'); 5 | 6 | const allUsers = await usersCollection.find({}).toArray(); 7 | 8 | allUsers.forEach(async (user) => { 9 | if (!user.siteLogo) { 10 | const logo = { 11 | iconUrl: "astroluma", 12 | iconUrlLight: null, 13 | iconProvider: 'com.astroluma.self' 14 | }; 15 | await usersCollection.updateOne({ _id: user._id }, { $set: { siteLogo: logo } }); 16 | } 17 | }); 18 | 19 | allUsers.forEach(async (user) => { 20 | if (!user.hasOwnProperty('hideBranding')) { 21 | await usersCollection.updateOne({ _id: user._id }, { $set: { hideBranding: false } }); 22 | } 23 | }); 24 | 25 | }, 26 | 27 | async down(db, client) { 28 | // TODO write the statements to rollback your migration (if possible) 29 | // Example: 30 | // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}}); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /client/src/utils/Helper.js: -------------------------------------------------------------------------------- 1 | export const validateOTPAuthURL = (url) => { 2 | const otpAuthRegex = /^otpauth:\/\/totp\/[^?]+\?secret=([^&]+)(?:&|$)/; 3 | const match = url.match(otpAuthRegex); 4 | return match?.[1]; 5 | }; 6 | 7 | export const isValidSecretKey = (secret) => { 8 | const secretRegex = /^[A-Z2-7]+=*$/i; 9 | return secret && secret.length >= 16 && secret.length <= 128 && secret.match(secretRegex) ? true : false; 10 | }; 11 | 12 | export const isLocal = (hostname) => { 13 | const localIPs = ['localhost', '127.0.0.1']; 14 | const ipPattern = /^192\.168\.\d{1,3}\.\d{1,3}$/; 15 | return localIPs.includes(hostname) || ipPattern.test(hostname) ? true : false; 16 | }; 17 | 18 | export const base64ToBlob = (base64, contentType) => { 19 | const byteCharacters = atob(base64.split(',')[1]); 20 | const byteNumbers = new Array(byteCharacters.length); 21 | for (let i = 0; i < byteCharacters.length; i++) { 22 | byteNumbers[i] = byteCharacters.charCodeAt(i); 23 | } 24 | const byteArray = new Uint8Array(byteNumbers); 25 | return new Blob([byteArray], { type: contentType }); 26 | }; 27 | -------------------------------------------------------------------------------- /server/models/IconPack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | const { Schema } = mongoose; 4 | 5 | const creditSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | url: { 11 | type: String, 12 | required: true, 13 | } 14 | }); 15 | 16 | const iconpackSchema = new Schema({ 17 | iconProvider: { 18 | type: String, 19 | required: true, 20 | unique: true, 21 | }, 22 | iconName: { 23 | type: String, 24 | required: true, 25 | }, 26 | iconPackVersion: { 27 | type: String, 28 | required: true, 29 | }, 30 | jsonUrl: { 31 | type: String, 32 | required: true, 33 | }, 34 | packDeveloper: { 35 | type: String, 36 | required: true, 37 | }, 38 | credit: { 39 | type: creditSchema, 40 | required: false, 41 | }, 42 | userId: { 43 | type: Schema.Types.ObjectId, 44 | ref: 'User', 45 | required: false, 46 | default: null, 47 | } 48 | }, { 49 | timestamps: true, 50 | }); 51 | 52 | const IconPack = mongoose.model('IconPacks', iconpackSchema); 53 | 54 | module.exports = IconPack; -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceDrag.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NiceDrag = () => { 4 | 5 | return ( 6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 | ) 20 | } 21 | 22 | const MemoizedComponent = React.memo(NiceDrag); 23 | export default MemoizedComponent; 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js image as the base image 2 | FROM node:18 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the server and client folders from the host to the working directory in the container 8 | COPY server /app/server 9 | COPY client /app/client 10 | 11 | # Copy package.json and package-lock.json to the working directory 12 | COPY package*.json ./ 13 | 14 | # Install dependencies for both server and client 15 | RUN npm install 16 | 17 | # Set working directory to client 18 | WORKDIR /app/client 19 | 20 | # Build the React client 21 | RUN npm run build 22 | 23 | # Copy the dist directory to the server/dist 24 | RUN cp -r /app/client/dist /app/server/dist 25 | 26 | # Remove the client folder 27 | RUN rm -rf /app/client 28 | 29 | # Reset working directory to app root 30 | WORKDIR /app 31 | 32 | # Install ffmpeg and ping 33 | RUN apt-get update && apt-get install -y ffmpeg iputils-ping arp-scan 34 | 35 | # Setup 36 | RUN npm run setup 37 | 38 | # Expose the port on which your Node.js app will run 39 | EXPOSE 8000 40 | 41 | # Start the Node.js app 42 | CMD ["npm", "run", "server"] 43 | -------------------------------------------------------------------------------- /client/src/components/Sidebar/SidebarButtonItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const SidebarButtonItem = React.memo(({ icon, text, clickHandler, active, id }) => { 5 | return ( 6 |
  • 7 |
    13 | {icon} 14 | {text} 15 |
    16 |
  • 17 | ); 18 | }); 19 | 20 | SidebarButtonItem.displayName = 'SidebarButtonItem'; 21 | 22 | SidebarButtonItem.propTypes = { 23 | icon: PropTypes.element, 24 | text: PropTypes.string, 25 | clickHandler: PropTypes.func, 26 | active: PropTypes.bool, 27 | id: PropTypes.string 28 | }; 29 | 30 | export default SidebarButtonItem; -------------------------------------------------------------------------------- /server/apps/com.heimdall/templates/response.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 |
    6 | 7 | 8 | 9 | Items: 10 | {{items}}
    15 | 16 | 17 | 18 | Users: 19 | {{users}}
    24 |
    -------------------------------------------------------------------------------- /client/src/components/BuyMeACoffee.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion } from "framer-motion"; 3 | import { CONSTANTS } from "../utils/Constants"; 4 | 5 | const BuyMeACoffee = () => { 6 | 7 | return ( 8 | 21 | 26 | Buy me a coffee 31 | Buy me a coffee 32 | 33 | 34 | ); 35 | }; 36 | 37 | const MemoizedComponent = React.memo(BuyMeACoffee); 38 | export default MemoizedComponent; 39 | 40 | -------------------------------------------------------------------------------- /server/apps/com.proxmox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "ProxMox", 3 | "appId": "com.proxmox", 4 | "appIcon": "logo.png", 5 | "description": "", 6 | "autoRefreshAfterSeconds": 60, 7 | "alwaysShowDetailedView": true, 8 | "config": [ 9 | { 10 | "name": "overrideurl", 11 | "label": "Proxmox URL", 12 | "type": "string", 13 | "required": false, 14 | "placeholder": "If different from the Link URL, enter URL" 15 | }, 16 | { 17 | "name": "username", 18 | "label": "Proxmox Username", 19 | "type": "string", 20 | "required": true, 21 | "placeholder": "Enter username" 22 | }, 23 | { 24 | "name": "password", 25 | "label": "Proxmox Password", 26 | "type": "password", 27 | "required": true, 28 | "placeholder": "Enter password" 29 | }, 30 | { 31 | "name": "realm", 32 | "label": "Proxmox Realm", 33 | "type": "string", 34 | "required": true, 35 | "placeholder": "Enter realm" 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceBack.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion } from "framer-motion"; 3 | import { useNavigate } from "react-router-dom"; 4 | import NiceButton from "./NiceButton"; 5 | import PropTypes from "prop-types"; 6 | 7 | const NiceBack = ({ label = "Go Back" }) => { 8 | 9 | const navigate = useNavigate(); 10 | 11 | return ( 12 | 24 | navigate(-1)} 28 | /> 29 | 30 | ) 31 | } 32 | 33 | NiceBack.propTypes = { 34 | label: PropTypes.string 35 | } 36 | 37 | const MemoizedComponent = React.memo(NiceBack); 38 | export default MemoizedComponent; -------------------------------------------------------------------------------- /client/src/components/Misc/NetworkError.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Helmet } from "react-helmet"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const NetworkError = () => { 6 | 7 | return ( 8 |
    9 | 10 | Network Error 11 | 12 |
    13 |

    14 | Oops! Can not connect 15 |

    16 |

    17 | Unable to connect to the backend API. 18 |

    19 | Network Error 24 |

    25 | Go to Homepage 26 |

    27 |
    28 |
    29 | ); 30 | } 31 | 32 | 33 | const MemoizedComponent = React.memo(NetworkError); 34 | export default MemoizedComponent; 35 | -------------------------------------------------------------------------------- /client/src/components/Misc/ServerError.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Helmet } from "react-helmet"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const ServerError = () => { 6 | 7 | return ( 8 |
    9 | 10 | Server Error 11 | 12 |
    13 |

    14 | Oops! Some error occurred. 15 |

    16 |

    17 | Unable to handle this request at this moment. 18 |

    19 | Network Error 24 |

    25 | Go to Homepage 26 |

    27 |
    28 |
    29 | ); 30 | } 31 | 32 | 33 | const MemoizedComponent = React.memo(ServerError); 34 | export default MemoizedComponent; -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceClose.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion } from "framer-motion"; 3 | import PropTypes from "prop-types"; 4 | 5 | const NiceClose = ({ onClick = null }) => { 6 | 7 | return ( 8 | 9 | 15 | 16 | ) 17 | } 18 | 19 | NiceClose.propTypes = { 20 | onClick: PropTypes.func 21 | } 22 | 23 | const MemoizedComponent = React.memo(NiceClose); 24 | export default MemoizedComponent; 25 | -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceLink.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion } from "framer-motion"; 3 | import { Link } from "react-router-dom"; 4 | import PropTypes from "prop-types"; 5 | 6 | const NiceLink = ({ label = "Go Back", color = "blue", to = "/", className = "" }) => { 7 | 8 | return ( 9 | 21 | 25 | {label} 26 | 27 | 28 | ) 29 | } 30 | 31 | NiceLink.propTypes = { 32 | label: PropTypes.string, 33 | color: PropTypes.string, 34 | to: PropTypes.string, 35 | className: PropTypes.string 36 | } 37 | 38 | const MemoizedComponent = React.memo(NiceLink); 39 | export default MemoizedComponent; -------------------------------------------------------------------------------- /client/src/components/Misc/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Helmet } from "react-helmet"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const NotFound = () => { 6 | 7 | return ( 8 |
    9 | 10 | 404 - Not Found 11 | 12 |
    13 |

    14 | Oops! Page Not Found 15 |

    16 |

    17 | It seems like the page you are looking for does not exist. 18 |

    19 | Not Found 24 |

    25 | Go to Homepage 26 |

    27 |
    28 |
    29 | ); 30 | } 31 | 32 | 33 | const MemoizedComponent = React.memo(NotFound); 34 | export default MemoizedComponent; 35 | -------------------------------------------------------------------------------- /client/src/components/Misc/ClientError.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Helmet } from "react-helmet"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const ClientError = () => { 6 | 7 | return ( 8 |
    9 | 10 | Client Error 11 | 12 |
    13 |

    14 | Oops! Some error occurred. 15 |

    16 |

    17 | Unable to process this request at this moment. 18 |

    19 | Network Error 24 |

    25 | Go to Homepage 26 |

    27 |
    28 |
    29 | ); 30 | } 31 | 32 | const MemoizedComponent = React.memo(ClientError); 33 | export default MemoizedComponent; 34 | -------------------------------------------------------------------------------- /server/migrate-mongo-config.js: -------------------------------------------------------------------------------- 1 | // In this file you can configure migrate-mongo 2 | require('dotenv').config() 3 | 4 | const config = { 5 | mongodb: { 6 | // TODO Change (or review) the url to your MongoDB: 7 | url: process.env.MONGODB_URI || "mongodb://localhost:27017/astroluma", 8 | 9 | options: { 10 | connectTimeoutMS: 10000, // Set connection timeout to 10 seconds 11 | socketTimeoutMS: 10000, // Set socket timeout to 10 seconds 12 | } 13 | }, 14 | 15 | // The migrations dir, can be an relative or absolute path. Only edit this when really necessary. 16 | migrationsDir: "migrations", 17 | 18 | // The mongodb collection where the applied changes are stored. Only edit this when really necessary. 19 | changelogCollectionName: "changelog", 20 | 21 | // The file extension to create migrations and search for in migration dir 22 | migrationFileExtension: ".js", 23 | 24 | // Enable the algorithm to create a checksum of the file contents and use that in the comparison to determine 25 | // if the file should be run. Requires that scripts are coded to be run multiple times. 26 | useFileHash: false, 27 | 28 | // Don't change this, unless you know what you're doing 29 | moduleSystem: 'commonjs', 30 | }; 31 | 32 | module.exports = config; 33 | -------------------------------------------------------------------------------- /client/src/components/Page/ViewPage.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .page-content-area { 4 | user-select: text; 5 | white-space: pre-wrap; 6 | } 7 | 8 | .page-content-area h1, 9 | .page-content-area h2, 10 | .page-content-area h3, 11 | .page-content-area h4, 12 | .page-content-area h5, 13 | .page-content-area h6 { 14 | margin-top: 12px; 15 | margin-bottom: 12px; 16 | } 17 | 18 | .page-content-area h1 { 19 | font-size: 2em; 20 | } 21 | 22 | .page-content-area h2 { 23 | font-size: 1.5em; 24 | } 25 | 26 | .page-content-area h3 { 27 | font-size: 1.17em; 28 | } 29 | 30 | .page-content-area h4 { 31 | font-size: 1em; 32 | } 33 | 34 | .page-content-area h5 { 35 | font-size: 0.83em; 36 | } 37 | 38 | .page-content-area h6 { 39 | font-size: 0.67em; 40 | } 41 | 42 | .page-content-area ul { 43 | list-style-type: disc; 44 | margin-left: 20px; 45 | } 46 | 47 | .page-content-area ol { 48 | list-style-type: decimal; 49 | margin-left: 20px; 50 | } 51 | 52 | .page-content-area a { 53 | color: hsl(278, 100%, 50%); 54 | text-decoration: none; 55 | } 56 | 57 | .page-content-area .ql-syntax { 58 | padding: 8px; 59 | background-color: #333; 60 | margin-top: 8px; 61 | margin-bottom: 8px; 62 | border-radius: 8px; 63 | white-space: pre-wrap; 64 | } -------------------------------------------------------------------------------- /server/middlewares/uploadToLocalMiddleware.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | const sharp = require('sharp'); 4 | 5 | const uploadToLocalFolder = async(req, res, next) => { 6 | if (!req.file) { 7 | return next(); 8 | } 9 | 10 | try { 11 | const localFolder = path.join(__dirname, '../../storage/uploads'); 12 | await fs.mkdir(localFolder, { recursive: true }); 13 | 14 | const targetPath = path.join(localFolder, req.file.filename); 15 | const data = await fs.readFile(req.file.path); 16 | 17 | const image = sharp(data); 18 | const metadata = await image.metadata(); 19 | let resizeOptions = {}; 20 | 21 | if (metadata.width > 160 || metadata.height > 160) { 22 | resizeOptions = metadata.width > metadata.height 23 | ? { width: 160 } 24 | : { height: 160 }; 25 | } 26 | 27 | const processedImageBuffer = await image 28 | .resize(resizeOptions) 29 | .toBuffer(); 30 | 31 | await fs.writeFile(targetPath, processedImageBuffer); 32 | 33 | req.localUrl = req.file.filename; 34 | 35 | // Clean up the original file 36 | await fs.unlink(req.file.path); 37 | 38 | next(); 39 | } catch (err) { 40 | console.error(err); 41 | next(err); 42 | } 43 | } 44 | 45 | module.exports = uploadToLocalFolder; -------------------------------------------------------------------------------- /server/models/NetworkDevice.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { Schema } = mongoose; 3 | 4 | const networkSchema = new mongoose.Schema({ 5 | deviceMac: { 6 | type: String, 7 | required: false, 8 | default: null 9 | }, 10 | deviceName: { 11 | type: String, 12 | required: true, 13 | }, 14 | deviceIcon: { 15 | type: Schema.Types.Mixed, 16 | required: false, 17 | default: null, 18 | }, 19 | broadcastAddress: { 20 | type: String, 21 | required: false, 22 | default: null 23 | }, 24 | broadcastPort: { 25 | type: Number, 26 | required: false, 27 | default: 9 28 | }, 29 | deviceIp : { 30 | type: String, 31 | required: false, 32 | default: null 33 | }, 34 | supportsWol : { 35 | type: Boolean, 36 | default: false 37 | }, 38 | virtualDevice : { 39 | type: Boolean, 40 | default: false 41 | }, 42 | sortOrder: { 43 | type: Number, 44 | required: false, 45 | default: 9999 46 | }, 47 | isAlive: { 48 | type: Boolean, 49 | required: false, 50 | default: false 51 | }, 52 | userId: { 53 | type: mongoose.Schema.Types.ObjectId, 54 | ref: 'User', 55 | required: true 56 | } 57 | }, { 58 | timestamps: true 59 | }); 60 | 61 | const NetworkDevice = mongoose.model('NetworkDevice', networkSchema); 62 | 63 | module.exports = NetworkDevice; -------------------------------------------------------------------------------- /client/src/utils/SystemThemes.js: -------------------------------------------------------------------------------- 1 | const SystemThemes = [ 2 | { value: 'light', label: 'Light', type: 'light', accentColor: '#00a3cc' }, 3 | { value: 'themoon', label: 'The Moon', type: 'light', accentColor: '#c9d7e3' }, 4 | { value: 'aurora', label: 'Aurora', type: 'light', accentColor: '#ffffff' }, 5 | { value: 'iceberg', label: 'Iceberg', type: 'light', accentColor: '#ffffff' }, 6 | { value: 'solarflare', label: 'Solar Flare', type: 'light', accentColor: '#ff7043' }, 7 | { value: 'europa', label: 'Europa', type: 'light', accentColor: '#ffffff' }, 8 | { value: 'titan', label: 'Titan', type: 'light', accentColor: '#ffffff' }, 9 | { value: 'dark', label: 'Dark', type: 'dark', accentColor: '#4a4a4a' }, 10 | { value: 'deepspace', label: 'Deep Space', type: 'dark', accentColor: '#12181f' }, 11 | { value: 'themars', label: 'The Mars', type: 'dark', accentColor: '#3d1a1a' }, 12 | { value: 'cyberpunk', label: 'Cyber Punk', type: 'dark', accentColor: '#1c1c3a' }, 13 | { value: 'neptune', label: 'Neptune', type: 'dark', accentColor: '#2a2d42' }, 14 | { value: 'midnightRain', label: 'Midnight Rain', type: 'dark', accentColor: '#23262d' }, 15 | { value: 'galacticDream', label: 'Galactic Dream', type: 'dark', accentColor: '#1e2228' }, 16 | { value: 'nebula', label: 'Nebula', type: 'dark', accentColor: '#13131f' }, 17 | ]; 18 | 19 | export default SystemThemes; -------------------------------------------------------------------------------- /server/utils/allowedModules.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // Node.js Built-in Modules (Safe Subset) 3 | 'assert', 'buffer', 'crypto', 'dns', 'events', 'http', 'https', 4 | 'querystring', 'stream', 'timers', 'url', 'util', 'zlib', 5 | 'readline', 'string_decoder', 'perf_hooks', 6 | 7 | // Common Third-party Libraries 8 | 'axios', // HTTP requests 9 | 'moment', // Date/time manipulation 10 | 'dayjs', // Lightweight alternative to Moment.js 11 | 'lodash', // Utility library 12 | 'uuid', // Generate unique identifiers 13 | 'date-fns', // Modern date utility library 14 | 'debug', // Debugging utility 15 | 'node-fetch', // Fetch API for Node.js 16 | 'form-data', // Handling form data 17 | 'jsonschema', // JSON schema validation 18 | 'jsonwebtoken', // JSON Web Token utilities 19 | 'bcrypt', // Password hashing 20 | 'argon2', // Modern password hashing 21 | 'helmet', // Security headers for HTTP responses 22 | 'express-rate-limit', // API rate-limiting 23 | 'validator', // String validation and sanitization 24 | 'qs', // Query string parsing and formatting 25 | 'bluebird', // Promises with additional utilities 26 | 'pino', // Fast JSON logging 27 | 'winston', // Versatile logging library 28 | 'sharp', // Image processing 29 | 'md5', 30 | ]; -------------------------------------------------------------------------------- /client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/components/Page/Jodit.css: -------------------------------------------------------------------------------- 1 | .jodit-wysiwyg { 2 | @apply !bg-inputBg !text-inputText !placeholder-inputPlaceholder; 3 | } 4 | 5 | .jodit-ui-group, .jodit-popup__content { 6 | @apply !bg-bodyBg !text-bodyText; 7 | } 8 | 9 | .jodit-toolbar__box, .jodit-status-bar { 10 | @apply !bg-bodyBg !text-bodyText !placeholder-inputPlaceholder; 11 | } 12 | 13 | .jodit-container, .jodit-toolbar__box, .jodit-status-bar { 14 | @apply !border-inputBorder rounded-xl; 15 | } 16 | 17 | .jodit-toolbar-button__button { 18 | @apply !bg-bodyBg !text-bodyText !border-inputBorder; 19 | } 20 | 21 | .jodit-toolbar-button__button:active { 22 | @apply !bg-bodyBg !text-red-600 !border-inputBorder; 23 | } 24 | 25 | .jodit-toolbar-button, .jodit-toolbar-button__trigger, .jodit-toolbar-button__text{ 26 | @apply !bg-bodyBg !text-bodyText !border-inputBorder; 27 | } 28 | 29 | .jodit-toolbar-button__text:hover{ 30 | @apply !bg-inputBg !text-inputText; 31 | } 32 | 33 | .jodit-popup { 34 | @apply !bg-bodyBg !text-bodyText !border-inputBorder; 35 | } 36 | 37 | .jodit-dialog { 38 | @apply !bg-bodyBg !text-bodyText !border-inputBorder; 39 | } 40 | 41 | .jodit-dialog__header { 42 | @apply !bg-bodyBg !text-bodyText !border-inputBorder; 43 | } 44 | 45 | .jodit-ui-input { 46 | @apply !bg-inputBg !text-inputText !placeholder-inputPlaceholder !border-inputBorder; 47 | } 48 | 49 | .jodit-popup-container, .jodit-box { 50 | @apply !bg-bodyBg !text-bodyText !border-inputBorder; 51 | } -------------------------------------------------------------------------------- /server/apps/com.heimdall/templates/m-response.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 |
    10 | 11 | 12 | 13 | Items: 14 | {{items}}
    20 | 21 | 22 | 23 | Users: 24 | {{users}}
    30 | 31 | 32 | Open Heimdall 33 | 34 |
    -------------------------------------------------------------------------------- /server/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const User = require('../models/User'); 3 | 4 | exports.verifyToken = async (req, res, next) => { 5 | const authHeader = req.headers.authorization; 6 | 7 | const token = authHeader?.split(' ')[1]; 8 | 9 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 10 | return res.status(401).json({ 11 | error: true, 12 | message: 'Unauthorized: Missing or invalid token format', 13 | }); 14 | } 15 | 16 | if (!token) { 17 | return res.status(401).json({ 18 | error: true, 19 | message: 'Unauthorized: Missing token', 20 | }); 21 | } 22 | 23 | try { 24 | const decoded = jwt.verify(token, process.env.SECRET || "SomeRandomStringSecret"); 25 | 26 | // Find user by username 27 | const user = await User.findOne({ username: new RegExp(`^${decoded.username}$`, 'i') }).exec(); 28 | 29 | if (user) { 30 | // Records found, handle the result 31 | req.user = user; 32 | 33 | // Proceed to the next middleware or endpoint 34 | next(); 35 | } else { 36 | return res.status(401).json({ 37 | error: true, 38 | message: 'Unauthorized: User not found', 39 | }); 40 | } 41 | } catch (err) { 42 | return res.status(401).json({ 43 | error: true, 44 | message: 'Unauthorized: Invalid token', 45 | }); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /client/src/components/Layout/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | import { loadingState } from '../../atoms'; 4 | import { motion, AnimatePresence } from 'framer-motion'; 5 | import NiceLoader from '../NiceViews/NiceLoader'; 6 | import PropTypes from 'prop-types'; 7 | 8 | const Loader = ({ children }) => { 9 | const loading = useRecoilValue(loadingState); 10 | 11 | return ( 12 | <> 13 | {children} 14 | { 15 | loading && ( 16 | 17 | 24 |
    25 | 26 |
    27 |
    28 |
    29 | ) 30 | } 31 | 32 | ); 33 | }; 34 | 35 | 36 | Loader.propTypes = { 37 | children: PropTypes.node 38 | }; 39 | 40 | const MemoizedComponent = React.memo(Loader); 41 | export default MemoizedComponent; 42 | 43 | -------------------------------------------------------------------------------- /client/src/components/Settings/SingleSettingsItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { motion } from 'framer-motion'; 4 | 5 | const SingleSettingsItem = ({ Setting, onSelect }) => { 6 | 7 | const onSettingSelection = useCallback(() => { 8 | onSelect(Setting); 9 | }, [onSelect, Setting]); 10 | 11 | 12 | return ( 13 |
    14 | 15 |
    16 | { 17 | Setting?.icon &&
    18 | { 19 | Setting?.icon 20 | } 21 |
    22 | } 23 |
    24 |
    25 |
    26 |
    {Setting.title}
    27 |
    {Setting.description}
    28 |
    29 |
    30 |
    31 |
    32 | ); 33 | }; 34 | 35 | SingleSettingsItem.displayName = 'SingleSettingsItem'; 36 | 37 | SingleSettingsItem.propTypes = { 38 | Setting: PropTypes.object.isRequired, 39 | onSelect: PropTypes.func.isRequired 40 | }; 41 | 42 | 43 | const MemoizedComponent = React.memo(SingleSettingsItem); 44 | export default MemoizedComponent; 45 | -------------------------------------------------------------------------------- /client/src/components/ClickOutside.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const ClickOutside = ({ 5 | children, 6 | exceptionRef, 7 | onClick, 8 | className, 9 | }) => { 10 | const wrapperRef = useRef(null); 11 | 12 | useEffect(() => { 13 | const handleClickListener = (event) => { 14 | let clickedInside = false; 15 | if (exceptionRef) { 16 | clickedInside = 17 | wrapperRef.current?.contains(event.target) || 18 | exceptionRef.current === event.target || 19 | exceptionRef.current?.contains(event.target); 20 | } else { 21 | clickedInside = wrapperRef.current?.contains(event.target); 22 | } 23 | 24 | if (!clickedInside) { 25 | if (event.target.id === "btnAuth") { 26 | event.stopPropagation(); 27 | return; 28 | } 29 | onClick(); 30 | } 31 | }; 32 | 33 | document.addEventListener('mousedown', handleClickListener); 34 | 35 | return () => { 36 | document.removeEventListener('mousedown', handleClickListener); 37 | }; 38 | }, [exceptionRef, onClick]); 39 | 40 | return ( 41 |
    42 | {children} 43 |
    44 | ); 45 | }; 46 | 47 | ClickOutside.displayName = 'ClickOutside'; 48 | 49 | ClickOutside.propTypes = { 50 | children: PropTypes.node.isRequired, 51 | exceptionRef: PropTypes.object, 52 | onClick: PropTypes.func.isRequired, 53 | className: PropTypes.string, 54 | }; 55 | 56 | const MemoizedComponent = React.memo(ClickOutside); 57 | export default MemoizedComponent; -------------------------------------------------------------------------------- /server/apps/com.proxyman/templates/response.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 |
    6 | 7 | 8 | 9 | Proxy: 10 | {{proxy}}
    15 | 16 | 17 | 18 | Redirection: 19 | {{redirection}}
    24 | 25 | 26 | 27 | Streams: 28 | {{stream}}
    33 |
    -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceCheckbox.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const NiceCheckbox = ({ name="", label = "", checked = false, onChange = null, disabled = false }) => { 5 | 6 | const timestamp = Date.now(); 7 | let id = Math.random().toString(36).substring(7); 8 | 9 | if (label) { 10 | id = label.toLowerCase().replace(/\s+/g, '') + timestamp; 11 | } else { 12 | id = Math.random().toString(36).substring(7) + timestamp; 13 | } 14 | 15 | return ( 16 |
    17 |
    18 | 29 | { 30 | label && 33 | } 34 |
    35 |
    36 | ) 37 | } 38 | 39 | NiceCheckbox.propTypes = { 40 | label: PropTypes.string, 41 | checked: PropTypes.bool, 42 | onChange: PropTypes.func, 43 | disabled: PropTypes.bool, 44 | name: PropTypes.string 45 | } 46 | 47 | const MemoizedComponent = React.memo(NiceCheckbox); 48 | export default MemoizedComponent; 49 | -------------------------------------------------------------------------------- /client/src/components/Misc/WelcomeUser.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | import ImageView from './ImageView'; 3 | import { useRecoilValue } from 'recoil'; 4 | import { colorThemeState, userDataState } from '../../atoms'; 5 | import SystemThemes from '../../utils/SystemThemes'; 6 | 7 | const WelcomeUser = () => { 8 | 9 | const userData = useRecoilValue(userDataState); 10 | 11 | const colorTheme = useRecoilValue(colorThemeState); 12 | const [themeType, setThemeType] = useState("light"); 13 | 14 | useEffect(() => { 15 | const newThemeType = SystemThemes.find(theme => theme.value === colorTheme)?.type || "light"; 16 | setThemeType(newThemeType); 17 | }, [colorTheme]); 18 | 19 | const decideTheIcon = useCallback(() => { 20 | const iconObject = userData?.userAvatar; 21 | if (themeType === "dark" && iconObject?.iconUrlLight) { 22 | return iconObject?.iconUrlLight; 23 | } else { 24 | return iconObject?.iconUrl; 25 | } 26 | }, [userData, themeType]); 27 | 28 | return ( 29 |
    30 |
    31 | 32 |
    33 |
    34 |

    Welcome Back

    35 |

    {userData?.fullName}

    36 |
    37 |
    38 | ); 39 | }; 40 | 41 | const MemoizedComponent = React.memo(WelcomeUser); 42 | export default MemoizedComponent; -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrolumaclient", 3 | "private": true, 4 | "version": "1.0.2", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@dnd-kit/core": "3.0.3", 14 | "@dnd-kit/sortable": "3.0.1", 15 | "@dnd-kit/utilities": "2.0.0", 16 | "axios": "^1.7.4", 17 | "buffer": "^6.0.3", 18 | "framer-motion": "^11.3.0", 19 | "jodit-react": "^4.1.2", 20 | "jsqr": "^1.4.0", 21 | "md5": "^2.3.0", 22 | "mitt": "^3.0.1", 23 | "moment": "^2.30.1", 24 | "mpegts.js": "^1.7.3", 25 | "prop-types": "^15.8.1", 26 | "react": "^18.3.1", 27 | "react-bottom-scroll-listener": "^5.1.0", 28 | "react-dom": "^18.3.1", 29 | "react-drag-drop-files": "^2.4.0", 30 | "react-helmet": "^6.1.0", 31 | "react-icons": "^5.1.0", 32 | "react-modern-drawer": "^1.4.0", 33 | "react-router-dom": "^6.23.0", 34 | "react-select": "^5.9.0", 35 | "react-syntax-highlighter": "^15.5.0", 36 | "react-toastify": "^10.0.5", 37 | "recoil": "^0.7.7", 38 | "recoil-persist": "^5.1.0", 39 | "semver": "^7.6.3", 40 | "totp-generator": "^1.0.0" 41 | }, 42 | "devDependencies": { 43 | "@eslint/js": "^9.16.0", 44 | "@vitejs/plugin-react": "^4.3.1", 45 | "eslint": "^8.57.1", 46 | "eslint-plugin-react": "^7.37.2", 47 | "eslint-plugin-react-hooks": "^4.6.2", 48 | "eslint-plugin-react-refresh": "^0.4.7", 49 | "globals": "^15.13.0", 50 | "postcss": "^8.4.39", 51 | "tailwindcss": "^3.4.4", 52 | "tw-colors": "^3.3.2", 53 | "vite": "^5.3.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/models/Authenticator.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { Schema } = mongoose; 3 | const CryptoJS = require('crypto-js'); 4 | const { getSecretKey } = require('../utils/apiutils'); 5 | 6 | const authenticatorSchema = new Schema({ 7 | serviceName: { 8 | type: String, 9 | required: true, 10 | }, 11 | serviceIcon: { 12 | type: Schema.Types.Mixed, 13 | required: false, 14 | default: null, 15 | }, 16 | accountName: { 17 | type: String, 18 | required: true, 19 | }, 20 | secretKey: { 21 | type: String, 22 | required: true, 23 | get: function(encryptedValue) { 24 | 25 | if (!encryptedValue) { 26 | return null; 27 | } 28 | 29 | try { 30 | // Try direct decryption 31 | const bytes = CryptoJS.AES.decrypt(encryptedValue, getSecretKey()); 32 | 33 | const decrypted = bytes.toString(CryptoJS.enc.Utf8); 34 | 35 | if (!decrypted) { 36 | //console.log('Decryption resulted in empty string'); 37 | return encryptedValue; // Return original if decryption gives empty string 38 | } 39 | 40 | return decrypted; 41 | } catch (error) { 42 | return encryptedValue; // Return original value if decryption fails 43 | } 44 | } 45 | }, 46 | sortOrder: { 47 | type: Number, 48 | required: true, 49 | default: 9999, 50 | }, 51 | userId: { 52 | type: Schema.Types.ObjectId, 53 | ref: 'User', 54 | required: true, 55 | } 56 | }, { 57 | timestamps: true, 58 | toJSON: { getters: true }, 59 | toObject: { getters: true } 60 | }); 61 | 62 | const Authenticator = mongoose.model('Authenticator', authenticatorSchema); 63 | 64 | module.exports = Authenticator; -------------------------------------------------------------------------------- /server/apps/com.heimdall/app.js: -------------------------------------------------------------------------------- 1 | 2 | const connectionTest = async (testerInstance) => { 3 | try { 4 | const connectionUrl = testerInstance?.appUrl; 5 | 6 | if (!connectionUrl) { 7 | await testerInstance.connectionFailed("Please provide all the required configuration parameters"); 8 | return; 9 | } 10 | 11 | //console.log(`${connectionUrl}/health`); 12 | 13 | const response = await testerInstance?.axios.get(`${connectionUrl}/health`); 14 | 15 | if (response.status === 200) { 16 | await testerInstance.connectionSuccess(); 17 | } else { 18 | await testerInstance.connectionFailed('Invalid response from Heimdall'); 19 | } 20 | 21 | } catch (error) { 22 | //console.log(error); 23 | await testerInstance.connectionFailed(error); 24 | } 25 | } 26 | 27 | const initialize = async (application) => { 28 | 29 | const appUrl = application?.appUrl; 30 | 31 | if (!appUrl) { 32 | return await application.sendError('Please provide all the required configuration parameters'); 33 | } 34 | 35 | try { 36 | 37 | const response = await application?.axios.get(`${appUrl}/health`); 38 | 39 | const data = response.data; 40 | 41 | const variables = [ 42 | { key: '{{items}}', value: data.items }, 43 | { key: '{{users}}', value: data.users }, 44 | { key: '{{appUrl}}', value: appUrl } 45 | ]; 46 | 47 | await application.sendResponse('response.tpl', 200, variables); 48 | 49 | } catch (error) { 50 | await application.sendError(error); 51 | } 52 | } 53 | 54 | global.initialize = initialize; 55 | global.connectionTest = connectionTest; -------------------------------------------------------------------------------- /server/controllers/image.js: -------------------------------------------------------------------------------- 1 | const Icon = require("../models/Icon"); 2 | 3 | exports.uploadImage = async (req, res) => { 4 | const uploadedFile = req.localUrl; 5 | const userId = req.user?._id; 6 | 7 | if (!uploadedFile || !userId) { 8 | return res.status(400).json({ 9 | error: true, 10 | message: "Image upload failed." 11 | }); 12 | } 13 | 14 | try { 15 | const icon = new Icon({ 16 | iconPath: uploadedFile, 17 | userId 18 | }); 19 | 20 | await icon.save(); 21 | 22 | return res.status(200).json({ 23 | error: false, 24 | message: "Image uploaded successfully.", 25 | data: icon 26 | }); 27 | } catch (err) { 28 | console.error(err); 29 | return res.status(400).json({ 30 | error: true, 31 | message: "Image upload failed." 32 | }); 33 | } 34 | } 35 | 36 | exports.listImages = async (req, res) => { 37 | const userId = req.user?._id; 38 | 39 | try { 40 | const page = parseInt(req.query.page, 10) || 1; 41 | const limit = 20; 42 | const skip = (page - 1) * limit; 43 | 44 | const icons = await Icon.find({ 45 | $or: [{ userId }, { userId: null }] 46 | }) 47 | .sort({ _id: -1 }) // Sort by id in descending order 48 | .skip(skip) 49 | .limit(limit); 50 | 51 | return res.status(200).json({ 52 | error: false, 53 | message: "Icons retrieved successfully.", 54 | data: icons 55 | }); 56 | } catch (err) { 57 | console.error(err); 58 | return res.status(400).json({ 59 | error: true, 60 | message: "Failed to retrieve icons." 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /server/routes/listing.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { saveFolder, 3 | listingDetails, 4 | listItems, 5 | saveLink, 6 | reOrder, 7 | deleteListing, 8 | manageListItems, 9 | saveTodo, 10 | saveStream, 11 | listStreams, 12 | generatePreview, 13 | moveListingTo, 14 | saveSnippet 15 | } = require('../controllers/listing'); 16 | const { verifyToken } = require('../middlewares/auth'); 17 | 18 | const router = express.Router(); 19 | 20 | router.post('/listing/save/folder', verifyToken, saveFolder); 21 | router.post('/listing/save/link', verifyToken, saveLink); 22 | router.post('/listing/save/todo', verifyToken, saveTodo); 23 | router.post('/listing/save/snippet', verifyToken, saveSnippet); 24 | router.post('/listing/save/stream', verifyToken, saveStream); 25 | router.get('/listing/folder/list', verifyToken, listItems); 26 | router.get('/listing/folder/stream/list', verifyToken, listStreams); 27 | router.get('/listing/folder/:listingId/list/manage/:type', verifyToken, manageListItems); 28 | router.get('/listing/folder/:listingId/list', verifyToken, listItems); 29 | router.post('/listing/folder/:listingId/reorder', verifyToken, reOrder); 30 | router.get('/listing/folder/:listingId', verifyToken, listingDetails); 31 | router.get('/listing/link/:listingId', verifyToken, listingDetails); 32 | router.get('/listing/snippet/:listingId', verifyToken, listingDetails); 33 | router.get('/listing/todo/:listingId', verifyToken, listingDetails); 34 | router.get('/listing/stream/preview/:listingId', generatePreview); 35 | router.get('/listing/stream/:listingId', verifyToken, listingDetails); 36 | router.get('/listing/delete/:listingId', verifyToken, deleteListing); 37 | router.get('/listing/move/:listingId/to/:parentId', verifyToken, moveListingTo); 38 | 39 | module.exports = router; -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | Security is paramount for Astroluma. 3 | 4 | ## Supported Versions 5 | 6 | The current version, along with the previous minor versions and the last five releases, are actively supported. Any versions older than these, including those from the previous major release, are no longer maintained or monitored, so their security cannot be guaranteed. 7 | 8 | ## Reporting a Security Issue 9 | If you believe you've discovered a critical issue, please email me at sanjeet.pathak990@gmail.com. Security reports are treated with high priority, and you can expect a response within 48 hours. 10 | 11 | For non-critical issues, please raise an issue on Github repo and include the following details to help us address the problem effectively: 12 | 13 | - **Type of Issue:** Specify the nature of the issue (e.g., buffer overflow, SQL injection, cross-site scripting, etc.). 14 | 15 | - **Source File Details:** Provide the full paths of the source file(s) where the issue is observed. 16 | 17 | - **Code Location:** Mention the relevant tag, branch, commit, or provide a direct URL to the affected code. 18 | 19 | - **Configuration Details:** List any special configurations needed to reproduce the issue. 20 | 21 | - **Reproduction Steps:** Include clear, step-by-step instructions to replicate the issue. 22 | 23 | - **Proof-of-Concept:** Attach proof-of-concept or exploit code, if available. 24 | 25 | - **Impact:** Explain the potential impact of the issue and describe how an attacker might exploit it. 26 | 27 | This information will help us assess and resolve the issue promptly. 28 | 29 | Please refrain from raising issues in this repository related to ReactJS. We are already using the latest versions of these dependencies, so any problems should be directed to the React team. The same applies to other development dependencies, as they are also up-to-date. -------------------------------------------------------------------------------- /server/routes/image.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { verifyToken } = require('../middlewares/auth'); 3 | const multer = require('multer') 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const { uploadImage, listImages } = require('../controllers/image'); 7 | const uploadToLocalFolder = require('../middlewares/uploadToLocalMiddleware'); 8 | 9 | // Define the upload directory 10 | const uploadDirectory = './public/uploads/icons'; 11 | 12 | // Check if the directory exists, and create it if it doesn't 13 | if (!fs.existsSync(uploadDirectory)) { 14 | fs.mkdirSync(uploadDirectory, { recursive: true }); 15 | } 16 | 17 | const storage = multer.diskStorage({ 18 | destination: function (req, file, cb) { 19 | cb(null, uploadDirectory); 20 | }, 21 | filename: function (req, file, cb) { 22 | const timestamp = Date.now(); 23 | const originalname = path.parse(file.originalname); 24 | const filename = `${timestamp}${originalname.ext}`; 25 | 26 | cb(null, filename); 27 | } 28 | }); 29 | 30 | const fileFilter = function (req, file, cb) { 31 | // Check if the file is an image 32 | if (!file.mimetype.startsWith('image/')) { 33 | return cb(new Error('Only image files are allowed!'), false); 34 | } 35 | 36 | // Check file size (max 100KB) 37 | if (file.size > 100 * 1024) { 38 | return cb(new Error('File size exceeds the limit (100KB)!'), false); 39 | } 40 | 41 | // Accept the file 42 | cb(null, true); 43 | }; 44 | 45 | const upload = multer({ 46 | storage, 47 | fileFilter 48 | }); 49 | 50 | const router = express.Router(); 51 | 52 | router.post('/images/upload', verifyToken, upload.single('icon'), uploadToLocalFolder, uploadImage); 53 | router.get('/images', verifyToken, listImages); 54 | 55 | module.exports = router; -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion } from "framer-motion"; 3 | import PropTypes from "prop-types"; 4 | 5 | const NiceButton = ({ label = "Go Back", onClick = null, disabled = false, className = "", parentClassname = "" }) => { 6 | return ( 7 | 20 | 41 | 42 | ); 43 | }; 44 | 45 | NiceButton.propTypes = { 46 | label: PropTypes.string, 47 | onClick: PropTypes.func, 48 | disabled: PropTypes.bool, 49 | className: PropTypes.string, 50 | parentClassname: PropTypes.string 51 | }; 52 | 53 | const MemoizedComponent = React.memo(NiceButton); 54 | export default MemoizedComponent; -------------------------------------------------------------------------------- /client/src/components/Page/SinglePageItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import NiceLink from '../NiceViews/NiceLink'; 4 | import NiceButton from '../NiceViews/NiceButton'; 5 | import PropTypes from 'prop-types'; 6 | 7 | const SinglePageItem = ({ page, deletePage, managePublish }) => { 8 | return ( 9 | 15 |
    16 |

    {page.pageTitle}

    17 |
    18 |
    19 | 24 | deletePage(page._id)} 28 | /> 29 | managePublish(page._id, page.isPublished)} 33 | /> 34 |
    35 |
    36 | ); 37 | }; 38 | 39 | SinglePageItem.displayName = 'SinglePageItem'; 40 | 41 | SinglePageItem.propTypes = { 42 | page: PropTypes.object.isRequired, 43 | deletePage: PropTypes.func.isRequired, 44 | managePublish: PropTypes.func.isRequired 45 | }; 46 | 47 | 48 | const MemoizedComponent = React.memo(SinglePageItem); 49 | export default MemoizedComponent; 50 | -------------------------------------------------------------------------------- /client/src/components/Sidebar/SidebarLinkItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { motion } from 'framer-motion'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const SidebarLinkItem = React.memo(({ icon, text, to, active }) => { 7 | return ( 8 | 12 | {to.startsWith("http") ? ( 13 | 20 | {icon} 21 | {text} 22 | 23 | ) : ( 24 | 29 | {icon} 30 | {text} 31 | 32 | )} 33 | 34 | ); 35 | }); 36 | 37 | SidebarLinkItem.displayName = 'SidebarLinkItem'; 38 | 39 | SidebarLinkItem.propTypes = { 40 | icon: PropTypes.element.isRequired, 41 | text: PropTypes.string.isRequired, 42 | to: PropTypes.string.isRequired, 43 | active: PropTypes.bool.isRequired 44 | } 45 | 46 | export default SidebarLinkItem; -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 26 | Loading... 27 | 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /server/controllers/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const User = require('../models/User'); 3 | 4 | 5 | exports.doLogin = async (req, res) => { 6 | let error = ""; 7 | 8 | const username = req?.body?.username?.toLowerCase(); 9 | const password = req?.body?.password; 10 | 11 | if (!username || !password) { 12 | error = "Username and/or password must not be empty."; 13 | } 14 | 15 | if (!error) { 16 | try { 17 | // Find the user by username 18 | const user = await User.findOne({ username: new RegExp(`^${username}$`, 'i') }); 19 | 20 | if (user) { 21 | // Check if the password matches 22 | if (password === user.password) { 23 | const payload = { 24 | userId: user._id, // Use user._id for user ID 25 | username: user.username, 26 | role: user.isSuperAdmin ? 'admin' : 'user', 27 | }; 28 | 29 | // Create the JWT token 30 | const token = jwt.sign(payload, process.env.SECRET || "SomeRandomStringSecret", {}); 31 | 32 | return res.status(200).json({ 33 | error: false, 34 | message: { 35 | token, 36 | role: user.isSuperAdmin ? 'admin' : 'user', 37 | fullName: user.fullName, 38 | colorTheme: user.colorTheme || "light", 39 | avatar: user.profilePicture 40 | } 41 | }); 42 | } 43 | } 44 | error = "Invalid username and/or password"; 45 | } catch (err) { 46 | error = "An error occurred during authentication"; 47 | } 48 | } 49 | 50 | return res.status(400).json({ 51 | error: true, 52 | message: error 53 | }); 54 | } -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceModal.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion } from "framer-motion"; 3 | import NiceClose from "./NiceClose"; 4 | import PropTypes from "prop-types"; 5 | 6 | const NiceModal = ({ show = false, title = "", body, footer, closeModal = null }) => { 7 | 8 | return ( 9 | show && 15 | 21 |
    22 |

    23 | {title} 24 |

    25 | {closeModal && } 26 |
    27 |
    28 | {body} 29 | { 30 | footer &&
    31 | {footer} 32 |
    33 | } 34 |
    35 |
    36 |
    37 | ) 38 | } 39 | 40 | NiceModal.propTypes = { 41 | show: PropTypes.bool, 42 | title: PropTypes.string, 43 | body: PropTypes.node, 44 | footer: PropTypes.node, 45 | closeModal: PropTypes.func 46 | } 47 | 48 | const MemoizedComponent = React.memo(NiceModal); 49 | export default MemoizedComponent; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: astroluma 9 | environment: 10 | PORT: 8000 11 | NODE_ENV: production 12 | SECRET_KEY: jK8hB2mL6qv9NxX0PzWrDt5Yc3FgT4Vu 13 | MONGODB_URI: mongodb://127.0.0.1:27017/astroluma 14 | volumes: 15 | - uploads_data:/app/storage/uploads 16 | - uploads_app:/app/storage/apps 17 | depends_on: 18 | - mongodb 19 | restart: always 20 | network_mode: "host" ## is important to function the network scanning. The bridge mode puts it behind NAT and the host mode puts it on the same network as the host. 21 | 22 | mongodb: 23 | image: mongo:4.0 ##More recent versions of MongoDB needs AVX2 support, so it's better to use the 4.0 version. 24 | container_name: astroluma_mongodb 25 | ports: 26 | - "27017:27017" 27 | volumes: 28 | - mongo_data:/data/db 29 | restart: always 30 | 31 | volumes: 32 | mongo_data: 33 | driver: local 34 | uploads_data: 35 | driver: local 36 | 37 | ## Alt config, with more locked down networking & simplified addressing 38 | #services: 39 | # app: 40 | # build: 41 | # context: . 42 | # dockerfile: Dockerfile 43 | # ports: 44 | # - "8000:8000" 45 | # container_name: astroluma 46 | # environment: 47 | # PORT: 8000 48 | # NODE_ENV: production 49 | # SECRET_KEY: jK8hB2mL6qv9NxX0PzWrDt5Yc3FgT4Vu 50 | # MONGODB_URI: mongodb://mongodb/astroluma 51 | # volumes: 52 | # - uploads_data:/app/storage/uploads 53 | # depends_on: 54 | # - mongodb 55 | # restart: always 56 | 57 | # mongodb: 58 | # image: mongo:4.0 ##More recent versions of MongoDB needs AVX2 support, so it's better to use the 4.0 version. 59 | # container_name: astroluma_mongodb 60 | # volumes: 61 | # - mongo_data:/data/db 62 | # restart: always 63 | 64 | #volumes: 65 | # mongo_data: 66 | # driver: local 67 | # uploads_data: 68 | # driver: local 69 | -------------------------------------------------------------------------------- /server/apps/com.youtube/templates/response.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 |
    6 | 7 | 8 | 9 | Views: 10 | {{views}}
    15 | 16 | 17 | 18 | Likes: 19 | {{likes}}
    24 | 25 | 26 | 27 | Comments: 28 | {{comments}}
    33 |
    -------------------------------------------------------------------------------- /server/routes/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const multer = require('multer'); 3 | const fs = require('fs'); 4 | const router = express.Router(); 5 | 6 | const { verifyToken } = require('../middlewares/auth'); 7 | const { runIntegratedApp, connectTest, installedApps, installFromZip, removeInstalledApp, syncFromDisk, installRemoteApp, allInstalledApps, serveLogo, updateRemoteApp } = require('../controllers/app'); 8 | 9 | // Ensure the upload directory exists 10 | const uploadDir = './public/uploads/integrations'; 11 | if (!fs.existsSync(uploadDir)) { 12 | fs.mkdirSync(uploadDir, { recursive: true }); 13 | } 14 | 15 | // Configure multer 16 | const storage = multer.diskStorage({ 17 | destination: function (req, file, cb) { 18 | cb(null, uploadDir); 19 | }, 20 | filename: function (req, file, cb) { 21 | const timestamp = Date.now(); 22 | const randomNum = Math.floor(Math.random() * 1000); 23 | cb(null, `${timestamp}_${randomNum}.zip`); 24 | } 25 | }); 26 | 27 | const upload = multer({ 28 | storage: storage, 29 | limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB 30 | fileFilter: function (req, file, cb) { 31 | if (file.mimetype === 'application/zip' || file.mimetype === 'application/x-zip-compressed') { 32 | cb(null, true); 33 | } else { 34 | cb(new Error('Only .zip files are allowed! Found: ' + file.mimetype), false); 35 | } 36 | } 37 | }); 38 | 39 | router.get('/app/run/:listingId/:appId', verifyToken, runIntegratedApp); 40 | router.post('/app/test', verifyToken, connectTest); 41 | router.get('/app/installed', verifyToken, installedApps); 42 | router.get('/app/installed/all', verifyToken, allInstalledApps); 43 | router.post('/app/fromzip', verifyToken, upload.single('file'), installFromZip); 44 | router.get('/app/sync', verifyToken, syncFromDisk); 45 | router.get('/app/:appId/logo', serveLogo); 46 | router.get('/app/:appId/delete', verifyToken, removeInstalledApp); 47 | router.get('/app/:appId/install', verifyToken, installRemoteApp); 48 | router.get('/app/:appId/update', verifyToken, updateRemoteApp); 49 | 50 | module.exports = router; -------------------------------------------------------------------------------- /client/src/components/Layout/ContentLoader.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | import { contentLoadingState, loadingState } from '../../atoms'; 4 | import { motion, AnimatePresence } from 'framer-motion'; 5 | import NiceLoader from '../NiceViews/NiceLoader.jsx'; 6 | import PropTypes from 'prop-types'; 7 | 8 | const ContentLoader = ({ children }) => { 9 | const loading = useRecoilValue(contentLoadingState); 10 | const globalLoading = useRecoilValue(loadingState); 11 | const [showLoader, setShowLoader] = useState(false); 12 | 13 | useEffect(() => { 14 | let timeout; 15 | if (loading && !globalLoading) { 16 | setShowLoader(true); 17 | } else { 18 | timeout = setTimeout(() => setShowLoader(false), 300); // Wait for 300ms before hiding loader 19 | } 20 | return () => clearTimeout(timeout); // Cleanup timeout if component unmounts or state changes again 21 | }, [loading, globalLoading]); 22 | 23 | return ( 24 |
    25 | {children} 26 | 27 | {showLoader && ( 28 | 36 |
    37 | 38 |
    39 |
    40 | )} 41 |
    42 |
    43 | ); 44 | }; 45 | 46 | ContentLoader.propTypes = { 47 | children: PropTypes.node.isRequired 48 | }; 49 | 50 | const MemoizedComponent = React.memo(ContentLoader); 51 | export default MemoizedComponent; 52 | -------------------------------------------------------------------------------- /client/src/components/Modals/DeletePageModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { changedPageState, deletePageModalState, loadingState, loginState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import NiceModal from '../NiceViews/NiceModal'; 7 | import makeToast from '../../utils/ToastUtils'; 8 | import { useNavigate } from 'react-router-dom'; 9 | 10 | const DeletePageModal = () => { 11 | const navigate = useNavigate(); 12 | 13 | const [modalState, setModalState] = useRecoilState(deletePageModalState); 14 | const loginData = useRecoilValue(loginState); 15 | const setLoading = useSetRecoilState(loadingState); 16 | const setDeletedPage = useSetRecoilState(changedPageState); 17 | 18 | const closeModal = () => { 19 | setModalState({ ...modalState, isOpen: false }); 20 | }; 21 | 22 | const confirmDelete = () => { 23 | setLoading(true); 24 | 25 | ApiService.get(`/api/v1/page/delete/${modalState.data?.pageId}`, loginData?.token, navigate) 26 | .then(() => { 27 | makeToast("success", "Page deleted."); 28 | setDeletedPage(modalState.data?.pageId); 29 | closeModal(); 30 | }) 31 | .catch((error) => { 32 | if (!error.handled) makeToast("error", "Page cannot be deleted."); 33 | }) 34 | .finally(() => { 35 | setLoading(false); 36 | }); 37 | }; 38 | 39 | return ( 40 | Are you sure you want to delete this page?

    } 44 | footer={ 45 | <> 46 | 51 | 56 | 57 | } /> 58 | ); 59 | 60 | } 61 | 62 | const MemoizedComponent = React.memo(DeletePageModal); 63 | export default MemoizedComponent; 64 | -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | const { Schema } = mongoose; 4 | 5 | const userSchema = new Schema({ 6 | username: { 7 | type: String, 8 | required: true, 9 | unique: true, 10 | }, 11 | password: { 12 | type: String, 13 | required: true, 14 | }, 15 | fullName: { 16 | type: String, 17 | required: true, 18 | }, 19 | siteName: { 20 | type: String, 21 | default: "Astroluma", 22 | }, 23 | colorTheme: { 24 | type: String, 25 | default: "dark", 26 | }, 27 | userAvatar: { 28 | type: Object, 29 | required: false, 30 | default: { 31 | iconUrl: "defaultuser", 32 | iconUrlLight: null, 33 | iconProvider: 'com.astroluma.self' 34 | }, 35 | }, 36 | siteLogo: { 37 | type: Object, 38 | required: false, 39 | default: { 40 | iconUrl: "astroluma", 41 | iconUrlLight: null, 42 | iconProvider: 'com.astroluma.self' 43 | }, 44 | }, 45 | isSuperAdmin: { 46 | type: Boolean, 47 | default: false, 48 | }, 49 | hideBranding: { 50 | type: Boolean, 51 | default: false, 52 | required: false, 53 | }, 54 | location: { 55 | type: String, 56 | default: 'India', 57 | required: false, 58 | }, 59 | unit: { 60 | type: String, 61 | default: 'metric', 62 | required: false, 63 | }, 64 | longitude: { 65 | type: String, 66 | default: '77.216721', 67 | required: false, 68 | }, 69 | latitude: { 70 | type: String, 71 | default: '28.644800', 72 | required: false, 73 | }, 74 | camerafeed: { 75 | type: Boolean, 76 | default: false, 77 | }, 78 | networkdevices: { 79 | type: Boolean, 80 | default: false, 81 | }, 82 | todolist: { 83 | type: Boolean, 84 | default: false, 85 | }, 86 | snippetmanager: { 87 | type: Boolean, 88 | default: false, 89 | }, 90 | authenticator: { 91 | type: Boolean, 92 | default: false, 93 | }, 94 | }, { 95 | timestamps: true, 96 | }); 97 | 98 | const User = mongoose.model('User', userSchema); 99 | 100 | module.exports = User; -------------------------------------------------------------------------------- /client/src/components/Modals/DeleteUserModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { deletedUserState, deleteUserModalState, loadingState, loginState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import NiceModal from '../NiceViews/NiceModal'; 7 | import makeToast from '../../utils/ToastUtils'; 8 | import { useNavigate } from 'react-router-dom'; 9 | 10 | const DeleteUserModal = () => { 11 | const navigate = useNavigate(); 12 | 13 | const [modalState, setModalState] = useRecoilState(deleteUserModalState); 14 | const loginData = useRecoilValue(loginState); 15 | const setLoading = useSetRecoilState(loadingState); 16 | const setDeletedUser = useSetRecoilState(deletedUserState); 17 | 18 | const closeModal = () => { 19 | setModalState({ ...modalState, isOpen: false }); 20 | }; 21 | 22 | const confirmDelete = () => { 23 | setLoading(true); 24 | 25 | ApiService.get(`/api/v1/accounts/delete/${modalState.data?.userId}`, loginData?.token, navigate) 26 | .then(() => { 27 | makeToast("success", "User deleted."); 28 | setDeletedUser(modalState.data?.userId); 29 | closeModal(); 30 | }) 31 | .catch((error) => { 32 | if (!error.handled) makeToast("error", "User cannot be deleted."); 33 | }) 34 | .finally(() => { 35 | setLoading(false); 36 | }); 37 | }; 38 | 39 | return ( 40 | Are you sure you want to delete this user? All data will also be deleted.

    } 44 | footer={ 45 | <> 46 | 51 | 56 | 57 | } /> 58 | ); 59 | } 60 | 61 | const MemoizedComponent = React.memo(DeleteUserModal); 62 | export default MemoizedComponent; 63 | -------------------------------------------------------------------------------- /client/src/components/Modals/DeleteCodeItemModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { loadingState, loginState, newDeleteCodeModalState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import NiceModal from '../NiceViews/NiceModal'; 7 | import makeToast from '../../utils/ToastUtils'; 8 | import emitter, { RELOAD_CODE_SNIPPET } from '../../events'; 9 | import { useNavigate } from 'react-router-dom'; 10 | 11 | const DeleteCodeItemModal = () => { 12 | const navigate = useNavigate(); 13 | 14 | const [modalState, setModalState] = useRecoilState(newDeleteCodeModalState); 15 | const loginData = useRecoilValue(loginState); 16 | const setLoading = useSetRecoilState(loadingState); 17 | 18 | const closeModal = () => { 19 | setModalState({ ...modalState, isOpen: false }); 20 | }; 21 | 22 | const confirmDelete = () => { 23 | setLoading(true); 24 | 25 | ApiService.get(`/api/v1/snippet/${modalState.data?.snippetId}/delete/${modalState.data?.snippetItem?._id}`, loginData?.token, navigate) 26 | .then(() => { 27 | makeToast("success", "Code item deleted."); 28 | emitter.emit(RELOAD_CODE_SNIPPET); 29 | closeModal(); 30 | }) 31 | .catch((error) => { 32 | if (!error.handled) makeToast("error", "Code item cannot be deleted."); 33 | }) 34 | .finally(() => { 35 | setLoading(false); 36 | }); 37 | }; 38 | 39 | return ( 40 | Are you sure you want to delete this code item?

    } 44 | footer={ 45 | <> 46 | 51 | 56 | 57 | } /> 58 | ); 59 | } 60 | 61 | 62 | const MemoizedComponent = React.memo(DeleteCodeItemModal); 63 | export default MemoizedComponent; 64 | 65 | -------------------------------------------------------------------------------- /client/src/components/Modals/DeleteTodoModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { deletedTodoState, loadingState, loginState, newDeleteModalState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import NiceModal from '../NiceViews/NiceModal'; 7 | import makeToast from '../../utils/ToastUtils'; 8 | import { useNavigate } from 'react-router-dom'; 9 | 10 | const DeleteTodoModal = () => { 11 | const navigate = useNavigate(); 12 | 13 | const [modalState, setModalState] = useRecoilState(newDeleteModalState); 14 | const loginData = useRecoilValue(loginState); 15 | const setLoading = useSetRecoilState(loadingState); 16 | const setDeletedTodo = useSetRecoilState(deletedTodoState); 17 | 18 | const closeModal = () => { 19 | setModalState({ ...modalState, isOpen: false }); 20 | }; 21 | 22 | const confirmDelete = () => { 23 | setLoading(true); 24 | 25 | ApiService.get(`/api/v1/todo/${modalState.data?.listingId}/delete/${modalState.data?.todoItem?._id}`, loginData?.token, navigate) 26 | .then(() => { 27 | makeToast("success", "Todo item deleted."); 28 | setDeletedTodo(modalState.data?.todoItem); 29 | closeModal(); 30 | }) 31 | .catch((error) => { 32 | if (!error.handled) makeToast("error", "Todo item cannot be deleted."); 33 | }) 34 | .finally(() => { 35 | setLoading(false); 36 | }); 37 | }; 38 | 39 | return ( 40 | Are you sure you want to delete this todo item?

    } 44 | footer={ 45 | <> 46 | 51 | 56 | 57 | } /> 58 | ); 59 | 60 | } 61 | 62 | const MemoizedComponent = React.memo(DeleteTodoModal); 63 | export default MemoizedComponent; 64 | -------------------------------------------------------------------------------- /client/src/components/Modals/DeleteSnippetItemModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { deletedSnippetState, deleteSnippetModalState, loadingState, loginState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import NiceModal from '../NiceViews/NiceModal'; 7 | import makeToast from '../../utils/ToastUtils'; 8 | import { useNavigate } from 'react-router-dom'; 9 | 10 | const DeleteSnippetItemModal = () => { 11 | const navigate = useNavigate(); 12 | 13 | const [modalState, setModalState] = useRecoilState(deleteSnippetModalState); 14 | const loginData = useRecoilValue(loginState); 15 | const setLoading = useSetRecoilState(loadingState); 16 | const setDeletedSnippet = useSetRecoilState(deletedSnippetState); 17 | 18 | const closeModal = () => { 19 | setModalState({ ...modalState, isOpen: false }); 20 | }; 21 | 22 | const confirmDelete = () => { 23 | setLoading(true); 24 | 25 | ApiService.get(`/api/v1/snippet/${modalState.data?.snippetItem._id}/delete`, loginData?.token, navigate) 26 | .then(() => { 27 | makeToast("success", "Snippet deleted."); 28 | setDeletedSnippet(modalState.data?.snippetItem); 29 | closeModal(); 30 | }) 31 | .catch((error) => { 32 | if (!error.handled) makeToast("error", "Snippet cannot be deleted."); 33 | }) 34 | .finally(() => { 35 | setLoading(false); 36 | }); 37 | }; 38 | 39 | return ( 40 | Are you sure you want to delete this snippet?

    } 44 | footer={ 45 | <> 46 | 51 | 56 | 57 | } /> 58 | ); 59 | 60 | } 61 | 62 | const MemoizedComponent = React.memo(DeleteSnippetItemModal); 63 | export default MemoizedComponent; 64 | -------------------------------------------------------------------------------- /client/src/components/Modals/RemoveInstalledIntegration.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { removeInstalledIntegrationModalState, loadingState, loginState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import NiceModal from '../NiceViews/NiceModal'; 7 | import makeToast from '../../utils/ToastUtils'; 8 | import { useNavigate } from 'react-router-dom'; 9 | import emitter, { RELOAD_INSTALLED_APPS } from '../../events'; 10 | 11 | 12 | const RemoveInstalledIntegration = () => { 13 | const navigate = useNavigate(); 14 | 15 | const [modalState, setModalState] = useRecoilState(removeInstalledIntegrationModalState); 16 | const loginData = useRecoilValue(loginState); 17 | const setLoading = useSetRecoilState(loadingState); 18 | 19 | const closeModal = () => { 20 | setModalState({ ...modalState, isOpen: false }); 21 | }; 22 | 23 | const confirmDelete = () => { 24 | setLoading(true); 25 | 26 | ApiService.get(`/api/v1/app/${modalState.data?.app.appId}/delete`, loginData?.token, navigate) 27 | .then(() => { 28 | makeToast("success", "Integration removed."); 29 | //setDeletedSnippet(modalState.data?.snippetItem); 30 | emitter.emit(RELOAD_INSTALLED_APPS) 31 | closeModal(); 32 | }) 33 | .catch((error) => { 34 | if (!error.handled) makeToast("error", "Integration cannot be remove."); 35 | }) 36 | .finally(() => { 37 | setLoading(false); 38 | }); 39 | }; 40 | 41 | return ( 42 | Are you sure you want to remove this integration?

    } 46 | footer={ 47 | <> 48 | 53 | 58 | 59 | } /> 60 | ); 61 | 62 | } 63 | 64 | const MemoizedComponent = React.memo(RemoveInstalledIntegration); 65 | export default MemoizedComponent; 66 | -------------------------------------------------------------------------------- /server/migrations/20250108064334-migrate_old_icons.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(db, client) { 3 | 4 | const listingsCollection = db.collection('listings'); 5 | const authenticatorsCollection = db.collection('authenticators'); 6 | const networkdevicesCollection = db.collection('networkdevices'); 7 | 8 | //Migrate Listing Icons 9 | const allListings = await listingsCollection.find({}).toArray(); 10 | 11 | allListings.forEach(async (listing) => { 12 | //if listing.listingIcon is a String, make it an object in format {iconUrl: listing.listingIcons, iconUrlLight: null, iconProvider: 'com.astroluma.self'} 13 | if (typeof listing.listingIcon === 'string') { 14 | const icon = { 15 | iconUrl: listing.listingIcon, 16 | iconUrlLight: null, 17 | iconProvider: 'com.astroluma.self' 18 | }; 19 | await listingsCollection.updateOne({ _id: listing._id }, { $set: { listingIcon: icon } }); 20 | } 21 | }); 22 | 23 | //Migrate Authenticator Icons 24 | const allAuths = await authenticatorsCollection.find({}).toArray(); 25 | 26 | allAuths.forEach(async (singleAuth) => { 27 | if (typeof singleAuth.serviceIcon === 'string') { 28 | const icon = { 29 | iconUrl: singleAuth.serviceIcon, 30 | iconUrlLight: null, 31 | iconProvider: 'com.astroluma.self' 32 | }; 33 | await authenticatorsCollection.updateOne({ _id: singleAuth._id }, { $set: { serviceIcon: icon } }); 34 | } 35 | }); 36 | 37 | //Migrate Network Devices Icons 38 | const allDevices = await networkdevicesCollection.find({}).toArray(); 39 | 40 | allDevices.forEach(async (singleDevice) => { 41 | if (typeof singleDevice.deviceIcon === 'string') { 42 | const icon = { 43 | iconUrl: singleDevice.deviceIcon, 44 | iconUrlLight: null, 45 | iconProvider: 'com.astroluma.self' 46 | }; 47 | await networkdevicesCollection.updateOne({ _id: singleDevice._id }, { $set: { deviceIcon: icon } }); 48 | } 49 | }); 50 | 51 | }, 52 | 53 | async down(db, client) { 54 | // TODO write the statements to rollback your migration (if possible) 55 | // Example: 56 | // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}}); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /server/apps/com.github/templates/response.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 |
    6 | 7 | 8 | 9 | PRs: 10 | {{numPR}}
    15 | 16 | 17 | 18 | Last PR: 19 | {{lastPR}}
    24 | 25 | 26 | 27 | Author: 28 | {{author}}
    33 |
    -------------------------------------------------------------------------------- /client/src/components/Modals/ConfirmPacketSendModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useRecoilState, useRecoilValue } from 'recoil'; 3 | import { loginState, sendmagicPacketState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import NiceModal from '../NiceViews/NiceModal'; 7 | import NiceLoader from '../NiceViews/NiceLoader'; 8 | import makeToast from '../../utils/ToastUtils'; 9 | import { useNavigate } from 'react-router-dom'; 10 | 11 | const ConfirmPacketSendModal = () => { 12 | const navigate = useNavigate(); 13 | 14 | const [modalState, setModalState] = useRecoilState(sendmagicPacketState); 15 | const loginData = useRecoilValue(loginState); 16 | const [isSending, setIsSending] = useState(false); 17 | 18 | const closeModal = () => { 19 | setModalState({ data: null, isOpen: false }); 20 | }; 21 | 22 | const sendPacketNow = () => { 23 | setIsSending(true); 24 | 25 | ApiService.get(`/api/v1/networkdevices/wake/${modalState.data._id}`, loginData?.token, navigate) 26 | .then(data => { 27 | makeToast("success", String(data?.message)); 28 | }) 29 | .catch((error) => { 30 | if (!error.handled) makeToast("error", "Failed to send packet"); 31 | }).finally(() => { 32 | setIsSending(false); 33 | setModalState({ data: null, isOpen: false }); 34 | }); 35 | }; 36 | 37 | return ( 38 | 45 | 46 |
    :

    Are you sure you want to wake this device?

    47 | } 48 | footer={ 49 | !isSending && <> 50 | 55 | 60 | 61 | } /> 62 | ); 63 | } 64 | 65 | const MemoizedComponent = React.memo(ConfirmPacketSendModal); 66 | export default MemoizedComponent; 67 | 68 | -------------------------------------------------------------------------------- /client/src/components/Misc/NoListing.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | import PropTypes from 'prop-types'; 4 | import NiceLink from '../NiceViews/NiceLink'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | 7 | import { contentLoadingState, loadingState } from '../../atoms'; 8 | 9 | const NoListing = ({ mainText, subText, buttonText, buttonLink, displayIcon: DisplayIcon, buttonOnClick }) => { 10 | const contentLoading = useRecoilValue(contentLoadingState); 11 | const loading = useRecoilValue(loadingState); 12 | 13 | return ( 14 | (!contentLoading && !loading) &&
    17 |
    18 |

    19 | {mainText} 20 |

    21 |

    22 | {subText} 23 |

    24 |
    25 | {DisplayIcon &&
    26 | {DisplayIcon} 27 |
    } 28 |
    29 |
    30 | { 31 | buttonLink && 36 | } 37 | { 38 | buttonOnClick && 43 | } 44 | 45 |
    46 |
    47 |
    48 | ); 49 | }; 50 | 51 | NoListing.propTypes = { 52 | mainText: PropTypes.string.isRequired, 53 | subText: PropTypes.string.isRequired, 54 | buttonText: PropTypes.string.isRequired, 55 | buttonLink: PropTypes.string, 56 | displayIcon: PropTypes.element, 57 | buttonOnClick: PropTypes.func 58 | }; 59 | 60 | const MemoizedComponent = React.memo(NoListing); 61 | export default MemoizedComponent; -------------------------------------------------------------------------------- /server/apps/com.youtube/app.js: -------------------------------------------------------------------------------- 1 | 2 | const formatCount = (count) => { 3 | if (count >= 1000000) { 4 | return `${(count / 1000000).toFixed(1)}M`; 5 | } else if (count >= 1000) { 6 | return `${(count / 1000).toFixed(1)}K`; 7 | } else { 8 | return count.toString(); 9 | } 10 | } 11 | 12 | const connectionTest = async (testerInstance) => { 13 | //implementa a connection tester logic 14 | try { 15 | const connectionUrl = testerInstance?.appUrl; 16 | const apiKey = testerInstance?.config?.apiKey; 17 | 18 | if (!connectionUrl || !apiKey) { 19 | return testerInstance.connectionFailed("YouTube link or API key is missing."); 20 | } 21 | 22 | const videoId = connectionUrl.split('v=')[1]; 23 | const apiUrl = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${apiKey}&part=snippet,statistics`; 24 | 25 | await testerInstance?.axios.get(apiUrl); 26 | 27 | await testerInstance.connectionSuccess(); 28 | 29 | } catch (error) { 30 | await testerInstance.connectionFailed(error); 31 | } 32 | } 33 | 34 | const initialize = async (application) => { 35 | const youtubeLink = application?.appUrl; 36 | const apiKey = application?.config?.apiKey; 37 | 38 | if (!youtubeLink || !apiKey) { 39 | return application.sendError('YouTube link or API key is missing.'); 40 | } 41 | 42 | const videoId = youtubeLink.split('v=')[1]; 43 | const apiUrl = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${apiKey}&part=snippet,statistics`; 44 | 45 | try { 46 | const response = await application?.axios.get(apiUrl); 47 | const videoData = response.data.items[0]; 48 | const thumb = response.data.items[0].snippet.thumbnails.high.url; 49 | 50 | const variables = [ 51 | { key: '{{views}}', value: formatCount(videoData.statistics.viewCount) }, 52 | { key: '{{likes}}', value: formatCount(videoData.statistics.likeCount) }, 53 | { key: '{{comments}}', value: formatCount(videoData.statistics.commentCount) }, 54 | { key: '{{downloadLink}}', value: youtubeLink }, 55 | { key: '{{youtubeLink}}', value: youtubeLink } 56 | ]; 57 | 58 | await application.sendResponse('response.tpl', 200, variables, thumb); 59 | 60 | } catch (error) { 61 | //console.log(error); 62 | await application.sendError(error); 63 | } 64 | } 65 | 66 | global.initialize = initialize; 67 | global.connectionTest = connectionTest; -------------------------------------------------------------------------------- /client/src/components/Modals/ImageSelectorModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { iconPackState, imageModalState, selectedImageState } from '../../atoms'; 4 | import NiceModal from '../NiceViews/NiceModal'; 5 | import NiceTab from '../NiceViews/NiceTab'; 6 | import MyIconsSection from '../Icons/MyIconsSection'; 7 | import CustomIconPack from '../Icons/CustomIconPack'; 8 | import { BrowserRouter } from 'react-router-dom'; 9 | 10 | const ImageSelectorModal = () => { 11 | const [modalState, setModalState] = useRecoilState(imageModalState); 12 | const [tabConfig, setTabConfig] = useState([]); 13 | 14 | const setSelectedImage = useSetRecoilState(selectedImageState); 15 | 16 | const allIconPacks = useRecoilValue(iconPackState); 17 | 18 | const [activeTab, setActiveTab] = useState("com.astroluma.self"); 19 | 20 | useEffect(() => { 21 | setActiveTab("com.astroluma.self"); 22 | }, [modalState.isOpen]); 23 | 24 | useEffect(() => { 25 | const tempItemArray = allIconPacks?.map(pack => ({ 26 | name: pack.iconProvider, 27 | label: pack.iconName 28 | })); 29 | tempItemArray?.unshift({ name: 'com.astroluma.self', label: 'My Icons' }); 30 | setTabConfig(tempItemArray); 31 | }, [allIconPacks]); 32 | 33 | const closeModal = () => { 34 | setModalState(prev => ({ ...prev, isOpen: false })); 35 | }; 36 | 37 | const handleSelectImage = (image) => { 38 | setSelectedImage({image, data: modalState?.data}); 39 | closeModal(); 40 | }; 41 | 42 | const handleTabSelection = (tabName) => { 43 | setActiveTab(tabName); 44 | } 45 | 46 | return ( 47 | 53 | 54 | 55 |
    56 | { 57 | activeTab === "com.astroluma.self" && 58 | } 59 | { 60 | allIconPacks?.map(iconPack => ( 61 | iconPack.iconProvider === activeTab && ( 62 | 63 | ) 64 | )) 65 | } 66 |
    67 | 68 | } 69 | /> 70 | ); 71 | }; 72 | 73 | export default React.memo(ImageSelectorModal); -------------------------------------------------------------------------------- /client/src/components/Modals/ManagePublishPageModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { changedPageState, loadingState, loginState, publishPageModalState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import NiceModal from '../NiceViews/NiceModal'; 7 | import makeToast from '../../utils/ToastUtils'; 8 | import { useNavigate } from 'react-router-dom'; 9 | 10 | const ManagePublishPageModal = () => { 11 | const navigate = useNavigate(); 12 | 13 | const [modalState, setModalState] = useRecoilState(publishPageModalState); 14 | const loginData = useRecoilValue(loginState); 15 | const setLoading = useSetRecoilState(loadingState); 16 | const setDeletedPage = useSetRecoilState(changedPageState); 17 | 18 | const closeModal = () => { 19 | setModalState({ ...modalState, isOpen: false }); 20 | }; 21 | 22 | const confirmDelete = () => { 23 | setLoading(true); 24 | 25 | ApiService.get(`/api/v1/page/action/${modalState.data?.pageId}/${modalState.data?.isPublished ? "unpublish" : "publish"}`, loginData?.token, navigate) 26 | .then(() => { 27 | makeToast("success", modalState.data?.isPublished ? "Page unpublished." : "Page published."); 28 | setDeletedPage(modalState.data?.pageId); 29 | closeModal(); 30 | }) 31 | .catch((error) => { 32 | if (!error.handled) makeToast("error", modalState.data?.isPublished ? "Page can't be unpublished." : "Page can't be published."); 33 | }) 34 | .finally(() => { 35 | setLoading(false); 36 | }); 37 | }; 38 | 39 | return ( 40 | 46 |

    Are you sure you want to {modalState.data?.isPublished ? "unpublish" : "publish"} this page?

    47 |
    48 | } 49 | footer={ 50 |
    51 | 56 | 61 |
    62 | } 63 | /> 64 | ); 65 | } 66 | 67 | const MemoizedComponent = React.memo(ManagePublishPageModal); 68 | export default MemoizedComponent; 69 | 70 | -------------------------------------------------------------------------------- /client/src/components/Layout/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useRecoilState, useSetRecoilState } from 'recoil'; 3 | import { authenticatorPanelState, filterQueryState, selectedAuthState } from '../../atoms'; 4 | import AuthenticatorSidebar from '../Authenticator/AuthenticatorSidebar'; 5 | import { useLocation } from 'react-router-dom'; 6 | import { AnimatePresence } from 'framer-motion'; 7 | import Sidebar from '../Sidebar/index'; 8 | import Header from '../Header/index'; 9 | import PropTypes from 'prop-types'; 10 | import { useBottomScrollListener } from 'react-bottom-scroll-listener'; 11 | import Drawer from 'react-modern-drawer' 12 | import emitter, { PAGE_BOTTOM_EVENT } from '../../events'; 13 | 14 | const Layout = ({ children }) => { 15 | 16 | // Mobile sidebar visibility state 17 | const [showSidebar, setShowSidebar] = useState(false); 18 | const [showAuthenticator, setShowAuthenticator] = useRecoilState(authenticatorPanelState); 19 | const setSelectedService = useSetRecoilState(selectedAuthState); 20 | const setFilterQuery = useSetRecoilState(filterQueryState); 21 | 22 | 23 | const location = useLocation(); 24 | 25 | useEffect(() => { 26 | setShowSidebar(false); 27 | setSelectedService(null); 28 | setFilterQuery(""); 29 | }, [location, setSelectedService, setFilterQuery]); 30 | 31 | 32 | const scrollRef = useBottomScrollListener(() => { 33 | emitter.emit(PAGE_BOTTOM_EVENT) 34 | }); 35 | 36 | const authPanelClosed = () => { 37 | setShowAuthenticator(false); 38 | } 39 | 40 | return ( 41 | <> 42 | 43 | 44 |
    45 |
    46 |
    47 | {children} 48 |
    49 |
    50 | 51 | 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | 67 | Layout.propTypes = { 68 | children: PropTypes.node.isRequired 69 | }; 70 | 71 | const MemoizedComponent = React.memo(Layout); 72 | export default MemoizedComponent; 73 | -------------------------------------------------------------------------------- /server/apps/com.youtube/templates/m-response.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 |
    9 | 10 | 11 | 12 | Views: 13 | {{views}}
    18 | 19 | 20 | 21 | Likes: 22 | {{likes}}
    27 | 28 | 29 | 30 | Comments: 31 | {{comments}}
    36 | 37 | Download Video 38 | 39 | 40 | Open on YouTube 41 | 42 |
    -------------------------------------------------------------------------------- /server/apps/com.proxyman/app.js: -------------------------------------------------------------------------------- 1 | 2 | const connectionTest = async (testerInstance) => { 3 | //implementa a connection tester logic 4 | try { 5 | const apiUrl = testerInstance?.appUrl; 6 | 7 | const { email, password } = testerInstance.config; 8 | 9 | if (!email || !password || !apiUrl) { 10 | await testerInstance.connectionFailed("Please provide all the required configuration parameters"); 11 | } 12 | 13 | const tokenResponse = await testerInstance?.axios.post(`${apiUrl}/api/tokens`, { 14 | identity: email, 15 | secret: password, 16 | expiry: '1y' 17 | }, { 18 | headers: { 19 | 'Content-Type': 'application/json; charset=UTF-8' 20 | } 21 | }); 22 | 23 | if (tokenResponse?.data?.token) { 24 | await testerInstance.connectionSuccess(); 25 | } else { 26 | await testerInstance.connectionFailed("Invalid credentials"); 27 | } 28 | } catch (error) { 29 | await testerInstance.connectionFailed(error); 30 | } 31 | } 32 | 33 | 34 | const initialize = async (application) => { 35 | 36 | const { email, password } = application.config; 37 | 38 | const apiUrl = application?.appUrl; 39 | 40 | if (!email || !password || !apiUrl) { 41 | await application.sendError('Please provide all the required configuration parameters'); 42 | } 43 | 44 | try { 45 | // Obtain the bearer token 46 | const tokenResponse = await application?.axios.post(`${apiUrl}/api/tokens`, { 47 | identity: email, 48 | secret: password, 49 | expiry: '1y' 50 | }, { 51 | headers: { 52 | 'Content-Type': 'application/json; charset=UTF-8' 53 | } 54 | }); 55 | 56 | const token = tokenResponse.data.token; 57 | 58 | // Fetch the statistics using the bearer token 59 | const statsResponse = await application?.axios.get(`${apiUrl}/api/reports/hosts`, { 60 | headers: { 61 | 'Authorization': `Bearer ${token}` 62 | } 63 | }); 64 | 65 | const data = statsResponse.data; 66 | 67 | const variables = [ 68 | { key: '{{dead}}', value: data.dead }, 69 | { key: '{{proxy}}', value: data.proxy }, 70 | { key: '{{redirection}}', value: data.redirection }, 71 | { key: '{{stream}}', value: data.stream }, 72 | { key: '{{proxyManagerLink}}', value: apiUrl } 73 | ]; 74 | 75 | await application.sendResponse('response.tpl', 200, variables); 76 | 77 | } catch (error) { 78 | await application.sendError(error); 79 | } 80 | } 81 | 82 | global.initialize = initialize; 83 | global.connectionTest = connectionTest; -------------------------------------------------------------------------------- /server/utils/apiutils.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | const os = require('os'); 3 | const CryptoJS = require('crypto-js'); 4 | 5 | exports.isValidStream = (url) => { 6 | return new Promise((resolve, reject) => { 7 | // Spawn the ffprobe process 8 | const ffprobe = spawn('ffprobe', [ 9 | '-v', 'error', // Show only errors 10 | '-show_streams', // Display stream information 11 | '-select_streams', 'v', // Select video streams 12 | '-i', url // Input URL 13 | ]); 14 | 15 | let output = ''; 16 | let errorOutput = ''; 17 | 18 | // Collect stdout data 19 | ffprobe.stdout.on('data', (data) => { 20 | output += data.toString(); 21 | }); 22 | 23 | // Collect stderr data (for errors) 24 | ffprobe.stderr.on('data', (data) => { 25 | errorOutput += data.toString(); 26 | }); 27 | 28 | // Handle process close 29 | ffprobe.on('close', (code) => { 30 | if (code === 0 && output.includes('codec_name')) { 31 | // If successful and metadata includes codec information 32 | resolve(true); 33 | } else { 34 | console.error(`ffprobe error: ${errorOutput}`); 35 | resolve(false); 36 | } 37 | }); 38 | 39 | // Handle errors 40 | ffprobe.on('error', (err) => { 41 | console.error(`Failed to start ffprobe: ${err.message}`); 42 | reject(err); 43 | }); 44 | }); 45 | } 46 | 47 | exports.isHostMode = () => { 48 | const inHostMode = process.env.HOST_MODE === 'true'; 49 | return inHostMode; 50 | } 51 | 52 | exports.getSecretKey = () => { 53 | let secret = process.env.SECRET_KEY; 54 | 55 | //If secret is empty, generate a secret that doesn't change on this server. Probably use MAC address or something 56 | if (!secret) { 57 | const networkInterfaces = os.networkInterfaces(); 58 | let macAddress = ''; 59 | for (const key in networkInterfaces) { 60 | const networkInterface = networkInterfaces[key]; 61 | for (const ni of networkInterface) { 62 | if (ni.mac && ni.mac !== '00:00:00:00:00:00') { 63 | macAddress = ni.mac; 64 | break; 65 | } 66 | } 67 | if (macAddress) { 68 | break; 69 | } 70 | } 71 | 72 | if (!macAddress) { 73 | macAddress = '00:00:00:00:00:00'; 74 | } 75 | 76 | //Encrypt this string and make it 32 characters long 77 | secret = CryptoJS.AES.encrypt(macAddress, 'astroluma').toString().substring(0, 32); 78 | } 79 | 80 | return secret; 81 | } -------------------------------------------------------------------------------- /client/src/components/Page/ViewPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useNavigate, useParams } from 'react-router-dom'; 3 | import { Helmet } from "react-helmet" 4 | import useDynamicFilter from '../../hooks/useDynamicFilter'; 5 | import { useRecoilValue, useSetRecoilState } from 'recoil'; 6 | import { contentLoadingState, loginState } from '../../atoms'; 7 | import useCurrentRoute from '../../hooks/useCurrentRoute'; 8 | import ApiService from '../../utils/ApiService'; 9 | import Breadcrumb from '../Breadcrumb/Breadcrumb'; 10 | import './ViewPage.css' 11 | 12 | 13 | const ViewPage = () => { 14 | const params = useParams(); 15 | 16 | const pageId = params?.pageId; 17 | 18 | const navigate = useNavigate(); 19 | const loginData = useRecoilValue(loginState); 20 | const setLoading = useSetRecoilState(contentLoadingState); 21 | 22 | const [pageTitle, setPageTitle] = useState(""); 23 | const [pageContent, setPageContent] = useState(""); 24 | 25 | useDynamicFilter(false); 26 | useCurrentRoute("/page"); 27 | 28 | //fetch details by pageId 29 | useEffect(() => { 30 | if (pageId) { 31 | setLoading(true); 32 | ApiService.get(`/api/v1/page/info/${pageId}/active`, loginData?.token, navigate) 33 | .then(data => { 34 | if (data?.message) { 35 | setPageTitle(data?.message?.pageTitle); 36 | setPageContent(data?.message?.pageContent); 37 | } else { 38 | setPageTitle(""); 39 | setPageContent(""); 40 | } 41 | }) 42 | .catch((error) => { 43 | if (!error.handled) navigate("/"); 44 | }).finally(() => { 45 | setLoading(false); 46 | }); 47 | } else { 48 | navigate("/"); 49 | } 50 | }, [pageId, loginData?.token, navigate, setLoading]); 51 | 52 | return ( 53 | <> 54 | 55 | {pageTitle} 56 | 57 | 58 | 59 | 60 |
    61 |

    62 | {pageTitle} 63 |

    64 |
    65 | {/* skipcq: JS-0440 */} 66 |
    67 |
    68 |
    69 | 70 | ); 71 | }; 72 | 73 | const MemoizedComponent = React.memo(ViewPage); 74 | export default MemoizedComponent; 75 | -------------------------------------------------------------------------------- /server/install-app-dependencies.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { execSync } = require('child_process'); 4 | 5 | // Define directories 6 | const appsDir = path.resolve(__dirname, 'apps'); 7 | const storageAppsDir = path.resolve(__dirname, '../storage/apps'); 8 | 9 | // Function to create directory if it doesn't exist 10 | const ensureDirectoryExists = (dirPath) => { 11 | if (!fs.existsSync(dirPath)) { 12 | console.log(`Creating directory: ${dirPath}`); 13 | fs.mkdirSync(dirPath, { recursive: true }); 14 | } 15 | }; 16 | 17 | // Function to copy directory 18 | const copyDirectory = (source, destination) => { 19 | try { 20 | console.log(`Copying ${source} to ${destination}`); 21 | fs.cpSync(source, destination, { recursive: true }); 22 | console.log('Directory copied successfully.'); 23 | } catch (error) { 24 | console.error(`Failed to copy directory from ${source} to ${destination}`); 25 | console.error(error); 26 | throw error; 27 | } 28 | }; 29 | 30 | // Function to install npm dependencies 31 | const installDependencies = (moduleDir) => { 32 | try { 33 | console.log(`Installing dependencies for module at ${moduleDir}...`); 34 | execSync('npm install', { cwd: moduleDir, stdio: 'inherit' }); 35 | console.log('Dependencies installed successfully.'); 36 | } catch (error) { 37 | console.error(`Failed to install dependencies for module at ${moduleDir}`); 38 | console.error(error); 39 | throw error; 40 | } 41 | }; 42 | 43 | // Main process 44 | const main = async () => { 45 | try { 46 | // Ensure storage/apps directory exists 47 | ensureDirectoryExists(storageAppsDir); 48 | 49 | // Read the source apps directory 50 | const files = fs.readdirSync(appsDir); 51 | 52 | // Process each app 53 | files.forEach((file) => { 54 | const sourceModuleDir = path.join(appsDir, file); 55 | const destinationModuleDir = path.join(storageAppsDir, file); 56 | const sourcePackageJsonPath = path.join(sourceModuleDir, 'package.json'); 57 | 58 | // Only process directories that contain package.json 59 | if (fs.existsSync(sourcePackageJsonPath)) { 60 | console.log(`Found package.json at ${sourcePackageJsonPath}`); 61 | 62 | // Copy the directory to storage/apps 63 | copyDirectory(sourceModuleDir, destinationModuleDir); 64 | 65 | // Install dependencies in the copied directory 66 | installDependencies(destinationModuleDir); 67 | } else { 68 | console.log(`No package.json found in ${sourceModuleDir}. Skipping.`); 69 | } 70 | }); 71 | 72 | console.log('All operations completed successfully.'); 73 | } catch (error) { 74 | console.error('An error occurred during processing:', error); 75 | process.exit(1); 76 | } 77 | }; 78 | 79 | // Run the main process 80 | main(); -------------------------------------------------------------------------------- /client/src/components/Layout/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Navigate, Outlet, useNavigate } from 'react-router-dom'; 3 | import { useSetRecoilState, useRecoilValue, useRecoilState } from "recoil"; 4 | import { 5 | authListState, 6 | colorThemeState, 7 | homepageItemState, 8 | iconPackState, 9 | isHostModeState, 10 | loadingState, 11 | loginState, 12 | reloadDashboardDataState, 13 | sidebarItemState, 14 | userDataState 15 | } from '../../atoms'; 16 | import ApiService from '../../utils/ApiService'; 17 | import makeToast from '../../utils/ToastUtils'; 18 | 19 | const PrivateRoute = () => { 20 | const navigate = useNavigate(); 21 | 22 | const setLoading = useSetRecoilState(loadingState); 23 | 24 | const loginData = useRecoilValue(loginState); 25 | 26 | const [reloadData, setReloadData] = useRecoilState(reloadDashboardDataState); 27 | 28 | const [authList, setAuthList] = useRecoilState(authListState); 29 | const [userData, setUserData] = useRecoilState(userDataState); 30 | const [sidebarItems, setSidebarItems] = useRecoilState(sidebarItemState); 31 | const [homepageItems, setHomepageItems] = useRecoilState(homepageItemState); 32 | const [iconPacks, setIconPacks] = useRecoilState(iconPackState); 33 | const setColorTheme = useSetRecoilState(colorThemeState); 34 | const setHostMode = useSetRecoilState(isHostModeState); 35 | 36 | const isDataMissing = !authList?.length || 37 | !userData || 38 | !sidebarItems?.length || 39 | !homepageItems?.length || 40 | !iconPacks?.length; 41 | 42 | useEffect(() => { 43 | if ((reloadData || isDataMissing) && loginData?.token) { 44 | setLoading(true); 45 | 46 | ApiService.get("/api/v1/dashboard", loginData ? loginData?.token : null, navigate) 47 | .then(data => { 48 | setAuthList(data?.message?.authenticators); 49 | setUserData(data?.message?.userData); 50 | setSidebarItems(data?.message?.sidebarItems); 51 | setHomepageItems(data?.message?.homepageItems); 52 | setIconPacks(data?.message?.iconPacks); 53 | setHostMode(data?.message?.isHostMode); 54 | 55 | const theme = data?.message?.userData?.colorTheme; 56 | 57 | setColorTheme(theme); 58 | }) 59 | .catch(error => { 60 | console.log(error); 61 | if (!error.handled) makeToast("error", "Error loading data..."); 62 | }) 63 | .finally(() => { 64 | setLoading(false); 65 | setReloadData(false); 66 | }); 67 | } 68 | }, [loginData, reloadData, navigate, setLoading, setAuthList, setUserData, setSidebarItems, 69 | setHomepageItems, setColorTheme, setReloadData, setIconPacks, setHostMode, isDataMissing]); 70 | 71 | return loginData?.token ? : ; 72 | }; 73 | 74 | 75 | const MemoizedComponent = React.memo(PrivateRoute); 76 | export default MemoizedComponent; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | client/build 133 | client/node_modules 134 | 135 | public/uploads/* 136 | preview/stream/* 137 | 138 | server/app 139 | 140 | app 141 | storage 142 | 143 | **/package-lock.json 144 | 145 | server/public/uploads -------------------------------------------------------------------------------- /client/src/components/Theme/SingleThemeItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { motion } from 'framer-motion'; 4 | 5 | import { IoCheckmarkCircle } from "react-icons/io5"; 6 | const SingleThemeItem = ({ theme, onSelect, isCurrentTheme }) => { 7 | 8 | const onThemeSelection = useCallback(() => { 9 | onSelect(theme); 10 | }, [onSelect, theme]); 11 | 12 | return ( 13 |
    14 | 15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 | { 33 | isCurrentTheme &&
    34 | 35 | Current 36 |
    37 | } 38 | 39 |
    {theme.label}
    40 |
    41 | ); 42 | }; 43 | 44 | SingleThemeItem.displayName = 'SingleThemeItem'; 45 | 46 | SingleThemeItem.propTypes = { 47 | theme: PropTypes.object.isRequired, 48 | onSelect: PropTypes.func.isRequired, 49 | isCurrentTheme: PropTypes.bool.isRequired, 50 | }; 51 | 52 | 53 | const MemoizedComponent = React.memo(SingleThemeItem); 54 | export default MemoizedComponent; 55 | -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceInput.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { FaEye, FaEyeSlash } from "react-icons/fa"; 4 | 5 | const NiceInput = ({ 6 | label = "", 7 | name = "", 8 | type = "text", 9 | value = "", 10 | onChange = null, 11 | placeholder, 12 | disabled = false, 13 | min = "", 14 | max = "", 15 | error = "", 16 | className = "" }) => { 17 | 18 | const [showPassword, setShowPassword] = useState(false); 19 | const timestamp = Date.now(); 20 | let id = Math.random().toString(36).substring(7); 21 | 22 | if (label) { 23 | id = label.toLowerCase().replace(/\s+/g, '') + timestamp; 24 | } else { 25 | id = Math.random().toString(36).substring(7) + timestamp; 26 | } 27 | 28 | const togglePassword = () => { 29 | setShowPassword(!showPassword); 30 | }; 31 | 32 | return ( 33 |
    34 | { 35 | label && 38 | } 39 |
    40 | 52 | {type === 'password' && ( 53 | 65 | )} 66 |
    67 | {error &&

    {error}

    } 68 |
    69 | ); 70 | }; 71 | 72 | NiceInput.propTypes = { 73 | label: PropTypes.string, 74 | name: PropTypes.string, 75 | type: PropTypes.string, 76 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 77 | onChange: PropTypes.func, 78 | placeholder: PropTypes.string, 79 | disabled: PropTypes.bool, 80 | min: PropTypes.string, 81 | max: PropTypes.string, 82 | error: PropTypes.string, 83 | className: PropTypes.string 84 | }; 85 | 86 | const MemoizedComponent = React.memo(NiceInput); 87 | export default MemoizedComponent; -------------------------------------------------------------------------------- /client/src/components/Modals/UpdateAvatarModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { imageModalState, loadingState, loginState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import NiceInput from '../NiceViews/NiceInput'; 7 | import NiceModal from '../NiceViews/NiceModal'; 8 | import makeToast from '../../utils/ToastUtils'; 9 | import { useNavigate } from 'react-router-dom'; 10 | 11 | const UpdateAvatarModal = () => { 12 | const navigate = useNavigate(); 13 | 14 | const [modalState, setModalState] = useRecoilState(imageModalState); 15 | const loginData = useRecoilValue(loginState); 16 | const setLoading = useSetRecoilState(loadingState); 17 | 18 | const [password, setPassword] = useState(); 19 | const [repeatPassword, setRepeatPassword] = useState(); 20 | 21 | const closeModal = () => { 22 | setModalState({ ...modalState, isOpen: false }); 23 | }; 24 | 25 | const changePassword = () => { 26 | 27 | if (!password || !repeatPassword) { 28 | makeToast("warning", "Password and Repeat Password are required."); 29 | return; 30 | } 31 | 32 | if (password !== repeatPassword) { 33 | makeToast("warning", "Password and Repeat Password do not match."); 34 | return; 35 | } 36 | 37 | setLoading(true); 38 | 39 | ApiService.post(`/api/v1/accounts/password/${modalState.data?.userId}`, { password }, loginData?.token, navigate) 40 | .then(() => { 41 | makeToast("success", "Password changed."); 42 | closeModal(); 43 | }) 44 | .catch((error) => { 45 | if (!error.handled) makeToast("error", "Password can not be changed."); 46 | }) 47 | .finally(() => { 48 | setLoading(false); 49 | }); 50 | }; 51 | 52 | return ( 53 | 58 | setPassword(e.target.value)} 64 | /> 65 | 66 | setRepeatPassword(e.target.value)} 72 | /> 73 | 74 | } 75 | footer={ 76 | <> 77 | 82 | 87 | 88 | } 89 | closeModal={closeModal} 90 | /> 91 | ) 92 | 93 | } 94 | 95 | const MemoizedComponent = React.memo(UpdateAvatarModal); 96 | export default MemoizedComponent; 97 | 98 | -------------------------------------------------------------------------------- /client/src/components/NiceViews/NiceUploader.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from "react"; 2 | import ImageView from "../Misc/ImageView"; 3 | import { useRecoilValue, useSetRecoilState } from "recoil"; 4 | import { colorThemeState, imageModalState } from "../../atoms"; 5 | import PropTypes from "prop-types"; 6 | import SystemThemes from "../../utils/SystemThemes"; 7 | 8 | const NiceUploader = ({ label = "Link Icon", selectedImage = null, placeholder = "Select or upload icon" }) => { 9 | const setModalState = useSetRecoilState(imageModalState); 10 | const colorTheme = useRecoilValue(colorThemeState); 11 | const [themeType, setThemeType] = useState("light"); 12 | 13 | useEffect(() => { 14 | const newThemeType = SystemThemes.find(theme => theme.value === colorTheme)?.type || "light"; 15 | setThemeType(newThemeType); 16 | }, [colorTheme]); 17 | 18 | const decideTheIcon = useCallback(() => { 19 | if (themeType === "dark" && selectedImage?.iconUrlLight) { 20 | return selectedImage?.iconUrlLight; 21 | } else { 22 | return selectedImage?.iconUrl; 23 | } 24 | }, [selectedImage, themeType]); 25 | 26 | const handleClick = () => setModalState({ isOpen: true, data: null }); 27 | 28 | return ( 29 |
    30 | 33 |
    34 |
    39 | {selectedImage ? ( 40 | 49 | ) : ( 50 |
    51 | )} 52 |
    53 |
    58 | {placeholder} 59 |
    60 |
    61 |
    62 | ); 63 | }; 64 | 65 | NiceUploader.propTypes = { 66 | label: PropTypes.string, 67 | selectedImage: PropTypes.object, 68 | placeholder: PropTypes.string 69 | }; 70 | 71 | const MemoizedComponent = React.memo(NiceUploader); 72 | export default MemoizedComponent; -------------------------------------------------------------------------------- /server/apps/com.github/templates/m-response.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 |
    9 | 10 | 11 | 12 | PRs: 13 | {{numPR}}
    18 | 19 | 20 | 21 | Last PR: 22 | {{lastPR}}
    27 | 28 | 29 | 30 | Author: 31 | {{author}}
    36 | 37 | 38 | Open Repository 39 | 40 |
    -------------------------------------------------------------------------------- /server/apps/com.portainer/app.js: -------------------------------------------------------------------------------- 1 | 2 | const connectionTest = async (testerInstance) => { 3 | try { 4 | const connectionUrl = testerInstance?.appUrl; 5 | const { username, password } = testerInstance.config; 6 | 7 | if (!username || !password || !connectionUrl) { 8 | await testerInstance.connectionFailed("Please provide all the required configuration parameters"); 9 | return; 10 | } 11 | 12 | const authUrl = `${connectionUrl}/api/auth`; 13 | 14 | const response = await testerInstance?.axios.post(authUrl, { 15 | username, 16 | password 17 | }); 18 | 19 | const data = response.data; 20 | 21 | if (data?.jwt) { 22 | await testerInstance.connectionSuccess(); 23 | } else { 24 | await testerInstance.connectionFailed('Invalid response from Portainer API'); 25 | } 26 | 27 | } catch (error) { 28 | await testerInstance.connectionFailed(error); 29 | } 30 | } 31 | 32 | const initialize = async (application) => { 33 | 34 | const { username, password } = application.config; 35 | 36 | const sanitizedListingUrl = application?.appUrl; 37 | 38 | if (!username || !password || !sanitizedListingUrl) { 39 | return await application.sendError('Please provide all the required configuration parameters'); 40 | } 41 | 42 | const authUrl = `${sanitizedListingUrl}/api/auth`; 43 | const endpointsUrl = `${sanitizedListingUrl}/api/endpoints`; 44 | 45 | try { 46 | // Step 1: Authenticate using the access token to get a JWT token 47 | const authResponse = await application?.axios.post(authUrl, { 48 | username, 49 | password 50 | }); 51 | 52 | const jwtToken = authResponse.data.jwt; 53 | 54 | // Step 2: Fetch the available endpoints 55 | const endpointsResponse = await application?.axios.get(endpointsUrl, { 56 | headers: { 57 | 'Authorization': `Bearer ${jwtToken}` 58 | } 59 | }); 60 | 61 | const endpoints = endpointsResponse.data; 62 | 63 | const endpointId = endpoints[0].Id; 64 | 65 | // Step 3: Use the endpoint ID to fetch data from Portainer 66 | const apiUrl = `${sanitizedListingUrl}/api/endpoints/${endpointId}/docker/info`; 67 | const response = await application?.axios.get(apiUrl, { 68 | headers: { 69 | 'Authorization': `Bearer ${jwtToken}` 70 | } 71 | }); 72 | 73 | const data = response.data; 74 | 75 | const variables = [ 76 | { key: '{{containers}}', value: data.Containers }, 77 | { key: '{{images}}', value: data.Images }, 78 | { key: '{{containersRunning}}', value: data.ContainersRunning }, 79 | { key: '{{containersPaused}}', value: data.ContainersPaused }, 80 | { key: '{{containersStopped}}', value: data.ContainersStopped }, 81 | { key: '{{version}}', value: data.ServerVersion }, 82 | { key: '{{portainerLink}}', value: sanitizedListingUrl } 83 | ]; 84 | 85 | await application.sendResponse('response.tpl', 200, variables); 86 | 87 | } catch (error) { 88 | await application.sendError(error); 89 | } 90 | } 91 | 92 | global.initialize = initialize; 93 | global.connectionTest = connectionTest; -------------------------------------------------------------------------------- /client/src/components/Modals/UpdatePasswordModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { changePasswordModalState, loadingState, loginState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import NiceInput from '../NiceViews/NiceInput'; 7 | import NiceModal from '../NiceViews/NiceModal'; 8 | import makeToast from '../../utils/ToastUtils'; 9 | import { useNavigate } from 'react-router-dom'; 10 | 11 | const UpdatePasswordModal = () => { 12 | const navigate = useNavigate(); 13 | 14 | const [modalState, setModalState] = useRecoilState(changePasswordModalState); 15 | const loginData = useRecoilValue(loginState); 16 | const setLoading = useSetRecoilState(loadingState); 17 | 18 | const [password, setPassword] = useState(); 19 | const [repeatPassword, setRepeatPassword] = useState(); 20 | 21 | const closeModal = () => { 22 | setPassword(""); 23 | setRepeatPassword(""); 24 | setModalState({ ...modalState, isOpen: false }); 25 | }; 26 | 27 | const changePassword = () => { 28 | 29 | if (!password || !repeatPassword) { 30 | makeToast("warning", "Password and Repeat Password are required."); 31 | return; 32 | } 33 | 34 | if (password !== repeatPassword) { 35 | makeToast("warning", "Password and Repeat Password do not match."); 36 | return; 37 | } 38 | 39 | setLoading(true); 40 | 41 | ApiService.post(`/api/v1/accounts/password/${modalState.data?.userId}`, { password }, loginData?.token, navigate) 42 | .then(() => { 43 | makeToast("success", "Password changed."); 44 | closeModal(); 45 | }) 46 | .catch((error) => { 47 | console.log(error); 48 | if (!error.handled) makeToast("error", "Password can not be changed."); 49 | }) 50 | .finally(() => { 51 | setLoading(false); 52 | }); 53 | }; 54 | 55 | return ( 56 | 61 | setPassword(e.target.value)} 67 | /> 68 | 69 | setRepeatPassword(e.target.value)} 75 | /> 76 | 77 | } 78 | footer={ 79 | <> 80 | 85 | 90 | 91 | } 92 | closeModal={closeModal} 93 | /> 94 | ) 95 | 96 | } 97 | 98 | const MemoizedComponent = React.memo(UpdatePasswordModal); 99 | export default MemoizedComponent; 100 | 101 | -------------------------------------------------------------------------------- /server/apps/com.github/app.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | const extractRepoInfo = (url) => { 4 | const match = url.match(/github\.com\/([^/]+)\/([^/]+)/); 5 | if (!match) { 6 | throw new Error("Invalid GitHub URL"); 7 | } 8 | return { 9 | owner: match[1], 10 | repo: match[2] 11 | }; 12 | } 13 | 14 | const connectionTest = async (testerInstance) => { 15 | //implementa a connection tester logic 16 | try { 17 | const connectionUrl = testerInstance?.appUrl; 18 | const {username, password} = testerInstance?.config; 19 | 20 | if (!connectionUrl || !username || !password) { 21 | return testerInstance.connectionFailed("GitHub link or username or password is missing."); 22 | } 23 | 24 | const { owner, repo } = extractRepoInfo(connectionUrl); 25 | 26 | const apiUrl = `https://api.github.com/repos/${owner}/${repo}/pulls`; 27 | 28 | await testerInstance?.axios.get(apiUrl, { 29 | auth: { 30 | username, 31 | password, 32 | }, 33 | params: { 34 | state: 'open', 35 | }, 36 | }); 37 | 38 | await testerInstance.connectionSuccess(); 39 | } catch (error) { 40 | await testerInstance.connectionFailed(error); 41 | } 42 | } 43 | 44 | const initialize = async (application) => { 45 | 46 | const {username, password} = application.config; 47 | 48 | const listingUrl = application?.appUrl; 49 | 50 | if(!username || !password || !listingUrl) { 51 | await application.sendError('Please provide all the required configuration parameters'); 52 | } 53 | 54 | try { 55 | const { owner, repo } = extractRepoInfo(listingUrl); 56 | 57 | const apiUrl = `https://api.github.com/repos/${owner}/${repo}/pulls`; 58 | 59 | const response = await application?.axios.get(apiUrl, { 60 | auth: { 61 | username, 62 | password, 63 | }, 64 | params: { 65 | state: 'open', 66 | }, 67 | }); 68 | 69 | const pullRequests = response.data; 70 | 71 | // Number of opened PRs 72 | const numberOfOpenPRs = pullRequests.length; 73 | 74 | // Last PR's date and time 75 | let lastPRDate = null; 76 | let lastPRAuthor = null; 77 | if (numberOfOpenPRs > 0) { 78 | // Sort PRs by creation date to find the last PR 79 | pullRequests.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); 80 | lastPRDate = pullRequests[0].created_at; 81 | lastPRAuthor = pullRequests[0].user.login; 82 | } 83 | 84 | const formattedDate = lastPRDate ? moment(lastPRDate).format('MMM Do YYYY, h:mm A') : "N/A"; 85 | 86 | const variables = [ 87 | { key: '{{numPR}}', value: numberOfOpenPRs }, 88 | { key: '{{lastPR}}', value: formattedDate }, 89 | { key: '{{author}}', value: lastPRAuthor ? lastPRAuthor : "N/A" }, 90 | { key: '{{repoLink}}', value: listingUrl } 91 | ]; 92 | 93 | await application.sendResponse('response.tpl', 200, variables); 94 | 95 | } catch (error) { 96 | //console.log(error); 97 | await application.sendError(error); 98 | } 99 | } 100 | 101 | global.initialize = initialize; 102 | global.connectionTest = connectionTest; 103 | -------------------------------------------------------------------------------- /client/src/components/Modals/BrandingRemovalModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { loadingState, loginState, reloadDashboardDataState, removeBrandingModalState } from '../../atoms'; 4 | import NiceButton from '../NiceViews/NiceButton'; 5 | import NiceModal from '../NiceViews/NiceModal'; 6 | import { BiCoffee } from 'react-icons/bi'; 7 | import { AiOutlineHeart } from 'react-icons/ai'; 8 | import { CONSTANTS } from '../../utils/Constants'; 9 | import makeToast from '../../utils/ToastUtils'; 10 | import ApiService from '../../utils/ApiService'; 11 | import { useNavigate } from 'react-router-dom'; 12 | 13 | const BrandingRemovalModal = () => { 14 | 15 | const navigate = useNavigate(); 16 | const [modalState, setModalState] = useRecoilState(removeBrandingModalState); 17 | const setLoading = useSetRecoilState(loadingState); 18 | const loginData = useRecoilValue(loginState); 19 | const setReloadData = useSetRecoilState(reloadDashboardDataState); 20 | 21 | const closeModal = () => { 22 | setModalState({ ...modalState, isOpen: false }); 23 | }; 24 | 25 | const doDonate = () => { 26 | window.open(CONSTANTS.BuyMeACoffee, '_blank'); 27 | }; 28 | 29 | const doRemoveBranding = (donating) => { 30 | setLoading(true); 31 | ApiService.get("/api/v1/accounts/debrand", loginData?.token, navigate) 32 | .then(() => { 33 | setReloadData(true); 34 | makeToast("success", "Astroluma branding removed successfully."); 35 | }) 36 | .catch((error) => { 37 | if (!error.handled) makeToast("error", "Error removing branding."); 38 | }).finally(() => { 39 | setLoading(false); 40 | closeModal(); 41 | if (donating) { 42 | doDonate(); 43 | } 44 | }); 45 | } 46 | 47 | return ( 48 | 53 |
    54 | 55 | 56 |
    57 |

    Help Us Keep Creating

    58 |
    59 |

    60 | Your support keeps this tool alive and thriving! Every contribution helps us maintain and improve the project. 61 |

    62 |

    63 | Plus, as a token of appreciation, contributors will be featured on the Astroluma portal, showcasing their invaluable support to our community. 64 |

    65 |

    66 | Branding removal is complimentary, but your support ensures we can continue building great tools for everyone. Thank you for helping us grow! 67 |

    68 |
    69 |
    70 | } 71 | footer={ 72 |
    73 | doRemoveBranding(false)} 77 | /> 78 | doRemoveBranding(true)} 82 | /> 83 |
    84 | } 85 | /> 86 | ); 87 | } 88 | 89 | const MemoizedComponent = React.memo(BrandingRemovalModal); 90 | export default MemoizedComponent; -------------------------------------------------------------------------------- /client/src/components/Settings/ThemeSettings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { colorThemeState, loadingState, loginState } from '../../atoms'; 4 | import ApiService from '../../utils/ApiService'; 5 | import NiceButton from '../NiceViews/NiceButton'; 6 | import SystemThemes from '../../utils/SystemThemes'; 7 | import makeToast from '../../utils/ToastUtils'; 8 | import { useNavigate } from 'react-router-dom'; 9 | 10 | const ThemeSettings = () => { 11 | 12 | const navigate = useNavigate(); 13 | 14 | const setLoading = useSetRecoilState(loadingState); 15 | const loginData = useRecoilValue(loginState); 16 | 17 | const [colorTheme, setColorTheme] = useRecoilState(colorThemeState); 18 | 19 | const [currentTheme, setCurrentTheme] = useState(colorTheme); 20 | 21 | const handleSelection = (e) => { 22 | e.preventDefault(); 23 | const selectedValue = e.target.value; 24 | setCurrentTheme(selectedValue); 25 | } 26 | 27 | const saveThemeSelection = () => { 28 | 29 | if (!currentTheme) { 30 | return makeToast("warning", "Theme selection is required"); 31 | } 32 | 33 | if (currentTheme === colorTheme) { 34 | return; 35 | } 36 | 37 | setColorTheme(currentTheme); 38 | 39 | //send data to save 40 | setLoading(true); 41 | ApiService.post("/api/v1/settings/theme", { colorTheme: currentTheme }, loginData?.token, navigate) 42 | .then(() => { 43 | makeToast("success", "Theme saved successfully."); 44 | //setReloadData(true); 45 | }) 46 | .catch((error) => { 47 | if (!error.handled) makeToast("error", "Theme could not be saved."); 48 | }).finally(() => { 49 | setLoading(false); 50 | }); 51 | 52 | } 53 | 54 | return ( 55 |
    56 |
    57 |

    Theme Settings

    58 |
    59 | 62 | 78 |
    79 |
    80 |
    81 | 86 |
    87 |
    88 | ); 89 | }; 90 | 91 | const MemoizedComponent = React.memo(ThemeSettings); 92 | export default MemoizedComponent; --------------------------------------------------------------------------------