API & Client - max sizes and max number of files must be greater than 0 - #71

This commit is contained in:
Sam 2021-02-20 16:59:31 +01:00
parent c4e579dd25
commit fbc40385a0
18 changed files with 203 additions and 26 deletions

View File

@ -196,11 +196,11 @@
</dd>
<dt class="field-even">Request JSON Object</dt>
<dd class="field-even"><ul class="simple">
<li><p><strong>gpx_limit_import</strong> (<em>integrer</em>) max number of files in zip archive</p></li>
<li><p><strong>gpx_limit_import</strong> (<em>integer</em>) max number of files in zip archive</p></li>
<li><p><strong>is_registration_enabled</strong> (<em>boolean</em>) is registration enabled ?</p></li>
<li><p><strong>max_single_file_size</strong> (<em>integrer</em>) max size of a single file</p></li>
<li><p><strong>max_zip_file_size</strong> (<em>integrer</em>) max size of a zip archive</p></li>
<li><p><strong>max_users</strong> (<em>integrer</em>) max users allowed to register on instance</p></li>
<li><p><strong>max_single_file_size</strong> (<em>integer</em>) max size of a single file</p></li>
<li><p><strong>max_zip_file_size</strong> (<em>integer</em>) max size of a zip archive</p></li>
<li><p><strong>max_users</strong> (<em>integer</em>) max users allowed to register on instance</p></li>
</ul>
</dd>
<dt class="field-odd">Request Headers</dt>

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,7 @@ from fittrackee.responses import (
from fittrackee.users.decorators import authenticate_as_admin
from .models import AppConfig
from .utils import update_app_config_from_database
from .utils import update_app_config_from_database, verify_app_config
config_blueprint = Blueprint('config', __name__)
@ -96,11 +96,11 @@ def update_application_config(auth_user_id: int) -> Union[Dict, HttpResponse]:
:param integer auth_user_id: authenticate user id (from JSON Web Token)
:<json integrer gpx_limit_import: max number of files in zip archive
:<json integer gpx_limit_import: max number of files in zip archive
:<json boolean is_registration_enabled: is registration enabled ?
:<json integrer max_single_file_size: max size of a single file
:<json integrer max_zip_file_size: max size of a zip archive
:<json integrer max_users: max users allowed to register on instance
:<json integer max_single_file_size: max size of a single file
:<json integer max_zip_file_size: max size of a zip archive
:<json integer max_users: max users allowed to register on instance
:reqheader Authorization: OAuth 2.0 Bearer Token
@ -117,6 +117,10 @@ def update_application_config(auth_user_id: int) -> Union[Dict, HttpResponse]:
if not config_data:
return InvalidPayloadErrorResponse()
ret = verify_app_config(config_data)
if ret:
return InvalidPayloadErrorResponse(message=ret)
try:
config = AppConfig.query.one()
if 'gpx_limit_import' in config_data:

View File

@ -1,5 +1,5 @@
import os
from typing import Tuple
from typing import Dict, List, Tuple
from flask import Flask
@ -49,3 +49,30 @@ def update_app_config_from_database(
current_app.config[
'is_registration_enabled'
] = db_config.is_registration_enabled
def verify_app_config(config_data: Dict) -> List:
"""
Verify if application config is valid.
If not, it returns not empty string
"""
ret = []
if (
'gpx_limit_import' in config_data
and config_data['gpx_limit_import'] <= 0
):
ret.append('Max. files in a zip archive must be greater than 0')
if (
'max_single_file_size' in config_data
and config_data['max_single_file_size'] <= 0
):
ret.append('Max. size of uploaded files must be greater than 0')
if (
'max_zip_file_size' in config_data
and config_data['max_zip_file_size'] <= 0
):
ret.append('Max. size of zip archive must be greater than 0')
return ret

View File

@ -1,8 +1,8 @@
{
"files": {
"main.css": "/static/css/main.376b8924.chunk.css",
"main.js": "/static/js/main.7c5c861a.chunk.js",
"main.js.map": "/static/js/main.7c5c861a.chunk.js.map",
"main.js": "/static/js/main.4387b246.chunk.js",
"main.js.map": "/static/js/main.4387b246.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.1240af94.js",
"runtime-main.js.map": "/static/js/runtime-main.1240af94.js.map",
"static/js/2.301144a0.chunk.js": "/static/js/2.301144a0.chunk.js",
@ -19,6 +19,6 @@
"static/js/runtime-main.1240af94.js",
"static/js/2.301144a0.chunk.js",
"static/css/main.376b8924.chunk.css",
"static/js/main.7c5c861a.chunk.js"
"static/js/main.4387b246.chunk.js"
]
}

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.ico"><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.7/css/fork-awesome.min.css" integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/foundation-icons/3.0/foundation-icons.min.css"><link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""><title>FitTrackee</title><link href="/static/css/main.376b8924.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script><script type="text/javascript">$(document).ready((function(){$("li.nav-item").click((function(){$("button.navbar-toggler").toggleClass("collapsed"),$("#navbarSupportedContent").toggleClass("show")}))}))</script><script>!function(e){function t(t){for(var n,i,l=t[0],f=t[1],a=t[2],p=0,s=[];p<l.length;p++)i=l[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var f=r[l];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/";var l=this.webpackJsonpfittrackee_client=this.webpackJsonpfittrackee_client||[],f=l.push.bind(l);l.push=t,l=l.slice();for(var a=0;a<l.length;a++)t(l[a]);var c=f;r()}([])</script><script src="/static/js/2.301144a0.chunk.js"></script><script src="/static/js/main.7c5c861a.chunk.js"></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.ico"><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.7/css/fork-awesome.min.css" integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/foundation-icons/3.0/foundation-icons.min.css"><link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""><title>FitTrackee</title><link href="/static/css/main.376b8924.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script><script type="text/javascript">$(document).ready((function(){$("li.nav-item").click((function(){$("button.navbar-toggler").toggleClass("collapsed"),$("#navbarSupportedContent").toggleClass("show")}))}))</script><script>!function(e){function t(t){for(var n,i,l=t[0],f=t[1],a=t[2],p=0,s=[];p<l.length;p++)i=l[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var f=r[l];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/";var l=this.webpackJsonpfittrackee_client=this.webpackJsonpfittrackee_client||[],f=l.push.bind(l);l.push=t,l=l.slice();for(var a=0;a<l.length;a++)t(l[a]);var c=f;r()}([])</script><script src="/static/js/2.301144a0.chunk.js"></script><script src="/static/js/main.4387b246.chunk.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -32,7 +32,10 @@ class HttpResponse(Response):
class GenericErrorResponse(HttpResponse):
def __init__(
self, status_code: int, message: str, status: Optional[str] = None
self,
status_code: int,
message: Union[str, List],
status: Optional[str] = None,
) -> None:
response = {
'status': 'error' if status is None else status,
@ -46,7 +49,9 @@ class GenericErrorResponse(HttpResponse):
class InvalidPayloadErrorResponse(GenericErrorResponse):
def __init__(
self, message: Optional[str] = None, status: Optional[str] = None
self,
message: Optional[Union[str, List]] = None,
status: Optional[str] = None,
) -> None:
message = 'Invalid payload.' if message is None else message
super().__init__(status_code=400, message=message, status=status)

View File

@ -276,3 +276,105 @@ class TestUpdateConfig:
'Max. size of zip archive must be equal or greater than max. size '
'of uploaded files'
) in data['message']
def test_it_raises_error_if_archive_max_size_equals_0(
self, app_with_max_single_file_size: Flask, user_1_admin: User
) -> None:
client = app_with_max_single_file_size.test_client()
resp_login = client.post(
'/api/auth/login',
data=json.dumps(
dict(email='admin@example.com', password='12345678')
),
content_type='application/json',
)
response = client.patch(
'/api/config',
content_type='application/json',
data=json.dumps(
dict(
max_zip_file_size=0,
)
),
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
),
)
data = json.loads(response.data.decode())
assert response.status_code == 400
assert 'error' in data['status']
assert (
'Max. size of zip archive must be greater than 0'
in data['message']
)
def test_it_raises_error_if_files_max_size_equals_0(
self, app: Flask, user_1_admin: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
data=json.dumps(
dict(email='admin@example.com', password='12345678')
),
content_type='application/json',
)
response = client.patch(
'/api/config',
content_type='application/json',
data=json.dumps(
dict(
max_single_file_size=0,
)
),
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
),
)
data = json.loads(response.data.decode())
assert response.status_code == 400
assert 'error' in data['status']
assert (
'Max. size of uploaded files must be greater than 0'
in data['message']
)
def test_it_raises_error_if_gpx_limit_import_equals_0(
self, app: Flask, user_1_admin: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
data=json.dumps(
dict(email='admin@example.com', password='12345678')
),
content_type='application/json',
)
response = client.patch(
'/api/config',
content_type='application/json',
data=json.dumps(
dict(
gpx_limit_import=0,
)
),
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
),
)
data = json.loads(response.data.decode())
assert response.status_code == 400
assert 'error' in data['status']
assert (
'Max. files in a zip archive must be greater than 0'
in data['message']
)

View File

@ -11,11 +11,16 @@ from fittrackee.application.utils import update_app_config_from_database
def get_app_config(
with_config: Optional[bool] = False,
max_workouts: Optional[int] = None,
max_single_file_size: Optional[int] = None,
) -> Optional[AppConfig]:
if with_config:
config = AppConfig()
config.gpx_limit_import = 10 if max_workouts is None else max_workouts
config.max_single_file_size = 1 * 1024 * 1024
config.max_single_file_size = (
1 * 1024 * 1024
if max_single_file_size is None
else max_single_file_size
)
config.max_zip_file_size = 1 * 1024 * 1024 * 10
config.max_users = 100
db.session.add(config)
@ -27,6 +32,7 @@ def get_app_config(
def get_app(
with_config: Optional[bool] = False,
max_workouts: Optional[int] = None,
max_single_file_size: Optional[int] = None,
) -> Generator:
app = create_app()
with app.app_context():
@ -64,6 +70,14 @@ def app_with_max_workouts(monkeypatch: pytest.MonkeyPatch) -> Generator:
yield from get_app(with_config=True, max_workouts=2)
@pytest.fixture
def app_with_max_single_file_size(
monkeypatch: pytest.MonkeyPatch,
) -> Generator:
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025')
yield from get_app(with_config=True, max_single_file_size=0)
@pytest.fixture
def app_no_config() -> Generator:
yield from get_app(with_config=False)

View File

@ -1,6 +1,7 @@
import FitTrackeeGenericApi from '../fitTrackeeApi'
import { history } from '../index'
import { setError } from './index'
import { generateIds } from '../utils'
import { emptyMessages, setError } from './index'
export const setAppConfig = data => ({
type: 'SET_APP_CONFIG',
@ -12,6 +13,8 @@ export const setAppStats = data => ({
data,
})
const SetAppErrors = messages => ({ type: 'APP_ERRORS', messages })
export const getAppData = target => dispatch =>
FitTrackeeGenericApi.getData(target)
.then(ret => {
@ -27,14 +30,18 @@ export const getAppData = target => dispatch =>
})
.catch(error => dispatch(setError(`application|${error}`)))
export const updateAppConfig = formData => dispatch =>
export const updateAppConfig = formData => dispatch => {
dispatch(emptyMessages())
FitTrackeeGenericApi.updateData('config', formData)
.then(ret => {
if (ret.status === 'success') {
dispatch(setAppConfig(ret.data))
history.push('/admin/application')
} else if (Array.isArray(ret.message)) {
dispatch(SetAppErrors(generateIds(ret.message)))
} else {
dispatch(setError(`application|${ret.message}`))
dispatch(setError(ret.message))
}
})
.catch(error => dispatch(setError(`application|${error}`)))
}

View File

@ -3,11 +3,16 @@ import i18next from 'i18next'
import FitTrackeeApi from '../fitTrackeeApi/index'
import { history } from '../index'
export const emptyMessages = () => ({
type: 'CLEAN_ALL_MESSAGES',
})
export const setData = (target, data) => ({
type: 'SET_DATA',
data,
target,
})
export const setPaginatedData = (target, data, pagination) => ({
type: 'SET_PAGINATED_DATA',
data,
@ -51,7 +56,7 @@ export const getOrUpdateData = (
dispatch(setLoading(false))
return dispatch(setError(`${target}|Incorrect id`))
}
dispatch(setError(''))
dispatch(emptyMessages())
return FitTrackeeApi[action](target, data)
.then(ret => {
if (ret.status === 'success') {

View File

@ -48,13 +48,16 @@ class AdminApplication extends React.Component {
isInEdition,
loadAppConfig,
message,
messages,
onHandleConfigFormSubmit,
t,
} = this.props
const { formData } = this.state
return (
<div>
{message && <Message message={message} t={t} />}
{(message || messages) && (
<Message message={message} messages={messages} t={t} />
)}
{Object.keys(formData).length > 0 && (
<div className="row">
<div className="col-md-12">
@ -209,6 +212,7 @@ class AdminApplication extends React.Component {
export default connect(
state => ({
message: state.message,
messages: state.messages,
}),
dispatch => ({
loadAppConfig: () => {

View File

@ -14,7 +14,10 @@
"Invalid credentials.": "Invalid credentials.",
"Invalid payload.": "Invalid payload.",
"Invalid token. Please log in again.": "Invalid token. Please log in again.",
"Max. files in a zip archive must be greater than 0": "Max. files in a zip archive must be greater than 0",
"Max. size of uploaded files must be greater than 0": "Max. size of uploaded files must be greater than 0",
"Max. size of zip archive must be equal or greater than max. size of uploaded files": "Max. size of zip archive must be equal or greater than max. size of uploaded files",
"Max. size of zip archive must be greater than 0": "Max. size of zip archive must be greater than 0",
"No file part.": "No file part.",
"No picture.": "No picture.",
"No selected file.": "No selected file.",

View File

@ -14,7 +14,10 @@
"Invalid credentials.": "Identifiants invalides.",
"Invalid payload.": "Données incorrectes.",
"Invalid token. Please log in again.": "Jeton invalide. Merci de vous reconnecter.",
"Max. files in a zip archive must be greater than 0": "Le nombre max. de fichiers dans une archive doit être supérieur à 0",
"Max. size of uploaded files must be greater than 0": "La taille max. des fichiers doit être supérieure à 0",
"Max. size of zip archive must be equal or greater than max. size of uploaded files": "La taille max. d'une archive doit être supérieure ou égale à la taille max. d'un fichier",
"Max. size of zip archive must be greater than 0": "La taille max. d'une archive doit être supérieure à 0",
"No file part.": "Pas de fichier fourni.",
"No picture.": "Pas d'image.",
"No selected file.": "Pas de fichier sélectionné.",

View File

@ -108,6 +108,7 @@ const message = (state = initial.message, action) => {
case 'PICTURE_ERROR':
case 'SET_ERROR':
return action.message
case 'CLEAN_ALL_MESSAGES':
case 'LOGOUT':
case 'PROFILE_SUCCESS':
case 'SET_APP_CONFIG':
@ -122,7 +123,9 @@ const message = (state = initial.message, action) => {
const messages = (state = initial.messages, action) => {
switch (action.type) {
case 'AUTH_ERRORS':
case 'APP_ERRORS':
return action.messages
case 'CLEAN_ALL_MESSAGES':
case 'LOGOUT':
case 'PROFILE_SUCCESS':
case '@@router/LOCATION_CHANGE':