diff --git a/backend-start.sh b/backend-start.sh
index 060fea534..75afb0a29 100755
--- a/backend-start.sh
+++ b/backend-start.sh
@@ -1,3 +1,3 @@
#!/bin/bash
-sbt "~backend/reStart"
+sbt "~backend/reStart" -mem 3000
diff --git a/ui/package.json b/ui/package.json
index df88f6747..aaba10d83 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -19,7 +19,10 @@
"axios": "^0.20.0",
"bootstrap": "^4.5.2",
"formik": "^2.2.0",
+ "fp-ts": "^2.9.5",
"immer": "^7.0.9",
+ "io-ts": "^2.2.14",
+ "io-ts-reporters": "^1.2.2",
"jest-environment-jsdom-sixteen": "^1.0.3",
"noop-ts": "^1.0.3",
"react": "^16.13.1",
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index dcdab59f2..367aa4e4f 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -1,14 +1,24 @@
import React from "react";
import { BrowserRouter } from "react-router-dom";
import Main from "./main/Main/Main";
-import { UserContextProvider } from "./contexts/UserContext/UserContext";
+import {initialUserState, UserReducer, UserContext, matchUserState} from "./contexts/UserContext/UserContext";
+import {some} from "fp-ts/Option";
-const App: React.FC = () => (
-
-
-
-
-
-);
+const App: React.FC = () => {
+ const [state, dispatch] = React.useReducer(UserReducer, initialUserState);
+
+ return (
+
+ {matchUserState({
+ LoggedIn: userLoggedIn => (
+
+ ),
+ LoggedOut: () => logged out
,
+ Unknown: () => unknown
,
+ Initial: () => initial
+ })}
+
+ );
+}
export default App;
diff --git a/ui/src/contexts/UserContext/UserContext.tsx b/ui/src/contexts/UserContext/UserContext.tsx
index e7964e724..d0df4fc05 100644
--- a/ui/src/contexts/UserContext/UserContext.tsx
+++ b/ui/src/contexts/UserContext/UserContext.tsx
@@ -1,66 +1,99 @@
import React from "react";
import immer from "immer";
import noop from "noop-ts";
+import { ApiKey, UserDetails } from "../../services/UserService/UserServiceFP";
+import { none, Option } from "fp-ts/es6/Option";
+import {some} from "fp-ts/Option";
-export interface UserDetails {
- createdOn: string;
- email: string;
- login: string;
+type LoginState = "logged_in" | "logged_out" | "unknown" | "initial";
+
+interface UserLoginState {
+ tag: T;
+}
+
+interface UserLoggedIn extends UserLoginState<"logged_in"> {
+ apiKey: ApiKey;
+ user: UserDetails;
+}
+
+interface UserLoginStateUnknown extends UserLoginState<"unknown">{
+ apiKey: ApiKey;
}
-export interface UserState {
- apiKey: string | null;
- user: UserDetails | null;
- loggedIn: boolean | null;
+type UserLoggedOut = UserLoginState<"logged_out">;
+
+type InitialLoginState = UserLoginState<"initial">;
+
+export type UserState = UserLoggedIn | UserLoggedOut | UserLoginStateUnknown | InitialLoginState;
+
+const isUserLoggedIn = (state: UserState): state is UserLoggedIn => "apiKey" in state && "user" in state;
+const isUserLoginStateUnknown = (state: UserState): state is UserLoginStateUnknown => "apiKey" in state && state.tag === "unknown";
+const isUserLoggedOut = (state: UserState): state is UserLoggedOut => "tag" in state && state.tag === "logged_out";
+const isUserLoginStateInitial = (state: UserState): state is InitialLoginState => "tag" in state && state.tag === "initial";
+
+interface UserStateMatcher {
+ LoggedIn: (state: UserLoggedIn) => T;
+ LoggedOut: (state: UserLoggedOut) => T;
+ Unknown: (state: UserLoginStateUnknown) => T;
+ Initial: (state: InitialLoginState) => T;
}
-export const initialUserState: UserState = {
- apiKey: null,
- user: null,
- loggedIn: null,
+export const matchUserState = (matcher: UserStateMatcher) => (state: UserState): T => {
+ if(isUserLoggedIn(state)) {
+ return matcher.LoggedIn(state);
+ }
+ if(isUserLoginStateUnknown(state)) {
+ return matcher.Unknown(state);
+ }
+ if(isUserLoginStateInitial(state)) {
+ return matcher.Initial(state);
+ }
+ return matcher.LoggedOut(state);
};
+
+export const initialUserState: UserState = { tag: "initial" };
+
export type UserAction =
- | { type: "SET_API_KEY"; apiKey: string | null }
+ | { type: "SET_LOGIN_STATUS_UNKNOWN"; apiKey: ApiKey }
| { type: "UPDATE_USER_DATA"; user: Partial }
- | { type: "LOG_IN"; user: UserDetails }
+ | { type: "LOG_IN"; payload: UserLoggedIn }
| { type: "LOG_OUT" };
-const UserReducer = (state: UserState, action: UserAction): UserState => {
+export const UserReducer = (state: UserState, action: UserAction): UserState => {
switch (action.type) {
- case "SET_API_KEY":
- return immer(state, (draftState) => {
- draftState.apiKey = action.apiKey;
+ case "SET_LOGIN_STATUS_UNKNOWN":
+ return immer(state, () => {
+ const userState: UserState = { tag: "unknown", apiKey: action.apiKey };
+ return userState;
});
case "UPDATE_USER_DATA":
return immer(state, (draftState) => {
- if (!draftState.user) return;
- draftState.user = { ...draftState.user, ...action.user };
+ if (isUserLoggedIn(draftState)) {
+ return ({ ...draftState, user: {
+ ...draftState.user,
+ ...action.user
+ }})
+ } else {
+ return state;
+ }
});
case "LOG_IN":
return immer(state, (draftState) => {
- draftState.user = action.user;
- draftState.loggedIn = true;
+ return { user: action.payload.user, apiKey: action.payload.apiKey, tag: "logged_in" };
});
case "LOG_OUT":
return immer(state, (draftState) => {
- draftState.apiKey = null;
- draftState.user = null;
- draftState.loggedIn = false;
+ return { tag: "logged_out"};
});
}
};
+type UserContextData = Omit;
export const UserContext = React.createContext<{
- state: UserState;
- dispatch: React.Dispatch;
+ user: Option;
+ dispatch: React.Dispatch,
}>({
- state: initialUserState,
+ user: none,
dispatch: noop,
});
-
-export const UserContextProvider: React.FC = ({ children }) => {
- const [state, dispatch] = React.useReducer(UserReducer, initialUserState);
-
- return {children};
-};
diff --git a/ui/src/main/Main/Main.tsx b/ui/src/main/Main/Main.tsx
index e257d1e9e..c9b08f93f 100644
--- a/ui/src/main/Main/Main.tsx
+++ b/ui/src/main/Main/Main.tsx
@@ -3,32 +3,25 @@ import Footer from "../Footer/Footer";
import Top from "../Top/Top";
import ForkMe from "../ForkMe/ForkMe";
import { UserContext } from "../../contexts/UserContext/UserContext";
-import Loader from "../Loader/Loader";
import Routes from "../Routes/Routes";
import useLoginOnApiKey from "./useLoginOnApiKey";
import useLocalStoragedApiKey from "./useLocalStoragedApiKey";
const Main: React.FC = () => {
const {
- state: { loggedIn },
+ user,
} = React.useContext(UserContext);
useLocalStoragedApiKey();
useLoginOnApiKey();
- if (loggedIn === null) {
- return ;
- }
-
- return (
- <>
-
-
-
-
-
- >
- );
+ return <>
+
+
+
+
+
+ >
};
export default Main;
diff --git a/ui/src/main/Main/useLocalStoragedApiKey.tsx b/ui/src/main/Main/useLocalStoragedApiKey.tsx
index 4a6105ad6..ed41bfe85 100644
--- a/ui/src/main/Main/useLocalStoragedApiKey.tsx
+++ b/ui/src/main/Main/useLocalStoragedApiKey.tsx
@@ -1,37 +1,35 @@
import React from "react";
import { UserContext } from "../../contexts/UserContext/UserContext";
+import {fold, fromNullable, map, some} from "fp-ts/Option";
+import { pipe } from "fp-ts/pipeable";
-const useLocalStoragedApiKey = () => {
+const useLocalStorageApiKey = () => {
const {
dispatch,
- state: { apiKey, loggedIn },
+ user,
} = React.useContext(UserContext);
- const apiKeyRef = React.useRef(apiKey);
-
React.useEffect(() => {
- apiKeyRef.current = apiKey;
- }, [apiKey, dispatch]);
+ pipe(
+ user,
+ map(({ apiKey }) => apiKey),
+ fold(
+ () => localStorage.removeItem('apiKey'),
+ key => localStorage.setItem('apiKey', key),
+ )
+ )
+ }, [user]);
React.useEffect(() => {
- const storedApiKey = localStorage.getItem("apiKey");
-
- if (!storedApiKey) return dispatch({ type: "LOG_OUT" });
-
- dispatch({ type: "SET_API_KEY", apiKey: storedApiKey });
+ pipe(
+ fromNullable(localStorage.getItem("apiKey")),
+ fold(
+ () => dispatch({ type: 'LOG_OUT' }),
+ apiKey => dispatch({ type: 'SET_LOGIN_STATUS_UNKNOWN', apiKey })
+ )
+ );
}, [dispatch]);
- React.useEffect(() => {
- switch (loggedIn) {
- case true:
- return localStorage.setItem("apiKey", apiKeyRef.current || "");
- case false:
- return localStorage.removeItem("apiKey");
- case null:
- default:
- return;
- }
- }, [loggedIn]);
};
export default useLocalStoragedApiKey;
diff --git a/ui/src/main/Main/useLoginOnApiKey.tsx b/ui/src/main/Main/useLoginOnApiKey.tsx
index ce9f4fc22..033ac8d2c 100644
--- a/ui/src/main/Main/useLoginOnApiKey.tsx
+++ b/ui/src/main/Main/useLoginOnApiKey.tsx
@@ -1,21 +1,29 @@
import React from "react";
import { UserContext } from "../../contexts/UserContext/UserContext";
import userService from "../../services/UserService/UserService";
+import { pipe } from "fp-ts/pipeable";
+import { map, some } from "fp-ts/Option";
const useLoginOnApiKey = () => {
const {
dispatch,
- state: { apiKey },
+ user,
} = React.useContext(UserContext);
React.useEffect(() => {
- if (!apiKey) return;
+ console.log('useLoginOnApiKey');
+ pipe(
+ user,
+ map(({ apiKey }) => {
+ userService
+ .getCurrentUser(apiKey)
+ .then((user) => dispatch({ type: "LOG_IN", payload: { apiKey, tag: "logged_in", user: user } }))
+ .catch(() => dispatch({ type: "LOG_OUT" }));
+ })
+ )
- userService
- .getCurrentUser(apiKey)
- .then((user) => dispatch({ type: "LOG_IN", user }))
- .catch(() => dispatch({ type: "LOG_OUT" }));
- }, [apiKey, dispatch]);
+
+ }, [user, dispatch]);
};
export default useLoginOnApiKey;
diff --git a/ui/src/main/Routes/ProtectedRoute.tsx b/ui/src/main/Routes/ProtectedRoute.tsx
index 3c0626a6b..56e36be88 100644
--- a/ui/src/main/Routes/ProtectedRoute.tsx
+++ b/ui/src/main/Routes/ProtectedRoute.tsx
@@ -2,15 +2,21 @@ import React from "react";
import { Route, RouteProps } from "react-router-dom";
import { UserContext } from "../../contexts/UserContext/UserContext";
import Login from "../../pages/Login/Login";
+import { fold } from "fp-ts/Option";
+import { pipe } from "fp-ts/pipeable";
const ProtectedRoute: React.FC = ({ children, ...props }) => {
const {
- state: { loggedIn },
+ state: { user },
} = React.useContext(UserContext);
- if (!loggedIn) return ;
-
- return {children};
+ return pipe(
+ user,
+ fold(
+ () => ,
+ _ => {children}
+ )
+ );
};
export default ProtectedRoute;
diff --git a/ui/src/main/Top/Top.tsx b/ui/src/main/Top/Top.tsx
index b4276e535..e62596be7 100644
--- a/ui/src/main/Top/Top.tsx
+++ b/ui/src/main/Top/Top.tsx
@@ -4,12 +4,13 @@ import Nav from "react-bootstrap/Nav";
import Container from "react-bootstrap/Container";
import { LinkContainer } from "react-router-bootstrap";
import { UserContext } from "../../contexts/UserContext/UserContext";
-import { BiPowerOff, BiHappy } from "react-icons/bi";
+import { BiHappy, BiPowerOff } from "react-icons/bi";
+import { pipe } from "fp-ts/pipeable";
const Top: React.FC = () => {
const {
- state: { user, loggedIn },
- dispatch,
+ user,
+ dispatch
} = React.useContext(UserContext);
const handleLogOut = () => dispatch({ type: "LOG_OUT" });
@@ -30,29 +31,31 @@ const Top: React.FC = () => {
Home
- {loggedIn ? (
- <>
-
-
-
- {user?.login}
+ {pipe(
+ user,
+ () => (
+ <>
+
+ Register
+
+
+ Login
+
+ >),
+ u => (
+ <>
+
+
+
+ {}
+
+ {" "}
+
+
+ Logout
- {" "}
-
-
- Logout
-
- >
- ) : (
- <>
-
- Register
-
-
- Login
-
- >
- )}
+ >))
+ }
diff --git a/ui/src/pages/Login/Login.test.tsx b/ui/src/pages/Login/Login.test.tsx
index f0e52e766..53aa8e8ed 100644
--- a/ui/src/pages/Login/Login.test.tsx
+++ b/ui/src/pages/Login/Login.test.tsx
@@ -18,7 +18,7 @@ beforeEach(() => {
test("renders header", () => {
const { getByText } = render(
-
+
@@ -30,7 +30,7 @@ test("renders header", () => {
test("redirects when logged in", () => {
render(
-
+
@@ -73,7 +73,7 @@ test("handles login error", async () => {
const { getByLabelText, getByText, findByRole } = render(
-
+
diff --git a/ui/src/pages/Login/Login.tsx b/ui/src/pages/Login/Login.tsx
index aa349fe6d..3d93f8ffa 100644
--- a/ui/src/pages/Login/Login.tsx
+++ b/ui/src/pages/Login/Login.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { Link, Redirect } from "react-router-dom";
-import { Formik, Form as FormikForm } from "formik";
+import { Form as FormikForm, Formik } from "formik";
import * as Yup from "yup";
import userService from "../../services/UserService/UserService";
import Form from "react-bootstrap/Form";
@@ -12,6 +12,8 @@ import { BiLogInCircle } from "react-icons/bi";
import { usePromise } from "react-use-promise-matcher";
import FormikInput from "../../parts/FormikInput/FormikInput";
import FeedbackButton from "../../parts/FeedbackButton/FeedbackButton";
+import { fold, some } from "fp-ts/Option";
+import { pipe } from "fp-ts/pipeable";
interface LoginParams {
loginOrEmail: string;
@@ -26,42 +28,44 @@ const validationSchema: Yup.ObjectSchema = Yup.object({
const Login: React.FC = () => {
const {
dispatch,
- state: { loggedIn },
+ state: { user },
} = React.useContext(UserContext);
const [result, send, clear] = usePromise((values: LoginParams) =>
userService
.login(values)
- .then(({ apiKey }) => dispatch({ type: "SET_API_KEY", apiKey }))
+ .then(({ apiKey }) => dispatch({ type: "SET_API_KEY", apiKey: some(apiKey) }))
);
- if (loggedIn) return ;
-
- return (
-
-
-
- Please sign in
-
- initialValues={{
- loginOrEmail: "",
- password: "",
- }}
- onSubmit={send}
- validationSchema={validationSchema}
- >
-
-
-
-
-
+ return pipe(
+ user,
+ fold(
+ () =>
+
+
+ Please sign in
+
+ initialValues={{
+ loginOrEmail: "",
+ password: "",
+ }}
+ onSubmit={send}
+ validationSchema={validationSchema}
+ >
+
+
+
+
+ ,
+ _ =>
+ )
);
};
diff --git a/ui/src/pages/Profile/PasswordDetails.tsx b/ui/src/pages/Profile/PasswordDetails.tsx
index 78e8ee016..c5fd12e4d 100644
--- a/ui/src/pages/Profile/PasswordDetails.tsx
+++ b/ui/src/pages/Profile/PasswordDetails.tsx
@@ -1,7 +1,7 @@
import React from "react";
import { Formik, Form as FormikForm } from "formik";
import * as Yup from "yup";
-import userService from "../../services/UserService/UserService";
+import userService from "../../services/UserService/UserServiceFP";
import Form from "react-bootstrap/Form";
import Container from "react-bootstrap/Container";
import Col from "react-bootstrap/Col";
@@ -32,7 +32,7 @@ const ProfileDetails: React.FC = () => {
} = React.useContext(UserContext);
const [result, send, clear] = usePromise(({ currentPassword, newPassword }: PasswordDetailsParams) =>
- userService.changePassword(apiKey, { currentPassword, newPassword })
+ userService.changePassword({ currentPassword, newPassword })
);
return (
diff --git a/ui/src/pages/Profile/ProfileDetails.tsx b/ui/src/pages/Profile/ProfileDetails.tsx
index 55c67fb5a..ac59c447b 100644
--- a/ui/src/pages/Profile/ProfileDetails.tsx
+++ b/ui/src/pages/Profile/ProfileDetails.tsx
@@ -1,7 +1,7 @@
import React from "react";
-import { Formik, Form as FormikForm } from "formik";
+import { Form as FormikForm, Formik } from "formik";
import * as Yup from "yup";
-import userService from "../../services/UserService/UserService";
+import userServiceFP from "../../services/UserService/UserServiceFP";
import Form from "react-bootstrap/Form";
import Container from "react-bootstrap/Container";
import Col from "react-bootstrap/Col";
@@ -11,6 +11,8 @@ import { BiArrowFromBottom } from "react-icons/bi";
import { usePromise } from "react-use-promise-matcher";
import FormikInput from "../../parts/FormikInput/FormikInput";
import FeedbackButton from "../../parts/FeedbackButton/FeedbackButton";
+import { getOrElse } from 'fp-ts/Option';
+import { pipe } from "fp-ts/pipeable";
interface ProfileDetailsParams {
login: string;
@@ -25,11 +27,11 @@ const validationSchema: Yup.ObjectSchema = Yup
const ProfileDetails: React.FC = () => {
const {
dispatch,
- state: { apiKey, user },
+ state: { user },
} = React.useContext(UserContext);
const [result, send, clear] = usePromise((values: ProfileDetailsParams) =>
- userService.changeProfileDetails(apiKey, values).then(() => dispatch({ type: "UPDATE_USER_DATA", user: values }))
+ userServiceFP.changeProfileDetails(values).then((value) => dispatch({ type: "UPDATE_USER_DATA", user: values }))
);
return (
@@ -38,10 +40,12 @@ const ProfileDetails: React.FC = () => {
Profile details
- initialValues={{
- login: user?.login || "",
- email: user?.email || "",
- }}
+ initialValues={
+ pipe(
+ user,
+ getOrElse(() => ({ login: '', email: '' })),
+ )
+ }
onSubmit={send}
validationSchema={validationSchema}
>
diff --git a/ui/src/pages/Register/Register.test.tsx b/ui/src/pages/Register/Register.test.tsx
index d4a79f699..fbe04555b 100644
--- a/ui/src/pages/Register/Register.test.tsx
+++ b/ui/src/pages/Register/Register.test.tsx
@@ -18,7 +18,7 @@ beforeEach(() => {
test("renders header", () => {
const { getByText } = render(
-
+
@@ -30,7 +30,7 @@ test("renders header", () => {
test("redirects when registered", () => {
render(
-
+
@@ -46,7 +46,7 @@ test("handles register success", async () => {
const { getByLabelText, getByText, findByRole } = render(
-
+
@@ -81,7 +81,7 @@ test("handles register error", async () => {
const { getByLabelText, getByText, findByRole } = render(
-
+
diff --git a/ui/src/pages/Register/Register.tsx b/ui/src/pages/Register/Register.tsx
index 9f91bce8a..e40637fcb 100644
--- a/ui/src/pages/Register/Register.tsx
+++ b/ui/src/pages/Register/Register.tsx
@@ -1,8 +1,8 @@
import React from "react";
import { Redirect } from "react-router-dom";
-import { Formik, Form as FormikForm } from "formik";
+import { Form as FormikForm, Formik } from "formik";
import * as Yup from "yup";
-import userService from "../../services/UserService/UserService";
+import userService from "../../services/UserService/UserServiceFP";
import Form from "react-bootstrap/Form";
import Container from "react-bootstrap/Container";
import Col from "react-bootstrap/Col";
@@ -12,6 +12,8 @@ import { BiUserPlus } from "react-icons/bi";
import { usePromise } from "react-use-promise-matcher";
import FormikInput from "../../parts/FormikInput/FormikInput";
import FeedbackButton from "../../parts/FeedbackButton/FeedbackButton";
+import { pipe } from "fp-ts/pipeable";
+import { fold, some } from "fp-ts/Option";
interface RegisterParams {
login: string;
@@ -32,43 +34,48 @@ const validationSchema: Yup.ObjectSchema = Yup.objec
const Register: React.FC = () => {
const {
dispatch,
- state: { loggedIn },
+ state: { user },
} = React.useContext(UserContext);
+ // TODO: usePromise takes a function that works as a loader function. It's a function that makes an asynchronous computation
const [result, send, clear] = usePromise(({ login, email, password }: RegisterParams) =>
- userService.registerUser({ login, email, password }).then(({ apiKey }) => dispatch({ type: "SET_API_KEY", apiKey }))
+ userService
+ .registerUser({ login, email, password })
+ .then((value) => dispatch({ type: "SET_API_KEY", apiKey: some('') }))
);
- if (loggedIn) return ;
+ return pipe(
+ user,
+ fold(
+ () =>
+
+
+ Please sign up
+
+ initialValues={{
+ login: "",
+ email: "",
+ password: "",
+ repeatedPassword: "",
+ }}
+ onSubmit={send}
+ validationSchema={validationSchema}
+ >
+
+
+
+
+ ,
+ _ =>
+ )
+ )
};
export default Register;
diff --git a/ui/src/services/AxiosService/AxiosService.ts b/ui/src/services/AxiosService/AxiosService.ts
new file mode 100644
index 000000000..007bde183
--- /dev/null
+++ b/ui/src/services/AxiosService/AxiosService.ts
@@ -0,0 +1,69 @@
+import { Errors, Type } from "io-ts";
+import * as E from 'fp-ts/Either';
+import { pipe } from "fp-ts/pipeable";
+import reporter from 'io-ts-reporters';
+import axios, { AxiosRequestConfig } from "axios";
+
+export async function fetchJson(
+ url: string,
+ validator: Type,
+ config: AxiosSecuredRequestConfig = { securedRequest: true },
+): Promise> {
+ try {
+ const response = await axios.get(`${url}`, config);
+ const json: I = await response.data;
+ const result: E.Either = validator.decode(json);
+
+ return pipe(
+ result,
+ E.fold(
+ () => {
+ const messages = reporter.report(result);
+ return E.left(new Error(messages.join('\n')));
+ },
+ (value: T) => E.right(value)),
+ )
+ } catch (err) {
+ return Promise.resolve(E.left(err))
+ }
+}
+
+export async function sendJson(
+ url: string,
+ validator: Type,
+ data: T,
+ config: AxiosSecuredRequestConfig = { securedRequest: true },
+): Promise> {
+ try {
+ const response = await axios.post(`${url}`, data, config);
+ const json: I = await response.data;
+ const result: E.Either = validator.decode(json);
+
+ return pipe(
+ result,
+ E.fold(
+ () => {
+ const messages = reporter.report(result);
+ return E.left(new Error(messages.join('\n')));
+ },
+ (value: U) => E.right(value)
+ )
+ )
+ } catch (err) {
+ return Promise.resolve(E.left(err))
+ }
+}
+
+type AxiosSecuredRequestConfig = AxiosRequestConfig & { securedRequest?: boolean };
+
+const _securedRequest = (config: AxiosSecuredRequestConfig) => config.securedRequest;
+
+const requestHandler = (request: AxiosRequestConfig) => {
+ if (_securedRequest(request)) {
+ const apiKey = localStorage.getItem('apiKey');
+ request.headers['Authorization'] = `Bearer ${apiKey}`;
+ }
+ return request;
+};
+
+axios.interceptors.request.use(request => requestHandler(request));
diff --git a/ui/src/services/UserService/UserServiceFP.ts b/ui/src/services/UserService/UserServiceFP.ts
new file mode 100644
index 000000000..ea46c6822
--- /dev/null
+++ b/ui/src/services/UserService/UserServiceFP.ts
@@ -0,0 +1,42 @@
+import * as IO from 'io-ts';
+import { fetchJson, sendJson } from "../AxiosService/AxiosService";
+import * as E from 'fp-ts/Either';
+
+const context = "api/v1/user";
+
+const ApiKeyValidator = IO.string;
+
+const UserDetailsValidator = IO.type({
+ createdOn: IO.string,
+ email: IO.string,
+ login: IO.string,
+});
+
+export type UserDetails = IO.TypeOf;
+
+export type ApiKey = IO.TypeOf;
+
+const EmptyValidator = IO.partial({});
+
+const registerUser = (params: { login: string, email: string, password: string }): Promise> =>
+ sendJson(`${context}/register`, ApiKeyValidator, params);
+
+const login = (params: { login: string, email: string, password: string }): Promise> =>
+ sendJson(`${context}/login`, ApiKeyValidator, { ...params, apiKeyValidHours: 1 });
+
+const getCurrentUser = (apiKey: string): Promise> =>
+ fetchJson(`${context}`, UserDetailsValidator);
+
+const changeProfileDetails = (params: { email: string, login: string }): Promise> =>
+ sendJson(`${context}`, EmptyValidator, params);
+
+const changePassword = (params: { currentPassword: string, newPassword: string }): Promise> =>
+ sendJson(`${context}/changepassword`, EmptyValidator, params);
+
+export default {
+ registerUser,
+ login,
+ getCurrentUser,
+ changeProfileDetails,
+ changePassword,
+};
diff --git a/ui/yarn.lock b/ui/yarn.lock
index a2fb6e7f1..86de9c794 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -4934,6 +4934,11 @@ forwarded@~0.1.2:
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+fp-ts@^2.9.5:
+ version "2.9.5"
+ resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.9.5.tgz#6690cd8b76b84214a38fc77cbbbd04a38f86ea90"
+ integrity sha512-MiHrA5teO6t8zKArE3DdMPT/Db6v2GUt5yfWnhBTrrsVfeCJUUnV6sgFvjGNBKDmEMqVwRFkEePL7wPwqrLKKA==
+
fragment-cache@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
@@ -5670,6 +5675,16 @@ invert-kv@^2.0.0:
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
+io-ts-reporters@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/io-ts-reporters/-/io-ts-reporters-1.2.2.tgz#4d3219777ea5219c7d8f6ffac01fd68e72426dd1"
+ integrity sha512-igASwWWkDY757OutNcM6zTtdJf/eTZYkoe2ymsX2qpm5bKZLo74FJYjsCtMQOEdY7dRHLLEulCyFQwdN69GBCg==
+
+io-ts@^2.2.14:
+ version "2.2.14"
+ resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.14.tgz#99405fab547690784d70c54a3048a44711237316"
+ integrity sha512-UWL1mdDe5YI4+/7YlrbsSwKmsECFFlWcVHT2CPGzeNODHj2qY0cibjulYfrfz5SCPoDAsjVP7vFKGcF+L10+SQ==
+
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"