feat(oidc): added in so we could use an oidc to login as well :D
This commit is contained in:
@@ -50,3 +50,11 @@ GP_PASSWORD=
|
||||
# how often to check for new/updated queries in min
|
||||
QUERY_TIME_TYPE=m #valid options are m, h
|
||||
QUERY_CHECK=1
|
||||
|
||||
|
||||
# Oauth setup
|
||||
PROVIDER=""
|
||||
CLIENT_ID=""
|
||||
CLIENT_SECRET=""
|
||||
CLIENT_SCOPES="openid profile email groups"
|
||||
DISCOVERY_URL=""
|
||||
|
||||
@@ -2,6 +2,7 @@ import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import {
|
||||
admin as adminPlugin,
|
||||
genericOAuth,
|
||||
// apiKey,
|
||||
// createAuthMiddleware,
|
||||
//customSession,
|
||||
@@ -16,6 +17,46 @@ import { ac, admin, systemAdmin, user } from "./auth.permissions.js";
|
||||
import { allowedOrigins } from "./cors.utils.js";
|
||||
import { sendEmail } from "./sendEmail.utils.js";
|
||||
|
||||
function decodeJwtPayload<T = Record<string, unknown>>(jwt: string): T {
|
||||
const parts = jwt.split(".");
|
||||
if (parts.length < 2) {
|
||||
throw new Error("Invalid JWT");
|
||||
}
|
||||
|
||||
const payload = parts[1]?.replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
const padded = payload?.padEnd(
|
||||
payload.length + ((4 - (payload.length % 4)) % 4),
|
||||
"=",
|
||||
);
|
||||
|
||||
const json = Buffer.from(padded ?? "", "base64").toString("utf8");
|
||||
return JSON.parse(json) as T;
|
||||
}
|
||||
|
||||
function normalizeGroups(groups?: unknown): string[] {
|
||||
if (!Array.isArray(groups)) return [];
|
||||
|
||||
return groups
|
||||
.filter((g): g is string => typeof g === "string")
|
||||
.map((g) => g.trim().toLowerCase())
|
||||
.filter((g) => g.length > 0);
|
||||
}
|
||||
|
||||
type VoidAuthClaims = {
|
||||
sub: string;
|
||||
name?: string;
|
||||
preferred_username?: string;
|
||||
email?: string;
|
||||
email_verified?: boolean;
|
||||
groups?: string[];
|
||||
picture?: string;
|
||||
iss?: string;
|
||||
aud?: string;
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
};
|
||||
|
||||
export const schema = {
|
||||
user: rawSchema.user,
|
||||
session: rawSchema.session,
|
||||
@@ -25,9 +66,73 @@ export const schema = {
|
||||
apiKey: rawSchema.apikey, // 🔑 rename to apiKey
|
||||
};
|
||||
|
||||
const hasOAuth =
|
||||
Boolean(process.env.PROVIDER) &&
|
||||
Boolean(process.env.CLIENT_ID) &&
|
||||
Boolean(process.env.CLIENT_SECRET) &&
|
||||
Boolean(process.env.DISCOVERY_URL);
|
||||
|
||||
if (!hasOAuth) {
|
||||
console.warn("Missing oauth data.");
|
||||
}
|
||||
|
||||
const oauthPlugins = hasOAuth
|
||||
? [
|
||||
genericOAuth({
|
||||
config: [
|
||||
{
|
||||
providerId: process.env.PROVIDER!,
|
||||
clientId: process.env.CLIENT_ID!,
|
||||
clientSecret: process.env.CLIENT_SECRET!,
|
||||
discoveryUrl: process.env.DISCOVERY_URL!,
|
||||
scopes: (process.env.CLIENT_SCOPES ?? "")
|
||||
.split(/[,\s]+/)
|
||||
.filter(Boolean),
|
||||
pkce: true,
|
||||
requireIssuerValidation: true,
|
||||
redirectURI: `${process.env.URL}/lst/api/auth/oauth2/callback/${process.env.PROVIDER!}`,
|
||||
getUserInfo: async (tokens) => {
|
||||
if (!tokens.idToken) {
|
||||
throw new Error("VoidAuth did not return an idToken");
|
||||
}
|
||||
|
||||
const claims = decodeJwtPayload<VoidAuthClaims>(tokens.idToken);
|
||||
const groups = normalizeGroups(claims.groups);
|
||||
|
||||
return {
|
||||
id: claims.sub,
|
||||
email: claims.email ?? "",
|
||||
name:
|
||||
claims.name ??
|
||||
claims.preferred_username ??
|
||||
claims.email ??
|
||||
"Unknown User",
|
||||
image: claims.picture ?? null,
|
||||
emailVerified: Boolean(claims.email_verified),
|
||||
groups,
|
||||
username: claims.preferred_username ?? null,
|
||||
} as any;
|
||||
},
|
||||
|
||||
mapProfileToUser: async (profile) => {
|
||||
return {
|
||||
name: profile.name,
|
||||
role: profile.groups?.includes("lst_admins")
|
||||
? "systemAdmin"
|
||||
: profile.groups?.includes("admins")
|
||||
? "admin"
|
||||
: "user",
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
|
||||
export const auth = betterAuth({
|
||||
appName: "lst",
|
||||
baseURL: process.env.URL,
|
||||
baseURL: `${process.env.URL}/lst/api/auth`,
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema,
|
||||
@@ -42,6 +147,14 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
},
|
||||
account: {
|
||||
encryptOAuthTokens: true,
|
||||
updateAccountOnSignIn: true,
|
||||
accountLinking: {
|
||||
enabled: true,
|
||||
trustedProviders: ["voidauth"],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
jwt({ jwt: { expirationTime: "1h" } }),
|
||||
//apiKey(),
|
||||
@@ -63,6 +176,7 @@ export const auth = betterAuth({
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
...oauthPlugins,
|
||||
|
||||
// customSession(async ({ user, session }) => {
|
||||
// const roles = await db
|
||||
@@ -121,7 +235,7 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
cookie: {
|
||||
path: "/lst/app",
|
||||
path: "/lst",
|
||||
sameSite: "lax",
|
||||
secure: false,
|
||||
httpOnly: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { adminClient } from "better-auth/client/plugins";
|
||||
import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
import { ac, admin, systemAdmin, user } from "./auth-permissions";
|
||||
|
||||
@@ -13,6 +13,7 @@ export const authClient = createAuthClient({
|
||||
systemAdmin,
|
||||
},
|
||||
}),
|
||||
genericOAuthClient(),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { Cat, LogIn } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Card,
|
||||
@@ -9,13 +10,23 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { useAppForm } from "@/lib/formSutff";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import socket from "../../../lib/socket.io";
|
||||
|
||||
export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
||||
const loginEmail = localStorage.getItem("loginEmail") || "";
|
||||
const rememberMe = localStorage.getItem("rememberMe") === "true";
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const oauthLogin = async () => {
|
||||
await authClient.signIn.oauth2({
|
||||
providerId: "voidauth",
|
||||
callbackURL: "/lst/app",
|
||||
errorCallbackURL: "/lst/app/login",
|
||||
});
|
||||
};
|
||||
|
||||
const form = useAppForm({
|
||||
defaultValues: {
|
||||
email: loginEmail,
|
||||
@@ -26,7 +37,7 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
||||
// set remember me incase we want it later
|
||||
if (value.rememberMe) {
|
||||
localStorage.setItem("rememberMe", value.rememberMe.toString());
|
||||
localStorage.setItem("loginEmail", value.email);
|
||||
localStorage.setItem("loginEmail", value.email.toLocaleLowerCase());
|
||||
} else {
|
||||
localStorage.removeItem("rememberMe");
|
||||
localStorage.removeItem("loginEmail");
|
||||
@@ -62,7 +73,17 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
||||
<div>
|
||||
<Card className="p-3 w-96">
|
||||
<CardHeader>
|
||||
<CardTitle>Login to your account</CardTitle>
|
||||
<CardTitle>
|
||||
<div className="flex flex-row justify-center">
|
||||
<Button onClick={oauthLogin} size="lg" variant="ghost">
|
||||
<Cat />
|
||||
</Button>
|
||||
<span className="mt-2">Login to your account</span>{" "}
|
||||
<Button size="lg" variant="ghost">
|
||||
<Cat />
|
||||
</Button>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your username and password below
|
||||
</CardDescription>
|
||||
@@ -76,12 +97,19 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
||||
>
|
||||
<form.AppField name="email">
|
||||
{(field) => (
|
||||
<field.InputField label="Email" inputType="email" required />
|
||||
<field.InputField
|
||||
label="Email"
|
||||
inputType="email"
|
||||
required={rememberMe}
|
||||
/>
|
||||
)}
|
||||
</form.AppField>
|
||||
<form.AppField name="password">
|
||||
{(field) => (
|
||||
<field.InputPasswordField label="Password" required={true} />
|
||||
<field.InputPasswordField
|
||||
label="Password"
|
||||
required={rememberMe}
|
||||
/>
|
||||
)}
|
||||
</form.AppField>
|
||||
|
||||
@@ -98,7 +126,7 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-2">
|
||||
<div className="flex justify-between mt-2 ">
|
||||
<form.AppForm>
|
||||
<form.SubmitButton>Login</form.SubmitButton>
|
||||
</form.AppForm>
|
||||
|
||||
@@ -23,6 +23,8 @@ $password = $ADM_PASSWORD
|
||||
$securePass = ConvertTo-SecureString $password -AsPlainText -Force
|
||||
$credentials = New-Object System.Management.Automation.PSCredential($username, $securePass)
|
||||
|
||||
|
||||
|
||||
function Update-Server {
|
||||
param (
|
||||
[string]$Destination,
|
||||
@@ -84,6 +86,75 @@ function Update-Server {
|
||||
$AppUpdate = {
|
||||
param ($Server, $Token, $Destination, $BuildFile)
|
||||
|
||||
function Fix-Env {
|
||||
$envFile = ".env"
|
||||
|
||||
if (-not (Test-Path $envFile)) {
|
||||
Write-Host ".env not found, creating..."
|
||||
New-Item -ItemType File -Path $envFile | Out-Null
|
||||
}
|
||||
|
||||
$envContent = Get-Content $envFile -Raw
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($envContent)) {
|
||||
$envContent = ""
|
||||
}
|
||||
|
||||
$envVarsToAdd = @{
|
||||
"PROVIDER" = "voidauth"
|
||||
"CLIENT_ID" = "crIVcUilFWIS6ME3"
|
||||
"CLIENT_SECRET" = "zsJeyjMN2yDDqfyzSsh96OtlA2714F5d"
|
||||
"CLIENT_SCOPES" = "openid profile email groups"
|
||||
"DISCOVERY_URL" = "https://auth.tuffraid.net/oidc/.well-known/openid-configuration"
|
||||
}
|
||||
|
||||
$linesToAppend = @()
|
||||
|
||||
foreach ($key in $envVarsToAdd.Keys) {
|
||||
$escapedKey = [regex]::Escape($key)
|
||||
|
||||
if ($envContent -notmatch "(?m)^$escapedKey=") {
|
||||
$linesToAppend += "$key=$($envVarsToAdd[$key])"
|
||||
Write-Host "Adding missing env: $key"
|
||||
}
|
||||
else {
|
||||
Write-Host "Env exists, skipping add: $key"
|
||||
}
|
||||
}
|
||||
|
||||
###### to replace the values of mistakens or something fun where we need to fix across all 17 servers put it here.
|
||||
$envVarsToReplace = @{
|
||||
# "PORT" = "3000"
|
||||
#"URL" = "https://$($Token)prod.alpla.net/lst"
|
||||
}
|
||||
|
||||
foreach ($key in $envVarsToReplace.Keys) {
|
||||
$value = $envVarsToReplace[$key]
|
||||
$escapedKey = [regex]::Escape($key)
|
||||
|
||||
if ($envContent -match "(?m)^$escapedKey=") {
|
||||
Write-Host "Replacing env: $key -> $value"
|
||||
$envContent = $envContent -replace "(?m)^$escapedKey=.*", "$key=$value"
|
||||
}
|
||||
else {
|
||||
Write-Host "Env not found for replace, skipping: $key"
|
||||
}
|
||||
}
|
||||
|
||||
if ($linesToAppend.Count -gt 0) {
|
||||
if ($envContent.Length -gt 0 -and -not $envContent.EndsWith("`n")) {
|
||||
$envContent += "`r`n"
|
||||
}
|
||||
|
||||
$envContent += "`r`n# ---- VoidAuth Config ----`r`n"
|
||||
$envContent += ($linesToAppend -join "`r`n")
|
||||
Write-Host "Appending new env vars."
|
||||
}
|
||||
|
||||
Set-Content -Path $envFile -Value $envContent
|
||||
Write-Host "Env update completed."
|
||||
}
|
||||
|
||||
#convert everything to the server fun
|
||||
$LocalPath = $Destination -replace '\$', ':'
|
||||
$BuildFileLoc = "$LocalPath\$BuildFile"
|
||||
@@ -121,8 +192,12 @@ function Update-Server {
|
||||
Write-Host "Running install/update in: $LocalPath"
|
||||
npm install --omit=dev
|
||||
Start-Sleep -Seconds 3
|
||||
Write-Host "Install/update completed."
|
||||
# do the migrations
|
||||
Write-Host "Install/update completed."
|
||||
|
||||
# update the env to include the new and missing things silly people and wanting things fixed :(
|
||||
Fix-Env #-Path $LocalPath
|
||||
|
||||
# do the migrations
|
||||
# Push-Location $LocalPath
|
||||
Write-Host "Running migrations"
|
||||
npm run dev:db:migrate
|
||||
|
||||
Reference in New Issue
Block a user