Automatic login (#1900)
Browse files* Automatic login
* update debug message with correct port
* fix passing forward token
- .env +2 -0
- src/hooks.server.ts +16 -2
- src/lib/server/adminToken.ts +5 -3
- src/lib/server/auth.ts +31 -1
- src/lib/server/endpoints/openai/endpointOai.ts +19 -2
- src/routes/login/+server.ts +2 -26
.env
CHANGED
|
@@ -10,6 +10,8 @@ OPENAI_BASE_URL=https://router.huggingface.co/v1
|
|
| 10 |
OPENAI_API_KEY=#your provider API key (works for HF router, OpenAI, LM Studio, etc.).
|
| 11 |
# When set to true, user token will be used for inference calls
|
| 12 |
USE_USER_TOKEN=false
|
|
|
|
|
|
|
| 13 |
|
| 14 |
### MongoDB ###
|
| 15 |
MONGODB_URL=#your mongodb URL here, use chat-ui-db image if you don't want to set this
|
|
|
|
| 10 |
OPENAI_API_KEY=#your provider API key (works for HF router, OpenAI, LM Studio, etc.).
|
| 11 |
# When set to true, user token will be used for inference calls
|
| 12 |
USE_USER_TOKEN=false
|
| 13 |
+
# Automatically redirect to oauth login page if user is not logged in, when set to "true"
|
| 14 |
+
AUTOMATIC_LOGIN=false
|
| 15 |
|
| 16 |
### MongoDB ###
|
| 17 |
MONGODB_URL=#your mongodb URL here, use chat-ui-db image if you don't want to set this
|
src/hooks.server.ts
CHANGED
|
@@ -2,7 +2,12 @@ import { config, ready } from "$lib/server/config";
|
|
| 2 |
import type { Handle, HandleServerError, ServerInit, HandleFetch } from "@sveltejs/kit";
|
| 3 |
import { collections } from "$lib/server/database";
|
| 4 |
import { base } from "$app/paths";
|
| 5 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
import { ERROR_MESSAGES } from "$lib/stores/errors";
|
| 7 |
import { addWeeks } from "date-fns";
|
| 8 |
import { checkAndRunMigrations } from "$lib/migrations/migrations";
|
|
@@ -126,8 +131,17 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 126 |
{ type: "svelte", value: event.cookies }
|
| 127 |
);
|
| 128 |
|
| 129 |
-
event.locals.user = auth.user || undefined;
|
| 130 |
event.locals.sessionId = auth.sessionId;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
event.locals.token = auth.token;
|
| 132 |
|
| 133 |
event.locals.isAdmin =
|
|
|
|
| 2 |
import type { Handle, HandleServerError, ServerInit, HandleFetch } from "@sveltejs/kit";
|
| 3 |
import { collections } from "$lib/server/database";
|
| 4 |
import { base } from "$app/paths";
|
| 5 |
+
import {
|
| 6 |
+
authenticateRequest,
|
| 7 |
+
refreshSessionCookie,
|
| 8 |
+
requiresUser,
|
| 9 |
+
triggerOauthFlow,
|
| 10 |
+
} from "$lib/server/auth";
|
| 11 |
import { ERROR_MESSAGES } from "$lib/stores/errors";
|
| 12 |
import { addWeeks } from "date-fns";
|
| 13 |
import { checkAndRunMigrations } from "$lib/migrations/migrations";
|
|
|
|
| 131 |
{ type: "svelte", value: event.cookies }
|
| 132 |
);
|
| 133 |
|
|
|
|
| 134 |
event.locals.sessionId = auth.sessionId;
|
| 135 |
+
|
| 136 |
+
if (
|
| 137 |
+
!auth.user &&
|
| 138 |
+
config.AUTOMATIC_LOGIN === "true" &&
|
| 139 |
+
!event.url.pathname.startsWith(`${base}/login`)
|
| 140 |
+
) {
|
| 141 |
+
return await triggerOauthFlow({ request: event.request, url: event.url, locals: event.locals });
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
event.locals.user = auth.user || undefined;
|
| 145 |
event.locals.token = auth.token;
|
| 146 |
|
| 147 |
event.locals.isAdmin =
|
src/lib/server/adminToken.ts
CHANGED
|
@@ -37,9 +37,11 @@ class AdminTokenManager {
|
|
| 37 |
// if admin token is set, don't display it
|
| 38 |
if (!this.enabled || config.ADMIN_TOKEN) return;
|
| 39 |
|
| 40 |
-
let port = process.
|
| 41 |
-
? parseInt(process.
|
| 42 |
-
:
|
|
|
|
|
|
|
| 43 |
|
| 44 |
if (!port) {
|
| 45 |
const mode = process.argv.find((arg) => arg === "preview" || arg === "dev");
|
|
|
|
| 37 |
// if admin token is set, don't display it
|
| 38 |
if (!this.enabled || config.ADMIN_TOKEN) return;
|
| 39 |
|
| 40 |
+
let port = process.env.PORT
|
| 41 |
+
? parseInt(process.env.PORT)
|
| 42 |
+
: process.argv.includes("--port")
|
| 43 |
+
? parseInt(process.argv[process.argv.indexOf("--port") + 1])
|
| 44 |
+
: undefined;
|
| 45 |
|
| 46 |
if (!port) {
|
| 47 |
const mode = process.argv.find((arg) => arg === "preview" || arg === "dev");
|
src/lib/server/auth.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { config } from "$lib/server/config";
|
|
| 10 |
import { sha256 } from "$lib/utils/sha256";
|
| 11 |
import { z } from "zod";
|
| 12 |
import { dev } from "$app/environment";
|
| 13 |
-
import
|
| 14 |
import { collections } from "$lib/server/database";
|
| 15 |
import JSON5 from "json5";
|
| 16 |
import { logger } from "$lib/server/logger";
|
|
@@ -19,6 +19,7 @@ import type { Cookie } from "elysia";
|
|
| 19 |
import { adminTokenManager } from "./adminToken";
|
| 20 |
import type { User } from "$lib/types/User";
|
| 21 |
import type { Session } from "$lib/types/Session";
|
|
|
|
| 22 |
|
| 23 |
export interface OIDCSettings {
|
| 24 |
redirectURI: string;
|
|
@@ -359,3 +360,32 @@ export async function authenticateRequest(
|
|
| 359 |
|
| 360 |
return { user: undefined, sessionId, secretSessionId, isAdmin: false };
|
| 361 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
import { sha256 } from "$lib/utils/sha256";
|
| 11 |
import { z } from "zod";
|
| 12 |
import { dev } from "$app/environment";
|
| 13 |
+
import { redirect, type Cookies } from "@sveltejs/kit";
|
| 14 |
import { collections } from "$lib/server/database";
|
| 15 |
import JSON5 from "json5";
|
| 16 |
import { logger } from "$lib/server/logger";
|
|
|
|
| 19 |
import { adminTokenManager } from "./adminToken";
|
| 20 |
import type { User } from "$lib/types/User";
|
| 21 |
import type { Session } from "$lib/types/Session";
|
| 22 |
+
import { base } from "$app/paths";
|
| 23 |
|
| 24 |
export interface OIDCSettings {
|
| 25 |
redirectURI: string;
|
|
|
|
| 360 |
|
| 361 |
return { user: undefined, sessionId, secretSessionId, isAdmin: false };
|
| 362 |
}
|
| 363 |
+
|
| 364 |
+
export async function triggerOauthFlow({
|
| 365 |
+
request,
|
| 366 |
+
url,
|
| 367 |
+
locals,
|
| 368 |
+
}: {
|
| 369 |
+
request: Request;
|
| 370 |
+
url: URL;
|
| 371 |
+
locals: App.Locals;
|
| 372 |
+
}): Promise<Response> {
|
| 373 |
+
const referer = request.headers.get("referer");
|
| 374 |
+
let redirectURI = `${(referer ? new URL(referer) : url).origin}${base}/login/callback`;
|
| 375 |
+
|
| 376 |
+
// TODO: Handle errors if provider is not responding
|
| 377 |
+
|
| 378 |
+
if (url.searchParams.has("callback")) {
|
| 379 |
+
const callback = url.searchParams.get("callback") || redirectURI;
|
| 380 |
+
if (config.ALTERNATIVE_REDIRECT_URLS.includes(callback)) {
|
| 381 |
+
redirectURI = callback;
|
| 382 |
+
}
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
const authorizationUrl = await getOIDCAuthorizationUrl(
|
| 386 |
+
{ redirectURI },
|
| 387 |
+
{ sessionId: locals.sessionId }
|
| 388 |
+
);
|
| 389 |
+
|
| 390 |
+
throw redirect(302, authorizationUrl);
|
| 391 |
+
}
|
src/lib/server/endpoints/openai/endpointOai.ts
CHANGED
|
@@ -106,7 +106,14 @@ export async function endpointOai(
|
|
| 106 |
const imageProcessor = makeImageProcessor(multimodal.image);
|
| 107 |
|
| 108 |
if (completion === "completions") {
|
| 109 |
-
return async ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
const prompt = await buildPrompt({
|
| 111 |
messages,
|
| 112 |
continueMessage,
|
|
@@ -132,13 +139,21 @@ export async function endpointOai(
|
|
| 132 |
headers: {
|
| 133 |
"ChatUI-Conversation-ID": conversationId?.toString() ?? "",
|
| 134 |
"X-use-cache": "false",
|
|
|
|
| 135 |
},
|
| 136 |
});
|
| 137 |
|
| 138 |
return openAICompletionToTextGenerationStream(openAICompletion);
|
| 139 |
};
|
| 140 |
} else if (completion === "chat_completions") {
|
| 141 |
-
return async ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
// Format messages for the chat API, handling multimodal content if supported
|
| 143 |
let messagesOpenAI: OpenAI.Chat.Completions.ChatCompletionMessageParam[] =
|
| 144 |
await prepareMessages(messages, imageProcessor, isMultimodal ?? model.multimodal);
|
|
@@ -186,6 +201,7 @@ export async function endpointOai(
|
|
| 186 |
headers: {
|
| 187 |
"ChatUI-Conversation-ID": conversationId?.toString() ?? "",
|
| 188 |
"X-use-cache": "false",
|
|
|
|
| 189 |
},
|
| 190 |
}
|
| 191 |
);
|
|
@@ -198,6 +214,7 @@ export async function endpointOai(
|
|
| 198 |
headers: {
|
| 199 |
"ChatUI-Conversation-ID": conversationId?.toString() ?? "",
|
| 200 |
"X-use-cache": "false",
|
|
|
|
| 201 |
},
|
| 202 |
}
|
| 203 |
);
|
|
|
|
| 106 |
const imageProcessor = makeImageProcessor(multimodal.image);
|
| 107 |
|
| 108 |
if (completion === "completions") {
|
| 109 |
+
return async ({
|
| 110 |
+
messages,
|
| 111 |
+
preprompt,
|
| 112 |
+
continueMessage,
|
| 113 |
+
generateSettings,
|
| 114 |
+
conversationId,
|
| 115 |
+
locals,
|
| 116 |
+
}) => {
|
| 117 |
const prompt = await buildPrompt({
|
| 118 |
messages,
|
| 119 |
continueMessage,
|
|
|
|
| 139 |
headers: {
|
| 140 |
"ChatUI-Conversation-ID": conversationId?.toString() ?? "",
|
| 141 |
"X-use-cache": "false",
|
| 142 |
+
...(locals?.token ? { Authorization: `Bearer ${locals.token}` } : {}),
|
| 143 |
},
|
| 144 |
});
|
| 145 |
|
| 146 |
return openAICompletionToTextGenerationStream(openAICompletion);
|
| 147 |
};
|
| 148 |
} else if (completion === "chat_completions") {
|
| 149 |
+
return async ({
|
| 150 |
+
messages,
|
| 151 |
+
preprompt,
|
| 152 |
+
generateSettings,
|
| 153 |
+
conversationId,
|
| 154 |
+
isMultimodal,
|
| 155 |
+
locals,
|
| 156 |
+
}) => {
|
| 157 |
// Format messages for the chat API, handling multimodal content if supported
|
| 158 |
let messagesOpenAI: OpenAI.Chat.Completions.ChatCompletionMessageParam[] =
|
| 159 |
await prepareMessages(messages, imageProcessor, isMultimodal ?? model.multimodal);
|
|
|
|
| 201 |
headers: {
|
| 202 |
"ChatUI-Conversation-ID": conversationId?.toString() ?? "",
|
| 203 |
"X-use-cache": "false",
|
| 204 |
+
...(locals?.token ? { Authorization: `Bearer ${locals.token}` } : {}),
|
| 205 |
},
|
| 206 |
}
|
| 207 |
);
|
|
|
|
| 214 |
headers: {
|
| 215 |
"ChatUI-Conversation-ID": conversationId?.toString() ?? "",
|
| 216 |
"X-use-cache": "false",
|
| 217 |
+
...(locals?.token ? { Authorization: `Bearer ${locals.token}` } : {}),
|
| 218 |
},
|
| 219 |
}
|
| 220 |
);
|
src/routes/login/+server.ts
CHANGED
|
@@ -1,29 +1,5 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import { base } from "$app/paths";
|
| 3 |
-
import { config } from "$lib/server/config";
|
| 4 |
|
| 5 |
export async function GET({ request, url, locals }) {
|
| 6 |
-
|
| 7 |
-
let redirectURI = `${(referer ? new URL(referer) : url).origin}${base}/login/callback`;
|
| 8 |
-
|
| 9 |
-
// TODO: Handle errors if provider is not responding
|
| 10 |
-
|
| 11 |
-
if (url.searchParams.has("callback")) {
|
| 12 |
-
const callback = url.searchParams.get("callback") || redirectURI;
|
| 13 |
-
if (config.ALTERNATIVE_REDIRECT_URLS.includes(callback)) {
|
| 14 |
-
redirectURI = callback;
|
| 15 |
-
}
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
const authorizationUrl = await getOIDCAuthorizationUrl(
|
| 19 |
-
{ redirectURI },
|
| 20 |
-
{ sessionId: locals.sessionId }
|
| 21 |
-
);
|
| 22 |
-
|
| 23 |
-
return new Response(null, {
|
| 24 |
-
status: 302,
|
| 25 |
-
headers: {
|
| 26 |
-
Location: authorizationUrl,
|
| 27 |
-
},
|
| 28 |
-
});
|
| 29 |
}
|
|
|
|
| 1 |
+
import { triggerOauthFlow } from "$lib/server/auth";
|
|
|
|
|
|
|
| 2 |
|
| 3 |
export async function GET({ request, url, locals }) {
|
| 4 |
+
return await triggerOauthFlow({ request, url, locals });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
}
|