--------------------------------------------------------------------------------
/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 |
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 |
11 | {listItems.map((item, index) => (
12 |
13 | {item.label}
14 |
15 | ))}
16 |
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 |
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 |
6 |
7 |
8 |
9 | Items:
10 |
11 | {{items}}
12 |
13 |
14 |
15 |
16 |
17 |
18 | Users:
19 |
20 | {{users}}
21 |
22 |
23 |
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 |
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 |
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 |
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 |
10 |
11 |
12 |
13 | Close modal
14 |
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 |
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 |
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 |
10 |
11 |
12 |
13 | Items:
14 |
15 | {{items}}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Users:
24 |
25 | {{users}}
26 |
27 |
28 |
29 |
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 |
6 |
7 |
8 |
9 | Proxy:
10 |
11 | {{proxy}}
12 |
13 |
14 |
15 |
16 |
17 |
18 | Redirection:
19 |
20 | {{redirection}}
21 |
22 |
23 |
24 |
25 |
26 |
27 | Streams:
28 |
29 | {{stream}}
30 |
31 |
32 |
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 &&
31 | {label}
32 |
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 |
39 | {label}
40 |
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 | You need to enable JavaScript to run this app.
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 |
6 |
7 |
8 |
9 | Views:
10 |
11 | {{views}}
12 |
13 |
14 |
15 |
16 |
17 |
18 | Likes:
19 |
20 | {{likes}}
21 |
22 |
23 |
24 |
25 |
26 |
27 | Comments:
28 |
29 | {{comments}}
30 |
31 |
32 |
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 |
6 |
7 |
8 |
9 | PRs:
10 |
11 | {{numPR}}
12 |
13 |
14 |
15 |
16 |
17 |
18 | Last PR:
19 |
20 | {{lastPR}}
21 |
22 |
23 |
24 |
25 |
26 |
27 | Author:
28 |
29 | {{author}}
30 |
31 |
32 |
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 |
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) &&
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 |