mirror of
https://github.com/tale/headplane.git
synced 2026-08-24 11:36:31 +00:00
105 lines
2.8 KiB
TypeScript
105 lines
2.8 KiB
TypeScript
import { ActionFunctionArgs, data, redirect } from 'react-router';
|
|
import { LoadContext } from '~/server';
|
|
import ResponseError from '~/server/headscale/api/error';
|
|
import { Key } from '~/types';
|
|
import log from '~/utils/log';
|
|
|
|
export async function loginAction({
|
|
request,
|
|
context,
|
|
}: ActionFunctionArgs<LoadContext>) {
|
|
const formData = await request.formData();
|
|
const apiKey = formData.has('api_key')
|
|
? String(formData.get('api_key'))
|
|
: undefined;
|
|
|
|
if (apiKey === undefined) {
|
|
log.warn('auth', 'Request made without API key');
|
|
log.warn(
|
|
'auth',
|
|
'If this is unexpected, ensure your reverse proxy (if applicable) is configured correctly',
|
|
);
|
|
throw data('Missing `api_key`', { status: 400 });
|
|
}
|
|
|
|
if (apiKey.length === 0) {
|
|
log.warn('auth', 'Request made with empty API key');
|
|
log.warn(
|
|
'auth',
|
|
'If this is unexpected, ensure your reverse proxy (if applicable) is configured correctly',
|
|
);
|
|
throw data('Received an empty `api_key`', { status: 400 });
|
|
}
|
|
|
|
try {
|
|
const { apiKeys } = await context.client.get<{ apiKeys: Key[] }>(
|
|
'v1/apikey',
|
|
apiKey,
|
|
);
|
|
|
|
// We don't need to check for 0 API keys because this request cannot
|
|
// be authenticated correctly without an API key
|
|
const lookup = apiKeys.find((key) => apiKey.startsWith(key.prefix));
|
|
if (!lookup) {
|
|
return {
|
|
success: false,
|
|
message: 'API key was not found in the Headscale database',
|
|
};
|
|
}
|
|
|
|
if (lookup.expiration === null || lookup.expiration === undefined) {
|
|
log.error('auth', 'Got an API key without an expiration');
|
|
throw data('API key is malformed', { status: 500 });
|
|
}
|
|
|
|
const expiry = new Date(lookup.expiration);
|
|
if (expiry.getTime() < Date.now()) {
|
|
return {
|
|
success: false,
|
|
message: 'API key has expired',
|
|
};
|
|
}
|
|
|
|
const expiresDays = Math.round(
|
|
(expiry.getTime() - Date.now()) / 1000 / 60 / 60 / 24,
|
|
);
|
|
|
|
return redirect('/machines', {
|
|
headers: {
|
|
'Set-Cookie': await context.sessions.createSession(
|
|
{
|
|
api_key: apiKey,
|
|
user: {
|
|
subject: 'unknown-non-oauth',
|
|
name: `${lookup.prefix}...`,
|
|
email: `expires@${expiresDays.toString()}-days`,
|
|
},
|
|
},
|
|
expiry.getTime() - Date.now(),
|
|
),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ResponseError) {
|
|
// TODO: What in gods name is wrong with the headscale API?
|
|
if (
|
|
error.status === 401 ||
|
|
error.status === 403 ||
|
|
(error.status === 500 && error.response.trim() === 'Unauthorized')
|
|
) {
|
|
return {
|
|
success: false,
|
|
message: 'API key is invalid (it may be incorrect or expired)',
|
|
};
|
|
}
|
|
}
|
|
|
|
log.error('auth', 'Error while validating API key: %s', error);
|
|
log.debug('auth', 'Error details: %o', error);
|
|
return {
|
|
success: false,
|
|
message: 'Error while validating API key (see logs for details)',
|
|
};
|
|
}
|
|
}
|