diff --git a/webapp_frontend/src/background/api/api.ts b/webapp_frontend/src/background/api/api.ts index 196f0c5c..2d156f5e 100644 --- a/webapp_frontend/src/background/api/api.ts +++ b/webapp_frontend/src/background/api/api.ts @@ -3,6 +3,8 @@ import {constants} from "../constants"; export const hostname:string =constants.url.API_URL+'/api'; +export const userPath:string='/v1/users'; + interface BackendHealthData { uptimeInSeconds: number; @@ -21,4 +23,4 @@ function callBackendHealth():Promise{ }); } -export { callBackendHealth} \ No newline at end of file +export { callBackendHealth} diff --git a/webapp_frontend/src/background/api/auth.ts b/webapp_frontend/src/background/api/auth.ts new file mode 100644 index 00000000..6bb3fbdb --- /dev/null +++ b/webapp_frontend/src/background/api/auth.ts @@ -0,0 +1,76 @@ +import Axios from "axios"; + + +import {hostname, userPath} from "./api"; + +import {AddUser, UserState} from "../redux/actions/userTypes"; +import store from "../redux/store"; +import {addAccessToken, addRefreshToken} from "../redux/actions/tokens"; +import {addUser} from "../redux/actions/user"; +import {AccessToken, AddAccessToken, AddRefreshToken, TokensState} from "../redux/actions/tokenTypes"; + + +// reference: https://daveceddia.com/access-redux-store-outside-react/ + +export interface BackendLoginData { + refreshToken: string, + user: UserState + +} + +export const loginWithUsernameAndPassword = (userName: string, password: string): Promise => { + + return new Promise((resolve, reject) => { + let config = { + headers: { + Authorization: `Basic ${btoa(userName + ':' + password)}`, + }, + }; + + return Axios.get(hostname + userPath + '/login', config) + .then((data) => { + store.dispatch(addRefreshToken(data.data.refreshToken) as AddRefreshToken) + store.dispatch(addUser(data.data.user as UserState) as AddUser) + + getAccessTokenWithRefreshToken() + }) + .catch(((error) => { + + reject(error); + })) + + + }) +} + + +export const getAccessTokenWithRefreshToken = () => { + console.log("getAccessTokenWithRefreshToken") + + let refreshToken: string|null = (store.getState().tokens as TokensState).refreshToken; + + let config = { + headers: { + Authorization: `Bearer ${refreshToken}`, + }, + }; + + Axios.get(hostname + userPath + '/auth', config) + .then((data) => { + setAuthHeaderToAxios(data.data.token) + + store.dispatch(addAccessToken({token: data.data.token, timestamp: data.data.validUntil}as AccessToken) as AddAccessToken); + + }) + .catch(((error) => { + console.log(error) + })); + +} + +function setAuthHeaderToAxios(accessToken: string) { + Axios.defaults.headers.common['Authorization'] = + `Bearer ${accessToken}`; +} + + diff --git a/webapp_frontend/src/background/api/login.ts b/webapp_frontend/src/background/api/login.ts deleted file mode 100644 index 92bbd6a2..00000000 --- a/webapp_frontend/src/background/api/login.ts +++ /dev/null @@ -1,7 +0,0 @@ - - -function test(){ - return null -} - -export default test; \ No newline at end of file diff --git a/webapp_frontend/src/background/constants.tsx b/webapp_frontend/src/background/constants.tsx index 1de2e26d..0d9a78d0 100644 --- a/webapp_frontend/src/background/constants.tsx +++ b/webapp_frontend/src/background/constants.tsx @@ -11,7 +11,7 @@ const prod: constants = { const dev: constants = { url: { - API_URL: 'http://filefighter.ddns.net:3001', + API_URL: 'https://cors-anywhere.herokuapp.com/http://filefighter.ddns.net:3001', } }; -export const constants = process.env.NODE_ENV === 'development' ? dev : prod; \ No newline at end of file +export const constants = process.env.NODE_ENV === 'development' ? dev : prod; diff --git a/webapp_frontend/src/background/redux/actions/sytemState.tsx b/webapp_frontend/src/background/redux/actions/sytemState.tsx index 91c66188..a5888e73 100644 --- a/webapp_frontend/src/background/redux/actions/sytemState.tsx +++ b/webapp_frontend/src/background/redux/actions/sytemState.tsx @@ -1,5 +1,7 @@ import {TokensState} from "./tokenTypes"; +import {UserState} from "./userTypes"; export interface SystemState { tokens: TokensState -} \ No newline at end of file + user: UserState +} diff --git a/webapp_frontend/src/background/redux/actions/tokenTypes.tsx b/webapp_frontend/src/background/redux/actions/tokenTypes.tsx index a4958dac..dc4bbe08 100644 --- a/webapp_frontend/src/background/redux/actions/tokenTypes.tsx +++ b/webapp_frontend/src/background/redux/actions/tokenTypes.tsx @@ -1,8 +1,9 @@ export const ADD_REFRESH_TOKEN = "ADD_REFRESH_TOKEN"; export const ADD_ACCESS_TOKEN = "ADD_ACCESS_TOKEN"; +export const CHECKED_COOKIES = "CHECKED_COOKIES"; -export interface AccessToken{ +export interface AccessToken { token: string | null; timestamp: number | null; } @@ -11,18 +12,24 @@ export interface AccessToken{ export interface TokensState { refreshToken: string | null; accessToken: AccessToken | null; + checkedCookies: boolean; } -interface AddRefreshToken { +export interface AddRefreshToken { type: typeof ADD_REFRESH_TOKEN payload: string } -interface AddAccessToken { +export interface AddAccessToken { type: typeof ADD_ACCESS_TOKEN payload: AccessToken } -export type TokenActionsTypes = AddRefreshToken | AddAccessToken; +export interface CheckedCookies{ + type: typeof CHECKED_COOKIES, + payload: boolean +} + +export type TokenActionsTypes = AddRefreshToken | AddAccessToken|CheckedCookies; diff --git a/webapp_frontend/src/background/redux/actions/tokens.tsx b/webapp_frontend/src/background/redux/actions/tokens.tsx index 219fa52f..e0592c0f 100644 --- a/webapp_frontend/src/background/redux/actions/tokens.tsx +++ b/webapp_frontend/src/background/redux/actions/tokens.tsx @@ -1,4 +1,4 @@ -import {ADD_REFRESH_TOKEN, ADD_ACCESS_TOKEN, AccessToken} from "./tokenTypes"; +import {ADD_REFRESH_TOKEN, ADD_ACCESS_TOKEN, AccessToken, CHECKED_COOKIES} from "./tokenTypes"; export const addRefreshToken = (content: string) => ({ @@ -9,4 +9,10 @@ export const addRefreshToken = (content: string) => ({ export const addAccessToken = (content: AccessToken) => ({ type: ADD_ACCESS_TOKEN, payload: content -}); \ No newline at end of file +}); + + +export const checkedCookies = (content: boolean) => ({ + type: CHECKED_COOKIES, + payload: content +}); diff --git a/webapp_frontend/src/background/redux/actions/user.tsx b/webapp_frontend/src/background/redux/actions/user.tsx new file mode 100644 index 00000000..f72b8f62 --- /dev/null +++ b/webapp_frontend/src/background/redux/actions/user.tsx @@ -0,0 +1,6 @@ +import {ADD_USER, UserState} from "./userTypes"; + +export const addUser = (content: UserState) => ({ + type: ADD_USER, + payload: content +}); diff --git a/webapp_frontend/src/background/redux/actions/userTypes.tsx b/webapp_frontend/src/background/redux/actions/userTypes.tsx new file mode 100644 index 00000000..43b2ade9 --- /dev/null +++ b/webapp_frontend/src/background/redux/actions/userTypes.tsx @@ -0,0 +1,18 @@ +export const ADD_USER = "ADD_USER"; + +export interface UserState { + id: number | null, + username: string | null, + groups: number[], + + +} + + +export interface AddUser { + type: typeof ADD_USER, + payload: UserState +} + + +export type UserActionTypes = AddUser; diff --git a/webapp_frontend/src/background/redux/reducers/index.tsx b/webapp_frontend/src/background/redux/reducers/index.tsx index 1f3c1cd6..d1cc483a 100644 --- a/webapp_frontend/src/background/redux/reducers/index.tsx +++ b/webapp_frontend/src/background/redux/reducers/index.tsx @@ -1,5 +1,7 @@ import {combineReducers} from "redux"; import tokens from "./tokens"; +import user from "./user"; // this combines all the stores from the reducers -export default combineReducers({tokens}); + +export default combineReducers({tokens, user}); diff --git a/webapp_frontend/src/background/redux/reducers/tokens.tsx b/webapp_frontend/src/background/redux/reducers/tokens.tsx index 69097ca4..bcb78587 100644 --- a/webapp_frontend/src/background/redux/reducers/tokens.tsx +++ b/webapp_frontend/src/background/redux/reducers/tokens.tsx @@ -1,24 +1,36 @@ -import {ADD_REFRESH_TOKEN, ADD_ACCESS_TOKEN, TokenActionsTypes, TokensState, AccessToken} from "../actions/tokenTypes"; +import {ADD_REFRESH_TOKEN, ADD_ACCESS_TOKEN, TokenActionsTypes, TokensState, AccessToken, CHECKED_COOKIES} from "../actions/tokenTypes"; const initialState: TokensState = { refreshToken: null, accessToken: null, + checkedCookies: false, }; export default function (state = initialState, action: TokenActionsTypes) { switch (action.type) { case ADD_REFRESH_TOKEN: { + console.log('[Redux] adding refreshToken') const refreshToken: string = action.payload; return { refreshToken: refreshToken, - accessToken: initialState.accessToken + accessToken: state.accessToken, + checkedCookies: state.checkedCookies }; } case ADD_ACCESS_TOKEN: { + console.log('[Redux] adding accessToken') const accessToken: AccessToken = action.payload; return { - refreshToken: initialState.refreshToken, - accessToken: accessToken + refreshToken: state.refreshToken, + accessToken: accessToken, + checkedCookies: state.checkedCookies + }; + } + case CHECKED_COOKIES: { + return { + refreshToken: state.refreshToken, + accessToken: state.refreshToken, + checkedCookies: action.payload }; } default: diff --git a/webapp_frontend/src/background/redux/reducers/user.tsx b/webapp_frontend/src/background/redux/reducers/user.tsx new file mode 100644 index 00000000..6b801027 --- /dev/null +++ b/webapp_frontend/src/background/redux/reducers/user.tsx @@ -0,0 +1,18 @@ +import {ADD_USER, UserActionTypes, UserState} from "../actions/userTypes"; + +const initialState: UserState = { + groups: [], id: null, username: null + +}; + + +export default function (state = initialState, action: UserActionTypes) { + switch (action.type) { + case ADD_USER: { + console.log('[Redux] adding user') + return action.payload; + } + default: + return state; + } +} diff --git a/webapp_frontend/src/components/App.tsx b/webapp_frontend/src/components/App.tsx index 2eb68011..10b0594d 100644 --- a/webapp_frontend/src/components/App.tsx +++ b/webapp_frontend/src/components/App.tsx @@ -9,18 +9,22 @@ import PermanentAssets from "./basicElements/PermanentAssets"; import {connect, ConnectedProps} from 'react-redux' -import {addAccessToken, addRefreshToken} from "../background/redux/actions/tokens"; +import {addAccessToken, addRefreshToken, checkedCookies} from "../background/redux/actions/tokens"; import {SystemState} from "../background/redux/actions/sytemState"; +import Login from "./basicElements/Login"; + + // this takes the redux store and maps everything that is needed to the function props const mapState = (state: SystemState) => ({ - tokens: {refreshToken: state.tokens.refreshToken, accessToken: state.tokens.accessToken} + tokens: {refreshToken: state.tokens.refreshToken, accessToken: state.tokens.accessToken, checkedCookies: state.tokens.checkedCookies}, + user: state.user }) // this takes the redux actions and maps them to the props const mapDispatch = { - addRefreshToken, addAccessToken + addRefreshToken, addAccessToken, checkedCookies } const connector = connect(mapState, mapDispatch) @@ -36,20 +40,33 @@ function App(props: Props): ReactElement { console.log("[App] props.tokens: ") console.log(props.tokens.refreshToken) console.log(props.tokens) + console.log(props.user) + + if (props.tokens.checkedCookies) { + + if (props.tokens.refreshToken) { + return ( +
+ +
+ + + +
+ + +
+ ); + } else { + console.log("[APP] showing login"); + return () + } + } else { + props.checkedCookies(true) - return ( -
- -
- - - -
- - -
- ); + return (
Loading
) + } } export default connector(App); diff --git a/webapp_frontend/src/components/basicElements/Login.tsx b/webapp_frontend/src/components/basicElements/Login.tsx index 2aa9c86b..b10c0191 100644 --- a/webapp_frontend/src/components/basicElements/Login.tsx +++ b/webapp_frontend/src/components/basicElements/Login.tsx @@ -1,6 +1,59 @@ -import React, {ReactElement} from 'react'; +import React, {FormEvent, ReactElement, useState} from 'react'; +import {Button, Form, Container, Row,Col} from "react-bootstrap"; +import {loginWithUsernameAndPassword} from "../../background/api/auth"; -export default function Login():ReactElement { - return(
hallo
); + +function Login(): ReactElement { + const [userName, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [stayLoggedIn, setStayLoggedIn] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + loginWithUsernameAndPassword(userName, password) + .then(() => { + //nothing to do here :) + setErrorMessage(""); + }) + .catch(((error) => { + console.log(error); + setErrorMessage(error.response.data.message); + })) + + + }; + + + return ( + + + +
+ + Username + setUsername(event.target.value)}/> + + + + Password + setPassword(event.target.value)}/> + + Contact your administrator if you have forgotten your password. + + + + setStayLoggedIn(!stayLoggedIn)}/> + + +

{errorMessage}

+
+ +
+
); } +export default Login; diff --git a/webapp_frontend/src/style/custom.scss b/webapp_frontend/src/style/custom.scss index 2e592e92..6fdb2183 100644 --- a/webapp_frontend/src/style/custom.scss +++ b/webapp_frontend/src/style/custom.scss @@ -21,4 +21,5 @@ $blue: #1a4965; @import "~bootstrap/scss/grid"; @import "~bootstrap/scss/buttons"; @import "~bootstrap/scss/tables"; -@import "~bootstrap/scss/utilities"; \ No newline at end of file +@import "~bootstrap/scss/utilities"; +@import "~bootstrap/scss/forms";