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
|
# how often to check for new/updated queries in min
|
||||||
QUERY_TIME_TYPE=m #valid options are m, h
|
QUERY_TIME_TYPE=m #valid options are m, h
|
||||||
QUERY_CHECK=1
|
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 { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import {
|
import {
|
||||||
admin as adminPlugin,
|
admin as adminPlugin,
|
||||||
|
genericOAuth,
|
||||||
// apiKey,
|
// apiKey,
|
||||||
// createAuthMiddleware,
|
// createAuthMiddleware,
|
||||||
//customSession,
|
//customSession,
|
||||||
@@ -16,6 +17,46 @@ import { ac, admin, systemAdmin, user } from "./auth.permissions.js";
|
|||||||
import { allowedOrigins } from "./cors.utils.js";
|
import { allowedOrigins } from "./cors.utils.js";
|
||||||
import { sendEmail } from "./sendEmail.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 = {
|
export const schema = {
|
||||||
user: rawSchema.user,
|
user: rawSchema.user,
|
||||||
session: rawSchema.session,
|
session: rawSchema.session,
|
||||||
@@ -25,9 +66,73 @@ export const schema = {
|
|||||||
apiKey: rawSchema.apikey, // 🔑 rename to apiKey
|
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({
|
export const auth = betterAuth({
|
||||||
appName: "lst",
|
appName: "lst",
|
||||||
baseURL: process.env.URL,
|
baseURL: `${process.env.URL}/lst/api/auth`,
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
provider: "pg",
|
provider: "pg",
|
||||||
schema,
|
schema,
|
||||||
@@ -42,6 +147,14 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
account: {
|
||||||
|
encryptOAuthTokens: true,
|
||||||
|
updateAccountOnSignIn: true,
|
||||||
|
accountLinking: {
|
||||||
|
enabled: true,
|
||||||
|
trustedProviders: ["voidauth"],
|
||||||
|
},
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
jwt({ jwt: { expirationTime: "1h" } }),
|
jwt({ jwt: { expirationTime: "1h" } }),
|
||||||
//apiKey(),
|
//apiKey(),
|
||||||
@@ -63,6 +176,7 @@ export const auth = betterAuth({
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
...oauthPlugins,
|
||||||
|
|
||||||
// customSession(async ({ user, session }) => {
|
// customSession(async ({ user, session }) => {
|
||||||
// const roles = await db
|
// const roles = await db
|
||||||
@@ -121,7 +235,7 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
cookie: {
|
cookie: {
|
||||||
path: "/lst/app",
|
path: "/lst",
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
secure: false,
|
secure: false,
|
||||||
httpOnly: true,
|
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 { createAuthClient } from "better-auth/react";
|
||||||
import { ac, admin, systemAdmin, user } from "./auth-permissions";
|
import { ac, admin, systemAdmin, user } from "./auth-permissions";
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ export const authClient = createAuthClient({
|
|||||||
systemAdmin,
|
systemAdmin,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
genericOAuthClient(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Link, useNavigate } from "@tanstack/react-router";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { Cat, LogIn } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -9,13 +10,23 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { useAppForm } from "@/lib/formSutff";
|
import { useAppForm } from "@/lib/formSutff";
|
||||||
|
import { Button } from "../../../components/ui/button";
|
||||||
import socket from "../../../lib/socket.io";
|
import socket from "../../../lib/socket.io";
|
||||||
|
|
||||||
export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
||||||
const loginEmail = localStorage.getItem("loginEmail") || "";
|
const loginEmail = localStorage.getItem("loginEmail") || "";
|
||||||
const rememberMe = localStorage.getItem("rememberMe") === "true";
|
const rememberMe = localStorage.getItem("rememberMe") === "true";
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const oauthLogin = async () => {
|
||||||
|
await authClient.signIn.oauth2({
|
||||||
|
providerId: "voidauth",
|
||||||
|
callbackURL: "/lst/app",
|
||||||
|
errorCallbackURL: "/lst/app/login",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const form = useAppForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: loginEmail,
|
email: loginEmail,
|
||||||
@@ -26,7 +37,7 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
|||||||
// set remember me incase we want it later
|
// set remember me incase we want it later
|
||||||
if (value.rememberMe) {
|
if (value.rememberMe) {
|
||||||
localStorage.setItem("rememberMe", value.rememberMe.toString());
|
localStorage.setItem("rememberMe", value.rememberMe.toString());
|
||||||
localStorage.setItem("loginEmail", value.email);
|
localStorage.setItem("loginEmail", value.email.toLocaleLowerCase());
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem("rememberMe");
|
localStorage.removeItem("rememberMe");
|
||||||
localStorage.removeItem("loginEmail");
|
localStorage.removeItem("loginEmail");
|
||||||
@@ -62,7 +73,17 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
|||||||
<div>
|
<div>
|
||||||
<Card className="p-3 w-96">
|
<Card className="p-3 w-96">
|
||||||
<CardHeader>
|
<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>
|
<CardDescription>
|
||||||
Enter your username and password below
|
Enter your username and password below
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@@ -76,12 +97,19 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
|||||||
>
|
>
|
||||||
<form.AppField name="email">
|
<form.AppField name="email">
|
||||||
{(field) => (
|
{(field) => (
|
||||||
<field.InputField label="Email" inputType="email" required />
|
<field.InputField
|
||||||
|
label="Email"
|
||||||
|
inputType="email"
|
||||||
|
required={rememberMe}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</form.AppField>
|
</form.AppField>
|
||||||
<form.AppField name="password">
|
<form.AppField name="password">
|
||||||
{(field) => (
|
{(field) => (
|
||||||
<field.InputPasswordField label="Password" required={true} />
|
<field.InputPasswordField
|
||||||
|
label="Password"
|
||||||
|
required={rememberMe}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</form.AppField>
|
</form.AppField>
|
||||||
|
|
||||||
@@ -98,7 +126,7 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end mt-2">
|
<div className="flex justify-between mt-2 ">
|
||||||
<form.AppForm>
|
<form.AppForm>
|
||||||
<form.SubmitButton>Login</form.SubmitButton>
|
<form.SubmitButton>Login</form.SubmitButton>
|
||||||
</form.AppForm>
|
</form.AppForm>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ $password = $ADM_PASSWORD
|
|||||||
$securePass = ConvertTo-SecureString $password -AsPlainText -Force
|
$securePass = ConvertTo-SecureString $password -AsPlainText -Force
|
||||||
$credentials = New-Object System.Management.Automation.PSCredential($username, $securePass)
|
$credentials = New-Object System.Management.Automation.PSCredential($username, $securePass)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Update-Server {
|
function Update-Server {
|
||||||
param (
|
param (
|
||||||
[string]$Destination,
|
[string]$Destination,
|
||||||
@@ -84,6 +86,75 @@ function Update-Server {
|
|||||||
$AppUpdate = {
|
$AppUpdate = {
|
||||||
param ($Server, $Token, $Destination, $BuildFile)
|
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
|
#convert everything to the server fun
|
||||||
$LocalPath = $Destination -replace '\$', ':'
|
$LocalPath = $Destination -replace '\$', ':'
|
||||||
$BuildFileLoc = "$LocalPath\$BuildFile"
|
$BuildFileLoc = "$LocalPath\$BuildFile"
|
||||||
@@ -122,6 +193,10 @@ function Update-Server {
|
|||||||
npm install --omit=dev
|
npm install --omit=dev
|
||||||
Start-Sleep -Seconds 3
|
Start-Sleep -Seconds 3
|
||||||
Write-Host "Install/update completed."
|
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
|
# do the migrations
|
||||||
# Push-Location $LocalPath
|
# Push-Location $LocalPath
|
||||||
Write-Host "Running migrations"
|
Write-Host "Running migrations"
|
||||||
|
|||||||
Reference in New Issue
Block a user