Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/front end integration authentication #46

Merged
merged 8 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

* First download gguf from https://huggingface.co/TheBloke/Llama-2-13B-chat-GGUF , `llama-2-13b-chat.Q5_K_S.gguf`
* move this model to `model/llama-2-13b-chat.Q5_K_S.gguf`
* to locally develop the frontend, you need to:
1. Navigate to the frontend directory: `cd frontend`
2. Install PNPM: `npm install -g pnpm`
3. Install Dependencies: `npm start`
4. start the application in dev mode: `pnpm dev`
5. production build: `pnpm build`

# To run the backend
* run docker containers: docker-compose up -d
* after making sure that the database and the frontend are running, run the LlmServiceApplication


## Running the frontend:
1. Navigate to the frontend directory: `cd frontend`
2. Install node `node:18.17.1`
3. Install PNPM: `npm install -g pnpm` `pnpm:>=8.0.0`
4. Install Dependencies: `pnpm install`
5. start the application in dev mode: `pnpm dev`
6. production build: `pnpm build`
7. any linter problem `pnpm lint:fix` or to check `pnpm lint`


## Running the backend
* run docker containers
* after making sure that the database and the frontend are running, run the `LlmServiceApplication`
* to run the Backend linter `mvn spotless:check` to fix `mvn spotless:check`
* Backend uses cpp plugin to run the modal , `C++11` `g++` `cmake`
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ public interface UserApiMapper {
LoginResponse map(AuthenticationResponse response);

@Mapping(target = "name", source = "username")
@Mapping(target = "role", source = "role")
UserResponse map(User user);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.llm_service.llm_service.controller.user;

import com.llm_service.llm_service.dto.User;
import com.llm_service.llm_service.exception.UnAuthorizedException;
import com.llm_service.llm_service.exception.user.UserAlreadyExistsException;
import com.llm_service.llm_service.exception.user.UserNotFoundException;
import com.llm_service.llm_service.exception.user.UsernameAlreadyExistsException;
Expand All @@ -26,6 +27,20 @@ public UserController(AuthenticationService authenticationService, UserApiMapper
this.userApiMapper = userApiMapper;
}

@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "Get Current User Data",
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "get the current user")
@GetMapping("/me")
public ResponseEntity<UserResponse> register() throws UnAuthorizedException {
User user = authenticationService.getUser().orElseThrow(UnAuthorizedException::new);
return ResponseEntity.status(HttpStatus.OK).body(userApiMapper.map(user));
}

@ApiResponses(
value = {
@ApiResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.llm_service.llm_service.controller.user;

import com.llm_service.llm_service.persistance.entities.Role;
import lombok.Value;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
Expand All @@ -9,4 +10,5 @@
@SuperBuilder
public class UserResponse {
String name;
Role role;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import com.llm_service.llm_service.persistance.repositories.token.TokenPersistenceManager;
import com.llm_service.llm_service.persistance.repositories.user.UserEntityMapper;
import com.llm_service.llm_service.persistance.repositories.user.UserPersistenceManager;
import com.llm_service.llm_service.service.user.UserContext;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -32,6 +34,12 @@ public class AuthenticationService {

private final UserEntityMapper userEntityMapper;

private final UserContext userContext;

public Optional<User> getUser() {
return userContext.getUserFromContext();
}

public User register(UserRequest userRequest) throws UsernameAlreadyExistsException {
User user = User.builder()
.role(userRequest.getRole())
Expand Down
1 change: 1 addition & 0 deletions frontend/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
}
},
"rules": {
"implicit-arrow-linebreak": "off",
"@typescript-eslint/no-unused-vars": "off",
"no-unused-vars": "off",
"object-curly-newline": "off",
Expand Down
56 changes: 6 additions & 50 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,17 @@
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Welcome from "pages/Welcome/Welcome.tsx";
import Conversation from "pages/Conversation/Conversation.tsx";
import Login from "pages/Auth/Login.tsx";
import Register from "pages/Auth/Register.tsx";
import ForgotPassword from "pages/Auth/ForgotPassword.tsx";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import ConversationRedirect from "./pages/Conversation/ConversationRedirect.tsx";
import Error500 from "./pages/Error/Error500.tsx";
import Error404 from "./pages/Error/Error404.tsx";

const router = createBrowserRouter([
{
path: "/",
Component: Welcome,
},
{
path: "/conversation",
Component: ConversationRedirect,
},
{
path: "/conversation/:id",
Component: Conversation,
},
{
path: "/login",
Component: Login,
},
{
path: "/register",
Component: Register,
},
{
path: "/forgot-password",
Component: ForgotPassword,
},
{
path: "/404",
Component: Error404,
},
{
path: "/500",
Component: Error500,
},
{
path: "*",
Component: Error404,
},
]);
import { AuthProvider } from "./context/AuthContext.tsx";
import Routes from "./router/Routes.tsx";

const queryClient = new QueryClient();

function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<ReactQueryDevtools initialIsOpen={false} />
<AuthProvider>
<Routes />
<ReactQueryDevtools initialIsOpen={false} />
</AuthProvider>
</QueryClientProvider>
);
}
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createContext, PropsWithChildren, useEffect, useMemo, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";

type AuthContextType = { token: string | null; setToken: (token: string | null) => void };

export const AuthContext = createContext<AuthContextType>({
token: null,
setToken: () => {},
});

export function AuthProvider({ children }: PropsWithChildren) {
// State to hold the authentication token
const [token, setToken_] = useState(localStorage.getItem("token"));
const queryClient = useQueryClient();

// Function to set the authentication token
const setToken = (newToken: string | null) => {
setToken_(newToken);
};

useEffect(() => {
if (token) {
localStorage.setItem("token", token);
} else {
localStorage.removeItem("token");
queryClient.clear();
}
}, [queryClient, token]);

// Memoized value of the authentication context
const contextValue = useMemo(
() => ({
token,
setToken,
}),
[token],
);

// Provide the authentication context to the children components
return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
}
6 changes: 6 additions & 0 deletions frontend/src/context/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useContext } from "react";
import { AuthContext } from "./AuthContext.tsx";

export default function useAuth() {
return useContext(AuthContext);
}
7 changes: 7 additions & 0 deletions frontend/src/hooks/api/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import { ConversationId } from "models/Id.ts";

export const Queries = {
Conversation: "Conversation",
User: "User",
} as const;

const basePath = import.meta.env.VITE_APP_BASE_URL;

// Conversation
export const ConversationsPath = `${basePath}/conversation`;
export const getConversationPath = (id: ConversationId) => `${basePath}/conversation/${id}`;
export const getContinueConversationPath = (id: ConversationId) => `${getConversationPath(id)}/continue`;

// User
export const UserPath = `${basePath}/me`;
export const LoginPath = `${basePath}/login`;
export const RegisterPath = `${basePath}/register`;
14 changes: 14 additions & 0 deletions frontend/src/hooks/api/useGetUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import { User } from "models/User.ts";
import useApi from "hooks/useApi.ts";
import { Queries, UserPath } from "./constants.ts";

export default function useGetUser() {
const callApi = useApi();

return useQuery({
queryKey: [Queries.User],
queryFn: () => callApi<User>({ url: UserPath }),
staleTime: Infinity,
});
}
9 changes: 9 additions & 0 deletions frontend/src/hooks/api/useLogout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import useAuth from "context/useAuth.ts";

export default function useLogout() {
const { setToken } = useAuth();

return () => {
setToken(null);
};
}
5 changes: 4 additions & 1 deletion frontend/src/hooks/useApi.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useCallback } from "react";
import { api, ApiRequest } from "utils/api.ts";
import useAuth from "context/useAuth.ts";

type CallApiArgs = Omit<ApiRequest, "token">;

export default function useApi() {
return useCallback(async <TData>(args: CallApiArgs) => api<TData>(args), []);
const { token } = useAuth();

return useCallback(async <TData>(args: CallApiArgs) => api<TData>({ ...args, token }), [token]);
}
22 changes: 22 additions & 0 deletions frontend/src/hooks/useLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useMutation } from "@tanstack/react-query";
import useAuth from "context/useAuth.ts";
import useApi from "./useApi.ts";
import { LoginPath } from "./api/constants.ts";

interface LoginRequest {
username: string;
password: string;
}

export default function useLogin() {
const callApi = useApi();
const { setToken } = useAuth();

return useMutation({
mutationFn: ({ username, password }: LoginRequest) =>
callApi<{ token: string }>({ url: LoginPath, body: { username, password }, method: "POST" }),
onSuccess: (res) => {
setToken(res.token);
},
});
}
24 changes: 24 additions & 0 deletions frontend/src/hooks/useRegister.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useMutation } from "@tanstack/react-query";
import { RolesType } from "models/User.ts";
import useApi from "./useApi.ts";
import { RegisterPath } from "./api/constants.ts";

interface RegisterRequest {
username: string;
firstName: string;
lastName: string;
role: RolesType;
password: string;
}

export default function useRegister() {
const callApi = useApi();
return useMutation({
mutationFn: ({ username, password, firstName, lastName, role }: RegisterRequest) =>
callApi<{ name: string }>({
url: RegisterPath,
body: { username, password, firstName, lastName, role },
method: "POST",
}),
});
}
13 changes: 13 additions & 0 deletions frontend/src/models/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ObjectValues } from "utils/types/index.ts";

export const Roles = {
FREE: "FREE",
PAID: "PAID",
} as const;

export type RolesType = ObjectValues<typeof Roles>;

export interface User {
username: string;
role: RolesType;
}
Loading