mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
feat: support gravatar profile pictures for oidc
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
- The nix overlay build is fixed for the SSH module (via [#282](https://github.com/tale/headplane/pull/282))
|
||||
- Switch our build processes to use TypeScript Go and Rolldown Vite for better build and type-check performance.
|
||||
- Cookies are now encrypted JWTs, preserving API key secrets (*GHSA-wrqq-v7qw-r5w7*)
|
||||
- OIDC profile pictures are now available from Gravatar by setting `oidc.profile_picture_source` to `gravatar` (closes [#232](https://github.com/tale/headplane/issues/232)).
|
||||
|
||||
### 0.6.0 (May 25, 2025)
|
||||
- Headplane 0.6.0 now requires **Headscale 0.26.0** or newer.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { count } from 'drizzle-orm';
|
||||
import { createCookie, type LoaderFunctionArgs, redirect } from 'react-router';
|
||||
import { ulid } from 'ulidx';
|
||||
import type { LoadContext } from '~/server';
|
||||
import { HeadplaneConfig } from '~/server/config/schema';
|
||||
import { users } from '~/server/db/schema';
|
||||
import { Roles } from '~/server/web/roles';
|
||||
import { finishAuthFlow, formatError } from '~/utils/oidc';
|
||||
import { FlowUser, finishAuthFlow, formatError } from '~/utils/oidc';
|
||||
import { send } from '~/utils/res';
|
||||
|
||||
interface OidcFlowSession {
|
||||
@@ -60,7 +62,14 @@ export async function loader({
|
||||
};
|
||||
|
||||
try {
|
||||
const user = await finishAuthFlow(context.oidc, flowOptions);
|
||||
let user = await finishAuthFlow(context.oidc, flowOptions);
|
||||
user = {
|
||||
...user,
|
||||
picture: setOidcPictureForSource(
|
||||
user,
|
||||
context.config.oidc?.profile_picture_source ?? 'oidc',
|
||||
),
|
||||
};
|
||||
|
||||
const [{ count: userCount }] = await context.db
|
||||
.select({ count: count() })
|
||||
@@ -96,3 +105,26 @@ export async function loader({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type PictureSource = NonNullable<
|
||||
HeadplaneConfig['oidc']
|
||||
>['profile_picture_source'];
|
||||
|
||||
function setOidcPictureForSource(user: FlowUser, source: PictureSource) {
|
||||
// Already set by default in the callback, so we can just return it
|
||||
if (source === 'oidc') {
|
||||
return user.picture;
|
||||
}
|
||||
|
||||
if (source === 'gravatar') {
|
||||
if (!user.email) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const emailHash = user.email.trim().toLowerCase();
|
||||
const hash = createHash('sha256').update(emailHash).digest('hex');
|
||||
return `https://www.gravatar.com/avatar/${hash}?s=200&d=identicon&r=x`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ const oidcConfig = type({
|
||||
disable_api_key_login: stringToBool,
|
||||
headscale_api_key: 'string?',
|
||||
headscale_api_key_path: 'string?',
|
||||
profile_picture_source: '"oidc" | "gravatar" = "oidc"',
|
||||
strict_validation: stringToBool.default(true),
|
||||
})
|
||||
.narrow((obj: Record<string, unknown>, ctx: any) => {
|
||||
@@ -94,6 +95,7 @@ const partialOidcConfig = type({
|
||||
disable_api_key_login: stringToBool.optional(),
|
||||
headscale_api_key: 'string?',
|
||||
headscale_api_key_path: 'string?',
|
||||
profile_picture_source: '("oidc" | "gravatar")?',
|
||||
strict_validation: stringToBool.default(true),
|
||||
});
|
||||
|
||||
|
||||
+9
-1
@@ -69,10 +69,18 @@ interface FlowOptions {
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
export interface FlowUser {
|
||||
subject: string;
|
||||
name: string;
|
||||
email: string | undefined;
|
||||
username: string | undefined;
|
||||
picture: string | undefined;
|
||||
}
|
||||
|
||||
export async function finishAuthFlow(
|
||||
config: Configuration,
|
||||
options: FlowOptions,
|
||||
) {
|
||||
): Promise<FlowUser> {
|
||||
const tokens = await client.authorizationCodeGrant(
|
||||
config,
|
||||
new URL(options.redirect_uri),
|
||||
|
||||
@@ -164,3 +164,8 @@ oidc:
|
||||
# Stores the users and their permissions for Headplane
|
||||
# This is a path to a JSON file, default is specified below.
|
||||
user_storage_file: "/var/lib/headplane/users.json"
|
||||
|
||||
# By default profile pictures are pulled from the OIDC provider when
|
||||
# we go to fetch the userinfo endpoint. Optionally, this can be set to
|
||||
# "oidc" or "gravatar" as of 0.6.1.
|
||||
# profile_picture_source: "gravatar"
|
||||
|
||||
Reference in New Issue
Block a user