mirror of https://github.com/ollama/ollama
app/ui: refactor to use Ollama endpoints for user auth and health checks (#13081)
This commit is contained in:
parent
bbbb6b2a01
commit
7cf6f18c1f
|
|
@ -273,10 +273,6 @@ func main() {
|
||||||
Handler: uiServer.Handler(),
|
Handler: uiServer.Handler(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := uiServer.UserData(ctx); err != nil {
|
|
||||||
slog.Warn("failed to load user data", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the UI server
|
// Start the UI server
|
||||||
slog.Info("starting ui server", "port", port)
|
slog.Info("starting ui server", "port", port)
|
||||||
go func() {
|
go func() {
|
||||||
|
|
@ -320,6 +316,17 @@ func main() {
|
||||||
slog.Debug("no URL scheme request to handle")
|
slog.Debug("no URL scheme request to handle")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
slog.Debug("waiting for ollama server to be ready")
|
||||||
|
if err := ui.WaitForServer(ctx, 10*time.Second); err != nil {
|
||||||
|
slog.Warn("ollama server not ready, continuing anyway", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := uiServer.UserData(ctx); err != nil {
|
||||||
|
slog.Warn("failed to load user data", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
osRun(cancel, hasCompletedFirstRun, startHidden)
|
osRun(cancel, hasCompletedFirstRun, startHidden)
|
||||||
|
|
||||||
slog.Info("shutting down desktop server")
|
slog.Info("shutting down desktop server")
|
||||||
|
|
@ -361,7 +368,7 @@ func checkUserLoggedIn(uiServerPort int) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/v1/me", uiServerPort))
|
resp, err := http.Post(fmt.Sprintf("http://127.0.0.1:%d/api/me", uiServerPort), "application/json", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug("failed to call local auth endpoint", "error", err)
|
slog.Debug("failed to call local auth endpoint", "error", err)
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -469,26 +469,24 @@ export class HealthResponse {
|
||||||
}
|
}
|
||||||
export class User {
|
export class User {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
avatarURL: string;
|
name: string;
|
||||||
plan: string;
|
bio?: string;
|
||||||
bio: string;
|
avatarurl?: string;
|
||||||
firstName: string;
|
firstname?: string;
|
||||||
lastName: string;
|
lastname?: string;
|
||||||
overThreshold: boolean;
|
plan?: string;
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
this.name = source["name"];
|
|
||||||
this.email = source["email"];
|
this.email = source["email"];
|
||||||
this.avatarURL = source["avatarURL"];
|
this.name = source["name"];
|
||||||
this.plan = source["plan"];
|
|
||||||
this.bio = source["bio"];
|
this.bio = source["bio"];
|
||||||
this.firstName = source["firstName"];
|
this.avatarurl = source["avatarurl"];
|
||||||
this.lastName = source["lastName"];
|
this.firstname = source["firstname"];
|
||||||
this.overThreshold = source["overThreshold"];
|
this.lastname = source["lastname"];
|
||||||
|
this.plan = source["plan"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class Attachment {
|
export class Attachment {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import { parseJsonlFromResponse } from "./util/jsonl-parsing";
|
import { parseJsonlFromResponse } from "./util/jsonl-parsing";
|
||||||
import { ollamaClient as ollama } from "./lib/ollama-client";
|
import { ollamaClient as ollama } from "./lib/ollama-client";
|
||||||
import type { ModelResponse } from "ollama/browser";
|
import type { ModelResponse } from "ollama/browser";
|
||||||
import { API_BASE } from "./lib/config";
|
import { API_BASE, OLLAMA_DOT_COM } from "./lib/config";
|
||||||
|
|
||||||
// Extend Model class with utility methods
|
// Extend Model class with utility methods
|
||||||
declare module "@/gotypes" {
|
declare module "@/gotypes" {
|
||||||
|
|
@ -27,7 +27,6 @@ declare module "@/gotypes" {
|
||||||
Model.prototype.isCloud = function (): boolean {
|
Model.prototype.isCloud = function (): boolean {
|
||||||
return this.model.endsWith("cloud");
|
return this.model.endsWith("cloud");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to convert Uint8Array to base64
|
// Helper function to convert Uint8Array to base64
|
||||||
function uint8ArrayToBase64(uint8Array: Uint8Array): string {
|
function uint8ArrayToBase64(uint8Array: Uint8Array): string {
|
||||||
const chunkSize = 0x8000; // 32KB chunks to avoid stack overflow
|
const chunkSize = 0x8000; // 32KB chunks to avoid stack overflow
|
||||||
|
|
@ -42,44 +41,50 @@ function uint8ArrayToBase64(uint8Array: Uint8Array): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchUser(): Promise<User | null> {
|
export async function fetchUser(): Promise<User | null> {
|
||||||
try {
|
const response = await fetch(`${API_BASE}/api/me`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/me`, {
|
method: "POST",
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const userData: User = await response.json();
|
|
||||||
return userData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching user:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchConnectUrl(): Promise<string> {
|
|
||||||
const response = await fetch(`${API_BASE}/api/v1/connect`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (response.ok) {
|
||||||
throw new Error("Failed to fetch connect URL");
|
const userData: User = await response.json();
|
||||||
|
|
||||||
|
if (userData.avatarurl && !userData.avatarurl.startsWith("http")) {
|
||||||
|
userData.avatarurl = `${OLLAMA_DOT_COM}${userData.avatarurl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
if (response.status === 401 || response.status === 403) {
|
||||||
return data.connect_url;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Failed to fetch user: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchConnectUrl(): Promise<string> {
|
||||||
|
const response = await fetch(`${API_BASE}/api/me`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.signin_url) {
|
||||||
|
return data.signin_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Failed to fetch connect URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function disconnectUser(): Promise<void> {
|
export async function disconnectUser(): Promise<void> {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/disconnect`, {
|
const response = await fetch(`${API_BASE}/api/signout`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -389,7 +394,8 @@ export async function getInferenceCompute(): Promise<InferenceCompute[]> {
|
||||||
|
|
||||||
export async function fetchHealth(): Promise<boolean> {
|
export async function fetchHealth(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/health`, {
|
// Use the /api/version endpoint as a health check
|
||||||
|
const response = await fetch(`${API_BASE}/api/version`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -398,7 +404,8 @@ export async function fetchHealth(): Promise<boolean> {
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.healthy || false;
|
// If we get a version back, the server is healthy
|
||||||
|
return !!data.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -299,9 +299,9 @@ export default function Settings() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user?.avatarURL && (
|
{user?.avatarurl && (
|
||||||
<img
|
<img
|
||||||
src={user.avatarURL}
|
src={user.avatarurl}
|
||||||
alt={user?.name}
|
alt={user?.name}
|
||||||
className="h-10 w-10 rounded-full bg-neutral-200 dark:bg-neutral-700 flex-shrink-0"
|
className="h-10 w-10 rounded-full bg-neutral-200 dark:bg-neutral-700 flex-shrink-0"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,20 @@
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { fetchUser, fetchConnectUrl, disconnectUser } from "@/api";
|
import { fetchUser, fetchConnectUrl, disconnectUser } from "@/api";
|
||||||
|
|
||||||
export function useUser() {
|
export function useUser() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [initialDataLoaded, setInitialDataLoaded] = useState(false);
|
|
||||||
|
|
||||||
// Wait for initial data to be loaded
|
|
||||||
useEffect(() => {
|
|
||||||
const initialPromise = window.__initialUserDataPromise;
|
|
||||||
if (initialPromise) {
|
|
||||||
initialPromise.finally(() => {
|
|
||||||
setInitialDataLoaded(true);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setInitialDataLoaded(true);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const userQuery = useQuery({
|
const userQuery = useQuery({
|
||||||
queryKey: ["user"],
|
queryKey: ["user"],
|
||||||
queryFn: () => fetchUser(),
|
queryFn: async () => {
|
||||||
|
const result = await fetchUser();
|
||||||
|
return result;
|
||||||
|
},
|
||||||
staleTime: 5 * 60 * 1000, // Consider data stale after 5 minutes
|
staleTime: 5 * 60 * 1000, // Consider data stale after 5 minutes
|
||||||
gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
||||||
initialData: null, // Start with null to prevent flashing
|
retry: 10,
|
||||||
|
retryDelay: (attemptIndex) => Math.min(500 * attemptIndex, 2000),
|
||||||
|
refetchOnMount: true, // Always fetch when component mounts
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mutation to refresh user data
|
// Mutation to refresh user data
|
||||||
|
|
@ -49,14 +40,15 @@ export function useUser() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isLoading = userQuery.isLoading || userQuery.isFetching;
|
||||||
|
const isAuthenticated = Boolean(userQuery.data?.name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: userQuery.data,
|
user: userQuery.data,
|
||||||
isLoading:
|
isLoading,
|
||||||
!initialDataLoaded ||
|
|
||||||
(userQuery.isLoading && userQuery.data === undefined), // Show loading until initial data is loaded
|
|
||||||
isError: userQuery.isError,
|
isError: userQuery.isError,
|
||||||
error: userQuery.error,
|
error: userQuery.error,
|
||||||
isAuthenticated: Boolean(userQuery.data?.name),
|
isAuthenticated,
|
||||||
refreshUser: refreshUser.mutate,
|
refreshUser: refreshUser.mutate,
|
||||||
isRefreshing: refreshUser.isPending,
|
isRefreshing: refreshUser.isPending,
|
||||||
refetchUser: userQuery.refetch,
|
refetchUser: userQuery.refetch,
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,6 @@ export const API_BASE = import.meta.env.DEV ? DEV_API_URL : "";
|
||||||
export const OLLAMA_HOST = import.meta.env.DEV
|
export const OLLAMA_HOST = import.meta.env.DEV
|
||||||
? DEV_API_URL
|
? DEV_API_URL
|
||||||
: window.location.origin;
|
: window.location.origin;
|
||||||
|
|
||||||
|
export const OLLAMA_DOT_COM =
|
||||||
|
import.meta.env.VITE_OLLAMA_DOT_COM_URL || "https://ollama.com";
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { routeTree } from "./routeTree.gen";
|
import { routeTree } from "./routeTree.gen";
|
||||||
import { fetchUser } from "./api";
|
import { fetchUser } from "./api";
|
||||||
import { StreamingProvider } from "./contexts/StreamingContext";
|
import { StreamingProvider } from "./contexts/StreamingContext";
|
||||||
import { User } from "@/gotypes";
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
__initialUserDataPromise?: Promise<User | null>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
|
|
@ -24,27 +17,11 @@ const queryClient = new QueryClient({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track initial user data fetch
|
fetchUser().then((userData) => {
|
||||||
let initialUserDataPromise: Promise<User | null> | null = null;
|
if (userData) {
|
||||||
|
|
||||||
// Initialize user data on app startup
|
|
||||||
const initializeUserData = async () => {
|
|
||||||
try {
|
|
||||||
const userData = await fetchUser();
|
|
||||||
queryClient.setQueryData(["user"], userData);
|
queryClient.setQueryData(["user"], userData);
|
||||||
return userData;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error initializing user data:", error);
|
|
||||||
queryClient.setQueryData(["user"], null);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Start initialization immediately and track the promise
|
|
||||||
initialUserDataPromise = initializeUserData();
|
|
||||||
|
|
||||||
// Export the promise so hooks can await it
|
|
||||||
window.__initialUserDataPromise = initialUserDataPromise;
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
|
|
|
||||||
|
|
@ -101,15 +101,14 @@ type HealthResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Email string `json:"email"`
|
||||||
Email string `json:"email"`
|
Name string `json:"name"`
|
||||||
AvatarURL string `json:"avatarURL"`
|
Bio string `json:"bio,omitempty"`
|
||||||
Plan string `json:"plan"`
|
AvatarURL string `json:"avatarurl,omitempty"`
|
||||||
Bio string `json:"bio"`
|
FirstName string `json:"firstname,omitempty"`
|
||||||
FirstName string `json:"firstName"`
|
LastName string `json:"lastname,omitempty"`
|
||||||
LastName string `json:"lastName"`
|
Plan string `json:"plan,omitempty"`
|
||||||
OverThreshold bool `json:"overThreshold"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Attachment struct {
|
type Attachment struct {
|
||||||
|
|
|
||||||
159
app/ui/ui.go
159
app/ui/ui.go
|
|
@ -23,7 +23,6 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
"github.com/ollama/ollama/app/auth"
|
|
||||||
"github.com/ollama/ollama/app/server"
|
"github.com/ollama/ollama/app/server"
|
||||||
"github.com/ollama/ollama/app/store"
|
"github.com/ollama/ollama/app/store"
|
||||||
"github.com/ollama/ollama/app/tools"
|
"github.com/ollama/ollama/app/tools"
|
||||||
|
|
@ -264,11 +263,10 @@ func (s *Server) Handler() http.Handler {
|
||||||
ollamaProxy := s.ollamaProxy()
|
ollamaProxy := s.ollamaProxy()
|
||||||
mux.Handle("GET /api/tags", ollamaProxy)
|
mux.Handle("GET /api/tags", ollamaProxy)
|
||||||
mux.Handle("POST /api/show", ollamaProxy)
|
mux.Handle("POST /api/show", ollamaProxy)
|
||||||
|
mux.Handle("GET /api/version", ollamaProxy)
|
||||||
mux.Handle("GET /api/v1/me", handle(s.me))
|
mux.Handle("HEAD /api/version", ollamaProxy)
|
||||||
mux.Handle("POST /api/v1/disconnect", handle(s.disconnect))
|
mux.Handle("POST /api/me", ollamaProxy)
|
||||||
mux.Handle("GET /api/v1/connect", handle(s.connectURL))
|
mux.Handle("POST /api/signout", ollamaProxy)
|
||||||
mux.Handle("GET /api/v1/health", handle(s.health))
|
|
||||||
|
|
||||||
// React app - catch all non-API routes and serve the React app
|
// React app - catch all non-API routes and serve the React app
|
||||||
mux.Handle("GET /", s.appHandler())
|
mux.Handle("GET /", s.appHandler())
|
||||||
|
|
@ -338,7 +336,7 @@ func (s *Server) doSelfSigned(ctx context.Context, method, path string) (*http.R
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserData fetches user data from ollama.com API for the current ollama key
|
// UserData fetches user data from ollama.com API for the current ollama key
|
||||||
func (s *Server) UserData(ctx context.Context) (*responses.User, error) {
|
func (s *Server) UserData(ctx context.Context) (*api.UserResponse, error) {
|
||||||
resp, err := s.doSelfSigned(ctx, http.MethodPost, "/api/me")
|
resp, err := s.doSelfSigned(ctx, http.MethodPost, "/api/me")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to call ollama.com/api/me: %w", err)
|
return nil, fmt.Errorf("failed to call ollama.com/api/me: %w", err)
|
||||||
|
|
@ -349,7 +347,7 @@ func (s *Server) UserData(ctx context.Context) (*responses.User, error) {
|
||||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var user responses.User
|
var user api.UserResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse user response: %w", err)
|
return nil, fmt.Errorf("failed to parse user response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -368,29 +366,27 @@ func (s *Server) UserData(ctx context.Context) (*responses.User, error) {
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForServer(ctx context.Context) error {
|
// WaitForServer waits for the Ollama server to be ready
|
||||||
timeout := time.Now().Add(10 * time.Second)
|
func WaitForServer(ctx context.Context, timeout time.Duration) error {
|
||||||
// TODO: this avoids an error on first load of the app
|
deadline := time.Now().Add(timeout)
|
||||||
// however we should either show a loading state or
|
for time.Now().Before(deadline) {
|
||||||
// wait for the Ollama server to be ready before redirecting
|
|
||||||
for {
|
|
||||||
c, err := api.ClientFromEnvironment()
|
c, err := api.ClientFromEnvironment()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := c.Version(ctx); err == nil {
|
if _, err := c.Version(ctx); err == nil {
|
||||||
break
|
slog.Debug("ollama server is ready")
|
||||||
}
|
return nil
|
||||||
if time.Now().After(timeout) {
|
|
||||||
return fmt.Errorf("timeout waiting for Ollama server to be ready")
|
|
||||||
}
|
}
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
}
|
}
|
||||||
return nil
|
return errors.New("timeout waiting for Ollama server to be ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) createChat(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) createChat(w http.ResponseWriter, r *http.Request) error {
|
||||||
waitForServer(r.Context())
|
if err := WaitForServer(r.Context(), 10*time.Second); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
id, err := uuid.NewV7()
|
id, err := uuid.NewV7()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1438,129 +1434,6 @@ func (s *Server) settings(w http.ResponseWriter, r *http.Request) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) me(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := s.UserData(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
// If fetching from API fails, try to return cached user data if available
|
|
||||||
if cachedUser, cacheErr := s.Store.User(); cacheErr == nil && cachedUser != nil {
|
|
||||||
s.log().Info("API request failed, returning cached user data", "error", err)
|
|
||||||
responseUser := &responses.User{
|
|
||||||
Name: cachedUser.Name,
|
|
||||||
Email: cachedUser.Email,
|
|
||||||
Plan: cachedUser.Plan,
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return json.NewEncoder(w).Encode(responseUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.log().Error("failed to get user data", "error", err)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return json.NewEncoder(w).Encode(responses.Error{
|
|
||||||
Error: "failed to get user data",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return json.NewEncoder(w).Encode(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) disconnect(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Store.ClearUser(); err != nil {
|
|
||||||
s.log().Warn("failed to clear cached user data", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the SSH public key to encode for the delete request
|
|
||||||
pubKey, err := ollamaAuth.GetPublicKey()
|
|
||||||
if err != nil {
|
|
||||||
s.log().Error("failed to get public key", "error", err)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return json.NewEncoder(w).Encode(responses.Error{
|
|
||||||
Error: "failed to get public key",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode the key using base64 URL encoding
|
|
||||||
encodedKey := base64.RawURLEncoding.EncodeToString([]byte(pubKey))
|
|
||||||
|
|
||||||
// Call the /api/user/keys/{encodedKey} endpoint with DELETE
|
|
||||||
resp, err := s.doSelfSigned(r.Context(), http.MethodDelete, fmt.Sprintf("/api/user/keys/%s", encodedKey))
|
|
||||||
if err != nil {
|
|
||||||
s.log().Error("failed to call ollama.com/api/user/keys", "error", err)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return json.NewEncoder(w).Encode(responses.Error{
|
|
||||||
Error: "failed to disconnect from ollama.com",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
s.log().Error("disconnect request failed", "status", resp.StatusCode)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return json.NewEncoder(w).Encode(responses.Error{
|
|
||||||
Error: "failed to disconnect from ollama.com",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return json.NewEncoder(w).Encode(map[string]string{"status": "disconnected"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) connectURL(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
connectURL, err := auth.BuildConnectURL(OllamaDotCom)
|
|
||||||
if err != nil {
|
|
||||||
s.log().Error("failed to build connect URL", "error", err)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return json.NewEncoder(w).Encode(responses.Error{
|
|
||||||
Error: "failed to build connect URL",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return json.NewEncoder(w).Encode(map[string]string{
|
|
||||||
"connect_url": connectURL,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) health(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
healthy := false
|
|
||||||
c, err := api.ClientFromEnvironment()
|
|
||||||
if err == nil {
|
|
||||||
if _, err := c.Version(r.Context()); err == nil {
|
|
||||||
healthy = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return json.NewEncoder(w).Encode(responses.HealthResponse{
|
|
||||||
Healthy: healthy,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) getInferenceCompute(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) getInferenceCompute(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
|
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue