diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f7a9a22bd..774467922 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -212,7 +212,7 @@ jobs: report_name: Coverage Frontend type: lcov result_path: ./coverage/lcov.info - min_coverage: 20 + min_coverage: 21 token: ${{ github.token }} ############################################################################## diff --git a/community_server/config/routes.php b/community_server/config/routes.php index 791692e96..20fc1ff62 100644 --- a/community_server/config/routes.php +++ b/community_server/config/routes.php @@ -60,19 +60,21 @@ Router::scope('/', function (RouteBuilder $routes) { $whitelist = ['JsonRequestHandler', 'ElopageWebhook', 'AppRequests']; $ajaxWhitelist = ['TransactionSendCoins', 'TransactionCreations']; + $callerIp = $request->clientIp(); + foreach($whitelist as $entry) { if($request->getParam('controller') === $entry) { if($entry == 'ElopageWebhook' || $entry == 'AppRequests') { return true; } $allowedIpLocalhost = ['127.0.0.1', 'localhost', '', '::1']; - if(in_array($request->clientIp(), $allowedIpLocalhost)) { + if(in_array($callerIp, $allowedIpLocalhost)) { return true; } $allowedCaller = Configure::read('API.allowedCaller'); $ipPerHost = []; if($allowedCaller && count($allowedCaller) > 0) { - $callerIp = $request->clientIp(); + foreach($allowedCaller as $allowed) { $ip = gethostbyname($allowed); $ipPerHost[$allowed] = $ip; diff --git a/community_server/src/Controller/AppRequestsController.php b/community_server/src/Controller/AppRequestsController.php index 2ece0c726..6b744ff69 100644 --- a/community_server/src/Controller/AppRequestsController.php +++ b/community_server/src/Controller/AppRequestsController.php @@ -348,7 +348,7 @@ class AppRequestsController extends AppController $decay = true; $transactions = []; $transactions_from_db = $stateUserTransactionsQuery->toArray(); - + if(count($transactions_from_db)) { if($orderDirection == 'DESC') { $transactions_from_db = array_reverse($transactions_from_db); diff --git a/community_server/src/Controller/JsonRequestHandlerController.php b/community_server/src/Controller/JsonRequestHandlerController.php index 65bf48440..611984118 100644 --- a/community_server/src/Controller/JsonRequestHandlerController.php +++ b/community_server/src/Controller/JsonRequestHandlerController.php @@ -391,8 +391,12 @@ class JsonRequestHandlerController extends AppController { } if ($transaction->save()) { + $result = ['state' => 'success']; + if($transaction->hasWarnings()) { + $result['warnings'] = $transaction->getWarnings(); + } // success - return $this->returnJson(['state' => 'success']); + return $this->returnJson($result); } else { $this->sendEMailTransactionFailed($transaction, 'save'); diff --git a/community_server/src/Model/Transactions/Transaction.php b/community_server/src/Model/Transactions/Transaction.php index db7ed9e5b..810f20c9d 100644 --- a/community_server/src/Model/Transactions/Transaction.php +++ b/community_server/src/Model/Transactions/Transaction.php @@ -198,8 +198,10 @@ class Transaction extends TransactionBase { $connection->commit(); - $this->mTransactionBody->getSpecificTransaction()->sendNotificationEmail($this->mTransactionBody->getMemo()); + $specificTransaction = $this->mTransactionBody->getSpecificTransaction(); + $specificTransaction->sendNotificationEmail($this->mTransactionBody->getMemo()); + $this->addWarnings($specificTransaction->getWarnings()); return true; } diff --git a/community_server/src/Model/Transactions/TransactionBase.php b/community_server/src/Model/Transactions/TransactionBase.php index 607903d8d..6b3817201 100644 --- a/community_server/src/Model/Transactions/TransactionBase.php +++ b/community_server/src/Model/Transactions/TransactionBase.php @@ -6,29 +6,43 @@ use Cake\ORM\TableRegistry; class TransactionBase { private $errors = []; + private $warnings = []; static $tables = []; public function getErrors() { - return $this->errors; + return $this->errors; + } + + public function getWarnings() { + return $this->warnings; } - public function addError($functionName, $errorName) { - array_push($this->errors, [$functionName => $errorName]); + array_push($this->errors, [$functionName => $errorName]); + } + public function addWarning($functionName, $warningName) { + array_push($this->warnings, [$functionName => $warningName]); } public function addErrors($errors) { - $this->errors = array_merge($this->errors, $errors); + $this->errors = array_merge($this->errors, $errors); + } + + public function addWarnings($warnings) { + $this->warnings = array_merge($this->warnings, $warnings); } public function hasErrors() { - return count($this->errors) > 0; + return count($this->errors) > 0; } + public function hasWarnings() { + return count($this->warnings) > 0; + } public static function getTable($tableName) { - if(!isset(self::$tables[$tableName])) { - self::$tables[$tableName] = TableRegistry::getTableLocator()->get($tableName); - } - return self::$tables[$tableName]; + if(!isset(self::$tables[$tableName])) { + self::$tables[$tableName] = TableRegistry::getTableLocator()->get($tableName); + } + return self::$tables[$tableName]; } diff --git a/community_server/src/Model/Transactions/TransactionCreation.php b/community_server/src/Model/Transactions/TransactionCreation.php index f9b3c7657..9150d24eb 100644 --- a/community_server/src/Model/Transactions/TransactionCreation.php +++ b/community_server/src/Model/Transactions/TransactionCreation.php @@ -209,6 +209,7 @@ class TransactionCreation extends TransactionBase { ->send(); } catch(Exception $e) { // $this->addError('TransactionCreation::sendNotificationEmail', 'error sending notification email: ' . $e->getMessage()); + $this->addWarning('TransactionCreation::sendNotificationEmail', 'error sending notification email: ' . $e->getMessage()); return false; } return true; diff --git a/community_server/src/Model/Transactions/TransactionTransfer.php b/community_server/src/Model/Transactions/TransactionTransfer.php index c36583c13..dc1606f55 100644 --- a/community_server/src/Model/Transactions/TransactionTransfer.php +++ b/community_server/src/Model/Transactions/TransactionTransfer.php @@ -204,13 +204,14 @@ class TransactionTransfer extends TransactionBase { $this->addError('TransactionCreation::sendNotificationEmail', 'to email is empty for user: ' . $receiverUser->id); return false; } - $email->setFrom([$serverAdminEmail => $senderUser->getNames() . ' via Gradido Community']) + $noReplyEmail = Configure::read('noReplyEmail'); + $email->setFrom([$noReplyEmail => 'Gradido (nicht antworten)']) ->setTo([$receiverUser->email => $receiverUser->getNames()]) - ->setReplyTo($senderUser->email) ->setSubject(__('Gradidos erhalten')) ->send(); } catch(Exception $e) { //$this->addError('TransactionTransfer::sendNotificationEmail', 'error sending notification email: ' . $e->getMessage()); + $this->addWarning('TransactionTransfer::sendNotificationEmail', 'error sending notification email: ' . $e->getMessage()); return false; } return true; diff --git a/community_server/src/Template/Email/text/notification_transfer.ctp b/community_server/src/Template/Email/text/notification_transfer.ctp index 2cc692e02..155304c2c 100644 --- a/community_server/src/Template/Email/text/notification_transfer.ctp +++ b/community_server/src/Template/Email/text/notification_transfer.ctp @@ -15,7 +15,7 @@ $senderNames = $senderUser->first_name . ' ' . $senderUser->last_name; - + Gradido Community Server \ No newline at end of file diff --git a/configs/community_server/app.php b/configs/community_server/app.php index 5bbdcdc4c..5acd4ce51 100644 --- a/configs/community_server/app.php +++ b/configs/community_server/app.php @@ -214,9 +214,8 @@ return [ 'timeout' => 30, 'username' => null, 'password' => null, - 'client' => null, - 'tls' => null, - 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null), + 'className' => 'Smtp', + 'tls' => true ], ], diff --git a/docu/login_server.api.md b/docu/login_server.api.md index b466be7fc..fb9409cf0 100644 --- a/docu/login_server.api.md +++ b/docu/login_server.api.md @@ -89,6 +89,56 @@ In case of success returns: nginx was wrong configured. - `session_id`: can be also negative +## Check username +### Request +`GET http://localhost/login_api/checkUsername?username=&group_id=` + +`POST http://localhost/login_api/checkUsername` +with +```json +{ + "username": "Maxilein", + "group_id": 1, + "group_alias": "gdd1" +} +``` + +group_id or group_alias, one of both is enough. +group_id is better, because one db request less + +### Response + +If username is not already taken +```json +{ + "state":"success" +} +``` + +If username is already taken +```json +{ + "state":"warning", + "msg":"username already in use" +} +``` + +If only group_alias was given and group with that alias was found in db +```json +{ + "state":"success", + "group_id": 1 +} +``` + +If group_id or group_alias unknown +```json +{ + "state":"error", + "msg": "unknown group" +} +``` + ## Create user Register a new User diff --git a/frontend/src/apis/communityAPI.js b/frontend/src/apis/communityAPI.js index 25ef10f43..b2df337b8 100644 --- a/frontend/src/apis/communityAPI.js +++ b/frontend/src/apis/communityAPI.js @@ -35,7 +35,7 @@ const communityAPI = { balance: async (sessionId) => { return apiGet(CONFIG.COMMUNITY_API_URL + 'getBalance/' + sessionId) }, - transactions: async (sessionId, firstPage = 1, items = 1000, order = 'DESC') => { + transactions: async (sessionId, firstPage = 1, items = 5, order = 'DESC') => { return apiGet( `${CONFIG.COMMUNITY_API_URL}listTransactions/${firstPage}/${items}/${order}/${sessionId}`, ) diff --git a/frontend/src/apis/loginAPI.js b/frontend/src/apis/loginAPI.js index 55e32e6dd..91e92a2cc 100644 --- a/frontend/src/apis/loginAPI.js +++ b/frontend/src/apis/loginAPI.js @@ -78,6 +78,27 @@ const loginAPI = { CONFIG.LOGIN_API_URL + 'loginViaEmailVerificationCode?emailVerificationCode=' + optin, ) }, + getUserInfos: async (sessionId, email) => { + const payload = { + session_id: sessionId, + email: email, + ask: ['user.first_name', 'user.last_name'], + } + return apiPost(CONFIG.LOGIN_API_URL + 'getUserInfos', payload) + }, + updateUserInfos: async (sessionId, email, data) => { + const payload = { + session_id: sessionId, + email, + update: { + 'User.first_name': data.firstName, + 'User.last_name': data.lastName, + 'User.description': data.description, + }, + } + return apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload) + }, + changePassword: async (sessionId, email, password) => { const payload = { session_id: sessionId, @@ -88,6 +109,27 @@ const loginAPI = { } return apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload) }, + changePasswordProfile: async (sessionId, email, password, passwordNew) => { + const payload = { + session_id: sessionId, + email, + update: { + 'User.password': password, + 'User.passwordNew': passwordNew, + }, + } + return apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload) + }, + changeUsernameProfile: async (sessionId, email, usernameNew) => { + const payload = { + session_id: sessionId, + email, + update: { + 'User.usernameNew': usernameNew, + }, + } + return apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload) + }, updateLanguage: async (sessionId, email, language) => { const payload = { session_id: sessionId, diff --git a/frontend/src/components/PaginationButtons.spec.js b/frontend/src/components/PaginationButtons.spec.js new file mode 100644 index 000000000..7a03d0443 --- /dev/null +++ b/frontend/src/components/PaginationButtons.spec.js @@ -0,0 +1,53 @@ +import { mount } from '@vue/test-utils' +import PaginationButtons from './PaginationButtons' + +const localVue = global.localVue + +describe('PaginationButtons', () => { + let wrapper + + const Wrapper = () => { + return mount(PaginationButtons, { localVue }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the component', () => { + expect(wrapper.find('div.pagination-buttons').exists()).toBeTruthy() + }) + + it('has previous page button disabled by default', () => { + expect(wrapper.find('button.previous-page').attributes('disabled')).toBe('disabled') + }) + + it('has bext page button disabled by default', () => { + expect(wrapper.find('button.next-page').attributes('disabled')).toBe('disabled') + }) + + it('shows the text "1 / 1" by default"', () => { + expect(wrapper.find('p.text-center').text()).toBe('1 / 1') + }) + + describe('with active buttons', () => { + beforeEach(async () => { + await wrapper.setProps({ + hasNext: true, + hasPrevious: true, + }) + }) + + it('emits show-previous when previous page button is clicked', () => { + wrapper.find('button.previous-page').trigger('click') + expect(wrapper.emitted('show-previous')).toBeTruthy() + }) + + it('emits show-next when next page button is clicked', () => { + wrapper.find('button.next-page').trigger('click') + expect(wrapper.emitted('show-next')).toBeTruthy() + }) + }) + }) +}) diff --git a/frontend/src/components/PaginationButtons.vue b/frontend/src/components/PaginationButtons.vue new file mode 100644 index 000000000..ac7ff73c6 --- /dev/null +++ b/frontend/src/components/PaginationButtons.vue @@ -0,0 +1,30 @@ + + diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 81fb90972..5a499b7f4 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -20,17 +20,25 @@ }, "decay": "Vergänglichkeit", "form": { - "cancel":"Abbrechen", + "cancel": "Abbrechen", "reset": "Zurücksetzen", - "close":"schließen", + "close": "schließen", + "edit": "bearbeiten", + "save": "speichern", "receiver":"Empfänger", "sender":"Absender", + "username":"Username", "firstname":"Vorname", "lastname":"Nachname", + "description": "Beschreibung", "email":"E-Mail", "email_repeat":"eMail wiederholen", "password":"Passwort", "password_repeat":"Passwort wiederholen", + "password_old":"altes Passwort", + "password_new":"neues Passwort", + "password_new_repeat":"neues Passwort wiederholen", + "change": "ändern", "amount":"Betrag", "memo":"Nachricht für den Empfänger", "message":"Nachricht", @@ -49,8 +57,9 @@ "send_transaction_error":"Leider konnte die Transaktion nicht ausgeführt werden!", "validation": { "double": "Das Feld {field} muss eine Dezimalzahl mit zwei Nachkommastellen sein", - "is-not": "Du kannst dir selbst keine Gradidos überweisen" - } + "is-not": "Du kannst Dir selbst keine Gradidos überweisen" + }, + "change_username_info": "Das ändern des Usernamens bedarf mehrerer Schritte." }, "error": { "error":"Fehler" @@ -96,7 +105,6 @@ "add_work":"neuer Gemeinschaftsbeitrag" }, "profil": { - "transactions":"transactions", "activity": { "chart":"Gemeinschaftsstunden Chart", "new":"Neue Gemeinschaftsstunden eintragen", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 58ebbf9f1..aa42d966e 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -1,7 +1,7 @@ { "message": "hello gradido !!", "welcome":"Welcome!", - "community": "Gemeinschaft", + "community": "Community", "logout":"Logout", "login":"Login", "signup": "Sign up", @@ -23,14 +23,22 @@ "cancel":"Cancel", "reset": "Reset", "close":"Close", + "edit": "Edit", + "save": "save", "receiver":"Receiver", "sender":"Sender", + "username":"Username", "firstname":"Firstname", "lastname":"Lastname", + "description": "Description", "email":"Email", "email_repeat":"Repeat Email", "password":"Password", "password_repeat":"Repeat password", + "password_old":"Old password", + "password_new":"New password", + "password_new_repeat":"Repeat new password", + "change": "change", "amount":"Amount", "memo":"Message for the recipient", "message":"Message", @@ -50,7 +58,8 @@ "validation": { "double": "The {field} field must be a decimal with two digits", "is-not": "You cannot send Gradidos to yourself" - } + }, + "change_username_info": "Changing the username requires several steps." }, "error": { "error":"Error" diff --git a/frontend/src/routes/routes.js b/frontend/src/routes/routes.js index 16ab3d78f..44ef1f27a 100755 --- a/frontend/src/routes/routes.js +++ b/frontend/src/routes/routes.js @@ -16,25 +16,11 @@ const routes = [ }, { path: '/profile', - component: () => import('../views/Pages/UserProfile.vue'), + component: () => import('../views/Pages/UserProfileOverview.vue'), meta: { requiresAuth: true, }, }, - // { - // path: '/profileedit', - // component: () => import('../views/Pages/UserProfileEdit.vue'), - // meta: { - // requiresAuth: true, - // }, - // }, - // { - // path: '/activity', - // component: () => import('../views/Pages/UserProfileActivity.vue'), - // meta: { - // requiresAuth: true, - // }, - // }, { path: '/transactions', component: () => import('../views/Pages/UserProfileTransactionList.vue'), diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 1d89894fb..4c7e10a87 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -13,6 +13,18 @@ export const mutations = { sessionId: (state, sessionId) => { state.sessionId = sessionId }, + username: (state, username) => { + state.username = username + }, + firstName: (state, firstName) => { + state.firstName = firstName + }, + lastName: (state, lastName) => { + state.lastName = lastName + }, + description: (state, description) => { + state.description = description + }, } export const actions = { @@ -20,10 +32,18 @@ export const actions = { commit('sessionId', data.sessionId) commit('email', data.user.email) commit('language', data.user.language) + commit('username', data.user.username) + commit('firstName', data.user.first_name) + commit('lastName', data.user.last_name) + commit('description', data.user.description) }, logout: ({ commit, state }) => { commit('sessionId', null) commit('email', null) + commit('username', '') + commit('firstName', '') + commit('lastName', '') + commit('description', '') sessionStorage.clear() }, } @@ -39,6 +59,10 @@ export const store = new Vuex.Store({ email: '', language: null, modals: false, + firstName: '', + lastName: '', + username: '', + description: '', }, getters: {}, // Syncronous mutation of the state diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js index 11dd5949b..6bc004273 100644 --- a/frontend/src/store/store.test.js +++ b/frontend/src/store/store.test.js @@ -40,7 +40,7 @@ describe('Vuex store', () => { { commit, state }, { sessionId: 1234, user: { email: 'someone@there.is', language: 'en' } }, ) - expect(commit).toHaveBeenCalledTimes(3) + expect(commit).toHaveBeenCalledTimes(7) }) it('commits sessionId', () => { @@ -74,7 +74,7 @@ describe('Vuex store', () => { it('calls two commits', () => { logout({ commit, state }) - expect(commit).toHaveBeenCalledTimes(2) + expect(commit).toHaveBeenCalledTimes(6) }) it('commits sessionId', () => { diff --git a/frontend/src/views/Layout/DashboardLayout_gdd.spec.js b/frontend/src/views/Layout/DashboardLayout_gdd.spec.js index 84c955042..62ce94322 100644 --- a/frontend/src/views/Layout/DashboardLayout_gdd.spec.js +++ b/frontend/src/views/Layout/DashboardLayout_gdd.spec.js @@ -78,7 +78,7 @@ describe('DashboardLayoutGdd', () => { }) it('has five items in the navbar', () => { - expect(navbar.findAll('ul > a')).toHaveLength(2) + expect(navbar.findAll('ul > a')).toHaveLength(3) }) it('has first item "send" in navbar', () => { @@ -103,21 +103,21 @@ describe('DashboardLayoutGdd', () => { expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/transactions') }) - // it('has tree items in the navbar', () => { - // expect(navbar.findAll('ul > li')).toHaveLength(3) - // }) - // - // it('has third item "My profile" in navbar', () => { - // expect(navbar.findAll('ul > li').at(2).text()).toEqual('site.navbar.my-profil') - // }) - // - // it.skip('has third item "My profile" linked to profile in navbar', async () => { - // navbar.findAll('ul > li > a').at(2).trigger('click') - // await flushPromises() - // await jest.runAllTimers() - // await flushPromises() - // expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/profile') - // }) + it('has tree items in the navbar', () => { + expect(navbar.findAll('ul > a')).toHaveLength(3) + }) + + it('has third item "My profile" in navbar', () => { + expect(navbar.findAll('ul > a').at(2).text()).toEqual('site.navbar.my-profil') + }) + + it.skip('has third item "My profile" linked to profile in navbar', async () => { + navbar.findAll('ul > a').at(2).trigger('click') + await flushPromises() + await jest.runAllTimers() + await flushPromises() + expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/profile') + }) // it('has fourth item "Settigs" in navbar', () => { // expect(navbar.findAll('ul > li').at(3).text()).toEqual('site.navbar.settings') diff --git a/frontend/src/views/Layout/DashboardLayout_gdd.vue b/frontend/src/views/Layout/DashboardLayout_gdd.vue index d6b9f44ff..70a35a40a 100755 --- a/frontend/src/views/Layout/DashboardLayout_gdd.vue +++ b/frontend/src/views/Layout/DashboardLayout_gdd.vue @@ -1,5 +1,5 @@