mirror of
https://github.com/DI0IK/homepage-plus.git
synced 2025-07-19 11:09:50 +00:00
homepage-plus
This commit is contained in:
parent
b2d75a99e7
commit
aa211bcc14
15 changed files with 271 additions and 56 deletions
|
@ -22,7 +22,7 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||
const [searchSuggestions, setSearchSuggestions] = useState([]);
|
||||
|
||||
const { data: widgets } = useSWR("/api/widgets");
|
||||
const searchWidget = Object.values(widgets).find((w) => w.type === "search");
|
||||
const searchWidget = widgets && Object.values(widgets).find((w) => w.type === "search");
|
||||
|
||||
let searchProvider;
|
||||
|
||||
|
|
17
src/pages/api/auth.js
Normal file
17
src/pages/api/auth.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { checkAllowedGroup, readIdentitySettings } from "utils/identity/identity-helpers";
|
||||
import { getSettings } from "utils/config/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { group } = req.query;
|
||||
const { provider, groups } = readIdentitySettings(getSettings().identity);
|
||||
|
||||
try {
|
||||
if (checkAllowedGroup(provider.getIdentity(req), groups, group)) {
|
||||
res.json({ group });
|
||||
} else {
|
||||
res.status(401).json({ message: "Group unathorized" });
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).send("Error getting user identity");
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
import { readIdentitySettings } from "utils/identity/identity-helpers";
|
||||
import { bookmarksResponse } from "utils/config/api-response";
|
||||
import { getSettings } from "utils/config/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.send(await bookmarksResponse());
|
||||
const { provider, groups } = readIdentitySettings(getSettings().identity);
|
||||
res.send(await bookmarksResponse(provider.getIdentity(req), groups));
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { readIdentitySettings } from "utils/identity/identity-helpers";
|
||||
import { servicesResponse } from "utils/config/api-response";
|
||||
import { getSettings } from "utils/config/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.send(await servicesResponse());
|
||||
const { provider, groups } = readIdentitySettings(getSettings().identity);
|
||||
res.send(await servicesResponse(provider.getIdentity(req), groups));
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { readIdentitySettings } from "utils/identity/identity-helpers";
|
||||
import { widgetsResponse } from "utils/config/api-response";
|
||||
import { getSettings } from "utils/config/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.send(await widgetsResponse());
|
||||
const { provider } = readIdentitySettings(getSettings().identity);
|
||||
res.send(await widgetsResponse(provider.getIdentity(req)));
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable react/no-array-index-key */
|
||||
import useSWR, { SWRConfig } from "swr";
|
||||
import useSWR, { unstable_serialize as unstableSerialize, SWRConfig } from "swr";
|
||||
import Head from "next/head";
|
||||
import Script from "next/script";
|
||||
import dynamic from "next/dynamic";
|
||||
|
@ -10,6 +10,7 @@ import { BiError } from "react-icons/bi";
|
|||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import NullIdentityProvider from "utils/identity/null";
|
||||
import Tab, { slugifyAndEncode } from "components/tab";
|
||||
import ServicesGroup from "components/services/group";
|
||||
import BookmarksGroup from "components/bookmarks/group";
|
||||
|
@ -26,6 +27,7 @@ import { bookmarksResponse, servicesResponse, widgetsResponse } from "utils/conf
|
|||
import ErrorBoundary from "components/errorboundry";
|
||||
import themes from "utils/styles/themes";
|
||||
import QuickLaunch from "components/quicklaunch";
|
||||
import { fetchWithIdentity, readIdentitySettings } from "utils/identity/identity-helpers";
|
||||
|
||||
const ThemeToggle = dynamic(() => import("components/toggles/theme"), {
|
||||
ssr: false,
|
||||
|
@ -41,25 +43,28 @@ const Version = dynamic(() => import("components/version"), {
|
|||
|
||||
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "openmeteo", "search", "datetime"];
|
||||
|
||||
export async function getStaticProps() {
|
||||
export async function getServerSideProps({ req }) {
|
||||
let logger;
|
||||
try {
|
||||
logger = createLogger("index");
|
||||
const { providers, ...settings } = getSettings();
|
||||
const { providers, identity, ...settings } = getSettings();
|
||||
const { provider, groups } = readIdentitySettings(identity);
|
||||
|
||||
const services = await servicesResponse();
|
||||
const bookmarks = await bookmarksResponse();
|
||||
const widgets = await widgetsResponse();
|
||||
const services = await servicesResponse(provider.getIdentity(req), groups);
|
||||
const bookmarks = await bookmarksResponse(provider.getIdentity(req), groups);
|
||||
const widgets = await widgetsResponse(provider.getIdentity(req));
|
||||
const identityContext = provider.getContext(req);
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialSettings: settings,
|
||||
fallback: {
|
||||
"/api/services": services,
|
||||
"/api/bookmarks": bookmarks,
|
||||
"/api/widgets": widgets,
|
||||
[unstableSerialize(["/api/services", identityContext])]: services,
|
||||
[unstableSerialize(["/api/bookmarks", identityContext])]: bookmarks,
|
||||
[unstableSerialize(["/api/widgets", identityContext])]: widgets,
|
||||
"/api/hash": false,
|
||||
},
|
||||
identityContext,
|
||||
...(await serverSideTranslations(settings.language ?? "en")),
|
||||
},
|
||||
};
|
||||
|
@ -67,22 +72,24 @@ export async function getStaticProps() {
|
|||
if (logger && e) {
|
||||
logger.error(e);
|
||||
}
|
||||
const identityContext = NullIdentityProvider.create().getContext(req);
|
||||
return {
|
||||
props: {
|
||||
initialSettings: {},
|
||||
fallback: {
|
||||
"/api/services": [],
|
||||
"/api/bookmarks": [],
|
||||
"/api/widgets": [],
|
||||
[unstableSerialize(["/api/services", identityContext])]: [],
|
||||
[unstableSerialize(["/api/bookmarks", identityContext])]: [],
|
||||
[unstableSerialize(["/api/widgets", identityContext])]: [],
|
||||
"/api/hash": false,
|
||||
},
|
||||
identityContext,
|
||||
...(await serverSideTranslations("en")),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function Index({ initialSettings, fallback }) {
|
||||
function Index({ initialSettings, fallback, identityContext }) {
|
||||
const windowFocused = useWindowFocus();
|
||||
const [stale, setStale] = useState(false);
|
||||
const { data: errorsData } = useSWR("/api/validate");
|
||||
|
@ -152,7 +159,7 @@ function Index({ initialSettings, fallback }) {
|
|||
return (
|
||||
<SWRConfig value={{ fallback, fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()) }}>
|
||||
<ErrorBoundary>
|
||||
<Home initialSettings={initialSettings} />
|
||||
<Home initialSettings={initialSettings} identityContext={identityContext} />
|
||||
</ErrorBoundary>
|
||||
</SWRConfig>
|
||||
);
|
||||
|
@ -166,7 +173,7 @@ const headerStyles = {
|
|||
boxedWidgets: "m-5 mb-0 sm:m-9 sm:mb-0 sm:mt-1",
|
||||
};
|
||||
|
||||
function Home({ initialSettings }) {
|
||||
function Home({ initialSettings, identityContext }) {
|
||||
const { i18n } = useTranslation();
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
const { color, setColor } = useContext(ColorContext);
|
||||
|
@ -178,9 +185,9 @@ function Home({ initialSettings }) {
|
|||
setSettings(initialSettings);
|
||||
}, [initialSettings, setSettings]);
|
||||
|
||||
const { data: services } = useSWR("/api/services");
|
||||
const { data: bookmarks } = useSWR("/api/bookmarks");
|
||||
const { data: widgets } = useSWR("/api/widgets");
|
||||
const { data: services } = useSWR(["/api/services", identityContext], fetchWithIdentity);
|
||||
const { data: bookmarks } = useSWR(["/api/bookmarks", identityContext], fetchWithIdentity);
|
||||
const { data: widgets } = useSWR(["/api/widgets", identityContext], fetchWithIdentity);
|
||||
|
||||
const servicesAndBookmarks = [
|
||||
...services.map((sg) => sg.services).flat(),
|
||||
|
@ -452,7 +459,7 @@ function Home({ initialSettings }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default function Wrapper({ initialSettings, fallback }) {
|
||||
export default function Wrapper({ initialSettings, fallback, identityContext }) {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const wrappedStyle = {};
|
||||
let backgroundBlur = false;
|
||||
|
@ -505,7 +512,7 @@ export default function Wrapper({ initialSettings, fallback }) {
|
|||
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
|
||||
)}
|
||||
>
|
||||
<Index initialSettings={initialSettings} fallback={fallback} />
|
||||
<Index initialSettings={initialSettings} fallback={fallback} identityContext={identityContext} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
findGroupByName,
|
||||
} from "utils/config/service-helpers";
|
||||
import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers";
|
||||
import { filterAllowedBookmarks, filterAllowedServices, filterAllowedWidgets } from "utils/identity/identity-helpers";
|
||||
|
||||
/**
|
||||
* Compares services by weight then by name.
|
||||
|
@ -25,7 +26,7 @@ function compareServices(service1, service2) {
|
|||
return service1.name.localeCompare(service2.name);
|
||||
}
|
||||
|
||||
export async function bookmarksResponse() {
|
||||
export async function bookmarksResponse(perms, idGroups) {
|
||||
checkAndCopyConfig("bookmarks.yaml");
|
||||
|
||||
const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml");
|
||||
|
@ -46,13 +47,17 @@ export async function bookmarksResponse() {
|
|||
}
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
const bookmarksArray = bookmarks.map((group) => ({
|
||||
name: Object.keys(group)[0],
|
||||
bookmarks: group[Object.keys(group)[0]].map((entries) => ({
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]][0],
|
||||
const bookmarksArray = filterAllowedBookmarks(
|
||||
perms,
|
||||
idGroups,
|
||||
bookmarks.map((group) => ({
|
||||
name: Object.keys(group)[0],
|
||||
bookmarks: group[Object.keys(group)[0]].map((entries) => ({
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]][0],
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
);
|
||||
|
||||
const sortedGroups = [];
|
||||
const unsortedGroups = [];
|
||||
|
@ -71,11 +76,11 @@ export async function bookmarksResponse() {
|
|||
return [...sortedGroups.filter((g) => g), ...unsortedGroups];
|
||||
}
|
||||
|
||||
export async function widgetsResponse() {
|
||||
export async function widgetsResponse(perms) {
|
||||
let configuredWidgets;
|
||||
|
||||
try {
|
||||
configuredWidgets = cleanWidgetGroups(await widgetsFromConfig());
|
||||
configuredWidgets = filterAllowedWidgets(perms, await cleanWidgetGroups(await widgetsFromConfig()));
|
||||
} catch (e) {
|
||||
console.error("Failed to load widgets, please check widgets.yaml for errors or remove example entries.");
|
||||
if (e) console.error(e);
|
||||
|
@ -96,14 +101,14 @@ function mergeSubgroups(configuredGroups, mergedGroup) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function servicesResponse() {
|
||||
export async function servicesResponse(perms, idGroups) {
|
||||
let discoveredDockerServices;
|
||||
let discoveredKubernetesServices;
|
||||
let configuredServices;
|
||||
let initialSettings;
|
||||
|
||||
try {
|
||||
discoveredDockerServices = cleanServiceGroups(await servicesFromDocker());
|
||||
discoveredDockerServices = filterAllowedServices(perms, idGroups, cleanServiceGroups(await servicesFromDocker()));
|
||||
if (discoveredDockerServices?.length === 0) {
|
||||
console.debug("No containers were found with homepage labels.");
|
||||
}
|
||||
|
@ -114,7 +119,11 @@ export async function servicesResponse() {
|
|||
}
|
||||
|
||||
try {
|
||||
discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes());
|
||||
discoveredKubernetesServices = filterAllowedServices(
|
||||
perms,
|
||||
idGroups,
|
||||
cleanServiceGroups(await servicesFromKubernetes()),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Failed to discover services, please check kubernetes.yaml for errors or remove example entries.");
|
||||
if (e) console.error(e.toString());
|
||||
|
@ -122,7 +131,7 @@ export async function servicesResponse() {
|
|||
}
|
||||
|
||||
try {
|
||||
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
||||
configuredServices = filterAllowedServices(perms, idGroups, cleanServiceGroups(await servicesFromConfig()));
|
||||
} catch (e) {
|
||||
console.error("Failed to load services.yaml, please check for errors");
|
||||
if (e) console.error(e.toString());
|
||||
|
|
|
@ -301,6 +301,12 @@ export async function servicesFromKubernetes() {
|
|||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`]) {
|
||||
constructedService.statusStyle = ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`];
|
||||
}
|
||||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/allowUsers`]) {
|
||||
constructedService.allowUsers = ingress.metadata.annotations[`${ANNOTATION_BASE}/allowUsers`].split(",");
|
||||
}
|
||||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/allowGroups`]) {
|
||||
constructedService.allowGroups = ingress.metadata.annotations[`${ANNOTATION_BASE}/allowGroups`].split(",");
|
||||
}
|
||||
Object.keys(ingress.metadata.annotations).forEach((annotation) => {
|
||||
if (annotation.startsWith(ANNOTATION_WIDGET_BASE)) {
|
||||
shvl.set(
|
||||
|
|
70
src/utils/identity/identity-helpers.js
Normal file
70
src/utils/identity/identity-helpers.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
import ProxyIdentityProvider from "./proxy";
|
||||
import NullIdentityProvider from "./null";
|
||||
|
||||
const IdentityProviders = {
|
||||
null: NullIdentityProvider,
|
||||
proxy: ProxyIdentityProvider,
|
||||
};
|
||||
|
||||
function getProviderByKey(key) {
|
||||
return IdentityProviders[key] || NullIdentityProvider;
|
||||
}
|
||||
|
||||
function identityAllow({ user, groups }, item) {
|
||||
const groupAllow =
|
||||
"allowGroups" in item && item.allowGroups && groups.some((group) => item.allowGroups.includes(group));
|
||||
const userAllow = "allowUsers" in item && item.allowUsers && item.allowUsers.includes(user);
|
||||
const allowAll = !("allowGroups" in item && item.allowGroups) && !("allowUsers" in item && item.allowUsers);
|
||||
|
||||
return userAllow || groupAllow || allowAll;
|
||||
}
|
||||
|
||||
export function checkAllowedGroup(perms, idGroups, groupName) {
|
||||
const testGroup = idGroups.find((group) => group.name === groupName);
|
||||
return testGroup ? identityAllow(perms, testGroup) : true;
|
||||
}
|
||||
|
||||
function filterAllowedItems(perms, idGroups, groups, groupKey) {
|
||||
return groups
|
||||
.filter((group) => checkAllowedGroup(perms, idGroups, group.name))
|
||||
.map((group) => ({
|
||||
name: group.name,
|
||||
[groupKey]: group[groupKey].filter((item) => identityAllow(perms, item)),
|
||||
}))
|
||||
.filter((group) => group[groupKey].length);
|
||||
}
|
||||
|
||||
export function readIdentitySettings({ provider, groups } = {}) {
|
||||
let groupArray = [];
|
||||
if (groups) {
|
||||
if (Array.isArray(groups)) {
|
||||
groupArray = groups.map((group) => ({
|
||||
name: Object.keys(group)[0],
|
||||
allowUsers: group.allowUsers,
|
||||
allowGroups: group.allowGroups,
|
||||
}));
|
||||
} else {
|
||||
groupArray = Object.keys(groups).map((group) => ({
|
||||
name: group,
|
||||
allowUsers: groups[group].allowUsers,
|
||||
allowGroups: groups[group].allowGroups,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
provider: provider ? getProviderByKey(provider.type).create(provider) : NullIdentityProvider.create(),
|
||||
groups: groupArray,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchWithIdentity(key, context) {
|
||||
return getProviderByKey(context.provider).fetch([key, context]);
|
||||
}
|
||||
|
||||
export const filterAllowedServices = (perms, idGroups, services) =>
|
||||
filterAllowedItems(perms, idGroups, services, "services");
|
||||
export const filterAllowedBookmarks = (perms, idGroups, bookmarks) =>
|
||||
filterAllowedItems(perms, idGroups, bookmarks, "bookmarks");
|
||||
export const filterAllowedWidgets = (perms, widgets) =>
|
||||
widgets.filter((widget) => identityAllow(perms, widget.options));
|
21
src/utils/identity/null.js
Normal file
21
src/utils/identity/null.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
const NullIdentity = { user: null, groups: [] };
|
||||
|
||||
function createNullIdentity() {
|
||||
return {
|
||||
getIdentity: () => NullIdentity,
|
||||
getContext: () => ({
|
||||
provider: "null",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchNullIdentity([key]) {
|
||||
return fetch(key).then((res) => res.json());
|
||||
}
|
||||
|
||||
const NullIdentityProvider = {
|
||||
create: createNullIdentity,
|
||||
fetch: fetchNullIdentity,
|
||||
};
|
||||
|
||||
export default NullIdentityProvider;
|
34
src/utils/identity/proxy.js
Normal file
34
src/utils/identity/proxy.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
// 'proxy' identity provider is meant to be used by a reverse proxy that injects permission headers into the origin
|
||||
// request. In this case we are relying on our proxy to authenitcate our users and validate their identity.
|
||||
function getProxyPermissions(userHeader, groupHeader, groupSeparator, request) {
|
||||
const user =
|
||||
userHeader && request.headers[userHeader.toLowerCase()] ? request.headers[userHeader.toLowerCase()] : null;
|
||||
const groupsString =
|
||||
groupHeader && request.headers[groupHeader.toLowerCase()] ? request.headers[groupHeader.toLowerCase()] : "";
|
||||
|
||||
return { user, groups: groupsString ? groupsString.split(groupSeparator ?? "|").map((v) => v.trim()) : [] };
|
||||
}
|
||||
|
||||
function createProxyIdentity({ groupHeader, groupSeparator, userHeader }) {
|
||||
return {
|
||||
getContext: (request) => ({
|
||||
provider: "proxy",
|
||||
...(userHeader &&
|
||||
request.headers[userHeader] && { [userHeader.toLowerCase()]: request.headers[userHeader.toLowerCase()] }),
|
||||
...(groupHeader &&
|
||||
request.headers[groupHeader] && { [groupHeader.toLowerCase()]: request.headers[groupHeader.toLowerCase()] }),
|
||||
}),
|
||||
getIdentity: (request) => getProxyPermissions(userHeader, groupHeader, groupSeparator, request),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchProxyIdentity([key, context]) {
|
||||
return fetch(key, { headers: context.headers }).then((res) => res.json());
|
||||
}
|
||||
|
||||
const ProxyIdentityProvider = {
|
||||
create: createProxyIdentity,
|
||||
fetch: fetchProxyIdentity,
|
||||
};
|
||||
|
||||
export default ProxyIdentityProvider;
|
Loading…
Add table
Add a link
Reference in a new issue