diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/Routes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/Routes.kt index 807236c7d..cd24cc3b4 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/Routes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/Routes.kt @@ -1,6 +1,9 @@ package com.xebia.functional.xef.server.http.routes import com.aallam.openai.api.BetaOpenAI +import com.xebia.functional.xef.server.models.LoginRequest +import com.xebia.functional.xef.server.models.LoginResponse +import com.xebia.functional.xef.server.models.RegisterRequest import com.xebia.functional.xef.server.services.PersistenceService import io.ktor.client.* import io.ktor.client.call.* @@ -12,7 +15,6 @@ import io.ktor.server.auth.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -import io.ktor.util.* import io.ktor.util.pipeline.* import io.ktor.utils.io.jvm.javaio.* import kotlinx.serialization.json.Json @@ -31,7 +33,6 @@ fun String.toProvider(): Provider? = when (this) { else -> Provider.OPENAI } - @OptIn(BetaOpenAI::class) fun Routing.routes( client: HttpClient, @@ -39,6 +40,18 @@ fun Routing.routes( ) { val openAiUrl = "https://api.openai.com/v1" + post("/register") { + // fake implementation for testing + val request = Json.decodeFromString(call.receive()) + call.respond(LoginResponse("token: ${request.password}")) + } + + post("/login") { + // fake implementation for testing + val request = Json.decodeFromString(call.receive()) + call.respond(LoginResponse("token: ${request.password}")) + } + authenticate("auth-bearer") { post("/chat/completions") { val token = call.getToken() diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/models/LoginResponse.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/models/LoginResponse.kt new file mode 100644 index 000000000..ecf60eedd --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/models/LoginResponse.kt @@ -0,0 +1,6 @@ +package com.xebia.functional.xef.server.models + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginResponse(val authToken: String) diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/models/Requests.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/models/Requests.kt new file mode 100644 index 000000000..8bf966e60 --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/models/Requests.kt @@ -0,0 +1,16 @@ +package com.xebia.functional.xef.server.models + +import kotlinx.serialization.Serializable + +@Serializable +data class RegisterRequest( + val name: String, + val email: String, + val password: String +) + +@Serializable +data class LoginRequest( + val email: String, + val password: String +) diff --git a/server/web/src/components/Header/Header.module.css b/server/web/src/components/Header/Header.module.css index 30e0fc560..652a00b51 100644 --- a/server/web/src/components/Header/Header.module.css +++ b/server/web/src/components/Header/Header.module.css @@ -3,3 +3,9 @@ height: 2rem; width: auto; } + +.panel-right { + display: block; + text-align: right; + margin-left: auto; +} \ No newline at end of file diff --git a/server/web/src/components/Header/Header.tsx b/server/web/src/components/Header/Header.tsx index 163b6dc3e..3b0044736 100644 --- a/server/web/src/components/Header/Header.tsx +++ b/server/web/src/components/Header/Header.tsx @@ -1,15 +1,26 @@ -import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material'; +import { AppBar, Box, Button, IconButton, Toolbar, Typography } from '@mui/material'; import { Menu } from '@mui/icons-material'; import logo from '@/assets/xef-brand-name.svg'; import styles from './Header.module.css'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '@/state/Auth'; export type HeaderProps = { action: () => void; }; export function Header({ action }: HeaderProps) { + const navigate = useNavigate(); + const auth = useAuth(); + + const handleSubmit = () => { + auth.signout(() => { + navigate("/login", { replace: true }); + }); + }; + return ( @@ -24,9 +35,13 @@ export function Header({ action }: HeaderProps) { Logo - - Dashboard - + diff --git a/server/web/src/components/Login/Login.module.css b/server/web/src/components/Login/Login.module.css new file mode 100644 index 000000000..0ce81ced3 --- /dev/null +++ b/server/web/src/components/Login/Login.module.css @@ -0,0 +1,6 @@ +.center { + position: absolute; + left: 50%; + top: 20%; + transform: translate(-50%, -50%); +} \ No newline at end of file diff --git a/server/web/src/components/Login/Login.tsx b/server/web/src/components/Login/Login.tsx new file mode 100644 index 000000000..37288f176 --- /dev/null +++ b/server/web/src/components/Login/Login.tsx @@ -0,0 +1,108 @@ +import { useAuth } from '@/state/Auth'; +import { Box, Button, TextField, Typography } from '@mui/material'; +import { ChangeEvent, useState } from 'react'; +import { Navigate, useLocation, useNavigate } from 'react-router-dom'; +import styles from './Login.module.css'; + +export function RequireAuth({ children }: { children: JSX.Element }) { + let auth = useAuth(); + let location = useLocation(); + + if (!auth.authToken) { + return ; + } + + return children; +} + +export function Login() { + const navigate = useNavigate(); + const location = useLocation(); + const auth = useAuth(); + + const from = location.state?.from?.pathname || '/'; + + const [emailInput, setEmailInput] = useState(''); + const [passwordInput, setPasswordInput] = useState(''); + + const handleSubmit = () => { + auth.signin(emailInput, () => { + navigate(from, { replace: true }); + }); + }; + + const emailHandleChange = (event: ChangeEvent) => { + setEmailInput(event.target.value); + }; + + const passwordHandleChange = (event: ChangeEvent) => { + setPasswordInput(event.target.value); + }; + + const disabledButton = passwordInput?.trim() == "" || emailInput?.trim() == ""; + + return ( + + + + Xef Server + + + + + + + + + + + ); + + // return ( + //
+ //

You must log in to view the page at {from}

+ + //
+ // {' '} + // + //
+ //
+ // ); +} diff --git a/server/web/src/components/Login/index.ts b/server/web/src/components/Login/index.ts new file mode 100644 index 000000000..a10c3a83a --- /dev/null +++ b/server/web/src/components/Login/index.ts @@ -0,0 +1 @@ +export * from './Login'; diff --git a/server/web/src/components/Pages/Root/Root.tsx b/server/web/src/components/Pages/Root/Root.tsx index 9e562e0f7..3619146fe 100644 --- a/server/web/src/components/Pages/Root/Root.tsx +++ b/server/web/src/components/Pages/Root/Root.tsx @@ -1,3 +1,3 @@ export function Root() { - return <>Root; + return <>Dashboard; } diff --git a/server/web/src/main.tsx b/server/web/src/main.tsx index 3fb807413..f719db16b 100644 --- a/server/web/src/main.tsx +++ b/server/web/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { createBrowserRouter, Navigate, RouterProvider, useLocation, useNavigate } from 'react-router-dom'; import { CssBaseline, StyledEngineProvider } from '@mui/material'; import { ThemeProvider } from '@emotion/react'; @@ -18,8 +18,20 @@ import { SettingsProvider } from '@/state/Settings'; import { theme } from '@/styles/theme'; import './main.css'; +import { AuthProvider } from './state/Auth'; +import { Login, RequireAuth } from './components/Login'; const router = createBrowserRouter([ + { + path: '/login', + errorElement: , + children: [ + { + path: '/login', + element: , + }, + ] + }, { path: '/', element: , @@ -27,23 +39,43 @@ const router = createBrowserRouter([ children: [ { path: '/', - element: , + element: ( + + + + ), }, { path: '1', - element: , + element: ( + + + + ), }, { path: '2', - element: , + element: ( + + + + ), }, { path: 'generic-question', - element: , + element: ( + + + + ), }, { path: 'settings', - element: , + element: ( + + + + ), }, ], }, @@ -54,11 +86,13 @@ createRoot(document.getElementById('root') as HTMLElement).render( - - - - - + + + + + + + , diff --git a/server/web/src/state/Auth/AuthContext.tsx b/server/web/src/state/Auth/AuthContext.tsx new file mode 100644 index 000000000..ddc154ef8 --- /dev/null +++ b/server/web/src/state/Auth/AuthContext.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { createContext, useState } from 'react'; + +export function useAuth() { + return React.useContext(AuthContext); +} + +interface AuthContextType { + authToken: string; + signin: (authToken: string, callback: VoidFunction) => void; + signout: (callback: VoidFunction) => void; +} + +const AuthContext = createContext(null!); + +function AuthProvider({ children }: { children: React.ReactNode }) { + const [authToken, setAuthToken] = useState(sessionStorage.getItem('authToken') || ''); + + const signin = (newAuthToken: string, callback: VoidFunction) => { + setAuthToken(newAuthToken); + sessionStorage.setItem('authToken', newAuthToken); + callback(); + }; + + const signout = (callback: VoidFunction) => { + setAuthToken(''); + sessionStorage.setItem('authToken', ''); + callback(); + }; + + const value = { authToken, signin, signout }; + + return {children}; +} + +export { AuthContext, AuthProvider }; diff --git a/server/web/src/state/Auth/index.ts b/server/web/src/state/Auth/index.ts new file mode 100644 index 000000000..dc39de3c1 --- /dev/null +++ b/server/web/src/state/Auth/index.ts @@ -0,0 +1 @@ +export * from './AuthContext';