Skip to content

Commit

Permalink
homepage-plus
Browse files Browse the repository at this point in the history
  • Loading branch information
DI0IK committed Dec 12, 2024
1 parent 01252c6 commit c027ba3
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 56 deletions.
27 changes: 12 additions & 15 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ name: Docker
# documentation.

on:
schedule:
- cron: '20 0 * * *'
# schedule:
# - cron: '20 0 * * *'
push:
branches:
- main
Expand All @@ -24,6 +24,7 @@ on:
- 'docs/**'
- 'mkdocs.yml'
merge_group:
workflow_dispatch:

env:
# github.repository as <account>/<repo>
Expand All @@ -45,20 +46,16 @@ jobs:
python-version: 3.x
-
name: Check files
uses: pre-commit/[email protected].1
uses: pre-commit/[email protected].0

build:
name: Docker Build & Push
if: github.repository == 'gethomepage/homepage'
runs-on: self-hosted
runs-on: ubuntu-latest
needs:
- pre-commit
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write

steps:
- name: Checkout repository
Expand All @@ -75,13 +72,13 @@ jobs:

# This step is being disabled because the runner is on a self-hosted machine
# where the cache will stick between runs.
# - name: Cache Docker layers
# uses: actions/cache@v3
# with:
# path: /tmp/.buildx-cache
# key: ${{ runner.os }}-buildx-${{ github.sha }}
# restore-keys: |
# ${{ runner.os }}-buildx-
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
Expand Down
39 changes: 39 additions & 0 deletions docs/configs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,42 @@ or per service widget (`services.yaml`) with:
```

If either value is set to true, the error message will be hidden.

## Identity Based Visibiltiy

Basic user identity integration is implemeted via an `identity` section. An identity provider can be configured using the `provider` section with the given type. Currently the only provider supported is `proxy`, where the users identification and group membership are passed via HTTP Request headers (in plaintext). The expectation is that the application will be accessed only via an authenticating proxy (i.e traefik or nginx).

The group and user headers are both configurable like so:

```yaml
identity:
provider:
type: proxy
groupHeader: "X-group-header"
userHeader: "X-user-header"
```

Identity based visibility can be configured on the service, bookmark, and widget level using the `allowUsers` and `allowGroups` list. The default is to allow all users and groups.

```yaml
- Example Servie:
allowGroups:
- Group1
- Group2
- Group3
allowUsers:
- User1
- User2
- User3
```

Identity visibility for groups can be set in the `groups` under `identity`. In general the `groups` tag follows the format of the `layout` section. For example:

```yaml
identity:
groups:
- My Service Group:
allowGroups: ["Group1", "Group2"]
- My Other Group:
allowGroups: ["Group1"]
```
6 changes: 3 additions & 3 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ site_name: Homepage
site_url: https://gethomepage.dev/

# Repository
repo_name: gethomepage/homepage
repo_url: https://github.com/gethomepage/homepage
edit_uri: https://github.com/gethomepage/homepage/tree/main/docs/
repo_name: di0ik/homepage-plus
repo_url: https://github.com/di0ik/homepage-plus
edit_uri: https://github.com/di0ik/homepage-plus/tree/main/docs/

nav:
- "Home":
Expand Down
2 changes: 1 addition & 1 deletion src/components/quicklaunch.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
17 changes: 17 additions & 0 deletions src/pages/api/auth.js
Original file line number Diff line number Diff line change
@@ -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");
}
}
5 changes: 4 additions & 1 deletion src/pages/api/bookmarks.js
Original file line number Diff line number Diff line change
@@ -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));
}
5 changes: 4 additions & 1 deletion src/pages/api/services/index.js
Original file line number Diff line number Diff line change
@@ -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));
}
5 changes: 4 additions & 1 deletion src/pages/api/widgets/index.js
Original file line number Diff line number Diff line change
@@ -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)));
}
47 changes: 27 additions & 20 deletions src/pages/index.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -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,
Expand All @@ -41,48 +43,53 @@ 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")),
},
};
} catch (e) {
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");
Expand Down Expand Up @@ -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>
);
Expand All @@ -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);
Expand All @@ -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(),
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>
Expand Down
Loading

0 comments on commit c027ba3

Please sign in to comment.