diff --git a/AGHTechDoc.md b/AGHTechDoc.md index c2206449..0522439e 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -50,6 +50,9 @@ Contents: * API: Get filtering parameters * API: Set filtering parameters * API: Set URL parameters +* Log-in page + * API: Log in + * API: Log out ## Relations between subsystems @@ -1097,3 +1100,82 @@ Request: Response: 200 OK + + +## Log-in page + +After user completes the steps of installation wizard, he must log in into dashboard using his name and password. After user successfully logs in, he gets the Cookie which allows the server to authenticate him next time without password. After the Cookie is expired, user needs to perform log-in operation again. All requests without a proper Cookie get redirected to Log-In page with prompt for name and password. + +YAML configuration: + + users: + - name: "..." + password: "..." // bcrypt hash + ... + + +Session DB file: + + session="..." expire=123456 + ... + +Session data is SHA(random()+name+password). +Expiration time is UNIX time when cookie gets expired. + +Any request to server must come with Cookie header: + + GET /... + Cookie: session=... + +If not authenticated, server sends a redirect response: + + 302 Found + Location: /login.html + + +### Reset password + +There is no mechanism to reset the password. Instead, the administrator must use `htpasswd` utility to generate a new hash: + + htpasswd -B -n -b username password + +It will print `username:` to the terminal. `` value may be used in AGH YAML configuration file as a value to `password` setting: + + users: + - name: "..." + password: + + + +### API: Log in + +Perform a log-in operation for administrator. Server generates a session for this name+password pair, stores it in file. UI needs to perform all requests with this value inside Cookie HTTP header. + +Request: + + POST /control/login + + { + name: "..." + password: "..." + } + +Response: + + 200 OK + Set-Cookie: session=...; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Path=/; HttpOnly + + +### API: Log out + +Perform a log-out operation for administrator. Server removes the session from its DB and sets an expired cookie value. + +Request: + + GET /control/logout + +Response: + + 302 Found + Location: /login.html + Set-Cookie: session=...; Expires=Thu, 01 Jan 1970 00:00:00 GMT diff --git a/client/public/login.html b/client/public/login.html new file mode 100644 index 00000000..03179b42 --- /dev/null +++ b/client/public/login.html @@ -0,0 +1,17 @@ + + + + + + + + + Login + + + +
+ + diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 9c59c654..d2d7bfe7 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -199,7 +199,7 @@ "install_settings_dns_desc": "You will need to configure your devices or router to use the DNS server on the following addresses:", "install_settings_all_interfaces": "All interfaces", "install_auth_title": "Authentication", - "install_auth_desc": "It is highly recommended to configure password authentication to your AdGuard Home admin web interface. Even if it is accessible only in your local network, it is still important to have it protected from unrestricted access.", + "install_auth_desc": "It is highly recommended to configure password authentication to your AdGuard Home admin web interface. Even if it is accessible only in your local network, it is still important to protect it from unrestricted access.", "install_auth_username": "Username", "install_auth_password": "Password", "install_auth_confirm": "Confirm password", @@ -384,5 +384,13 @@ "filters_configuration": "Filters configuration", "filters_enable": "Enable filters", "filters_interval": "Filters update interval", - "disabled": "Disabled" + "disabled": "Disabled", + "username_label": "Username", + "username_placeholder": "Enter username", + "password_label": "Password", + "password_placeholder": "Enter password", + "sign_in": "Sign in", + "sign_out": "Sign out", + "forgot_password": "Forgot password?", + "forgot_password_desc": "Please follow <0>these steps to create a new password for your user account." } diff --git a/client/src/__locales/ru.json b/client/src/__locales/ru.json index 88799f70..322d05f0 100644 --- a/client/src/__locales/ru.json +++ b/client/src/__locales/ru.json @@ -352,10 +352,17 @@ "unblock_all": "Разблокировать все", "domain": "Домен", "answer": "Ответ", + "interval_24_hour": "24 часа", "interval_hours_0": "{{count}} час", "interval_hours_1": "{{count}} часа", "interval_hours_2": "{{count}} часов", "interval_days_0": "{{count}} день", "interval_days_1": "{{count}} дня", - "interval_days_2": "{{count}} дней" + "interval_days_2": "{{count}} дней", + "for_last_days_0": "за последний {{count}} день", + "for_last_days_1": "за последние {{count}} дня", + "for_last_days_2": "за последние {{count}} дней", + "number_of_dns_query_days_0": "Количество DNS-запросов за {{count}} день", + "number_of_dns_query_days_1": "Количество DNS-запросов за {{count}} дня", + "number_of_dns_query_days_2": "Количество DNS-запросов за {{count}} дней" } \ No newline at end of file diff --git a/client/src/actions/login.js b/client/src/actions/login.js new file mode 100644 index 00000000..90cc0780 --- /dev/null +++ b/client/src/actions/login.js @@ -0,0 +1,20 @@ +import { createAction } from 'redux-actions'; + +import { addErrorToast } from './index'; +import apiClient from '../api/Api'; + +export const processLoginRequest = createAction('PROCESS_LOGIN_REQUEST'); +export const processLoginFailure = createAction('PROCESS_LOGIN_FAILURE'); +export const processLoginSuccess = createAction('PROCESS_LOGIN_SUCCESS'); + +export const processLogin = values => async (dispatch) => { + dispatch(processLoginRequest()); + try { + await apiClient.login(values); + window.location.replace(window.location.origin); + dispatch(processLoginSuccess()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(processLoginFailure()); + } +}; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 187b7312..b7a7d045 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -510,6 +510,18 @@ class Api { const { path, method } = this.QUERY_LOG_CLEAR; return this.makeRequest(path, method); } + + // Login + LOGIN = { path: 'login', method: 'POST' }; + + login(data) { + const { path, method } = this.LOGIN; + const config = { + data, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, config); + } } const apiClient = new Api(); diff --git a/client/src/components/App/index.js b/client/src/components/App/index.js index 6489649c..3fd4d1a5 100644 --- a/client/src/components/App/index.js +++ b/client/src/components/App/index.js @@ -64,7 +64,7 @@ class App extends Component { }; render() { - const { dashboard, encryption } = this.props; + const { dashboard, encryption, getVersion } = this.props; const updateAvailable = dashboard.isCoreRunning && dashboard.isUpdateAvailable; return ( @@ -109,7 +109,12 @@ class App extends Component { )} -