Merge branch 'master' into 732-use-slots-and-templates-instead-of-if-else

This commit is contained in:
Moriz Wahl 2021-09-13 20:34:42 +02:00
commit 2539504ba8
23 changed files with 262 additions and 105 deletions

3
.gitmodules vendored
View File

@ -31,3 +31,6 @@
[submodule "login_server/src/proto"]
path = login_server/src/proto
url = https://github.com/gradido/gradido_protocol.git
[submodule "login_server/dependencies/protobuf"]
path = login_server/dependencies/protobuf
url = https://github.com/protocolbuffers/protobuf.git

View File

@ -18,6 +18,7 @@
"apollo-server-express": "^2.25.2",
"axios": "^0.21.1",
"class-validator": "^0.13.1",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"graphql": "^15.5.1",

View File

@ -4,6 +4,7 @@ import { AuthChecker } from 'type-graphql'
import decode from '../jwt/decode'
import { apiGet } from '../apis/loginAPI'
import CONFIG from '../config'
import encode from '../jwt/encode'
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
export const isAuthorized: AuthChecker<any> = async ({ root, args, context, info }, roles) => {
@ -14,6 +15,7 @@ export const isAuthorized: AuthChecker<any> = async ({ root, args, context, info
`${CONFIG.LOGIN_API_URL}checkSessionState?session_id=${decoded.sessionId}`,
)
context.sessionId = decoded.sessionId
context.setHeaders.push({ key: 'token', value: encode(decoded.sessionId) })
return result.success
}
}

View File

@ -4,10 +4,11 @@
import { Resolver, Query, Args, Arg, Authorized, Ctx } from 'type-graphql'
import CONFIG from '../../config'
import { CheckUsernameResponse } from '../models/CheckUsernameResponse'
import { User } from '../models/User'
import { LoginViaVerificationCode } from '../models/LoginViaVerificationCode'
import { SendPasswordResetEmailResponse } from '../models/SendPasswordResetEmailResponse'
import { UpdateUserInfosResponse } from '../models/UpdateUserInfosResponse'
import { User } from '../models/User'
import encode from '../../jwt/encode'
import {
ChangePasswordArgs,
CheckUsernameArgs,
@ -16,12 +17,11 @@ import {
UpdateUserInfosArgs,
} from '../inputs/LoginUserInput'
import { apiPost, apiGet } from '../../apis/loginAPI'
import encode from '../../jwt/encode'
@Resolver()
export class UserResolver {
@Query(() => String)
async login(@Args() { email, password }: UnsecureLoginArgs): Promise<string> {
@Query(() => User)
async login(@Args() { email, password }: UnsecureLoginArgs, @Ctx() context: any): Promise<User> {
email = email.trim().toLowerCase()
const result = await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', { email, password })
@ -30,10 +30,9 @@ export class UserResolver {
throw new Error(result.data)
}
const data = result.data
const sessionId = data.session_id
delete data.session_id
return encode({ sessionId, user: new User(data.user) })
context.setHeaders.push({ key: 'token', value: encode(result.data.session_id) })
return new User(result.data.user)
}
@Query(() => LoginViaVerificationCode)

View File

@ -2,6 +2,7 @@
import 'reflect-metadata'
import express from 'express'
import cors from 'cors'
import { buildSchema } from 'type-graphql'
import { ApolloServer } from 'apollo-server-express'
import { RowDataPacket } from 'mysql2/promise'
@ -22,14 +23,15 @@ import { isAuthorized } from './auth/auth'
const DB_VERSION = '0001-init_db'
const context = (req: any) => {
const authorization = req.req.headers.authorization
const context = (args: any) => {
const authorization = args.req.headers.authorization
let token = null
if (authorization) {
token = req.req.headers.authorization.replace(/^Bearer /, '')
token = authorization.replace(/^Bearer /, '')
}
const context = {
token,
setHeaders: [],
}
return context
}
@ -61,8 +63,31 @@ async function main() {
// Express Server
const server = express()
const corsOptions = {
origin: '*',
exposedHeaders: ['token'],
}
server.use(cors(corsOptions))
const plugins = [
{
requestDidStart() {
return {
willSendResponse(requestContext: any) {
const { setHeaders = [] } = requestContext.context
setHeaders.forEach(({ key, value }: { [key: string]: string }) => {
requestContext.response.http.headers.append(key, value)
})
return requestContext
},
}
},
},
]
// Apollo Server
const apollo = new ApolloServer({ schema, playground, context })
const apollo = new ApolloServer({ schema, playground, context, plugins })
apollo.applyMiddleware({ app: server })
// Start Server

View File

@ -7,15 +7,12 @@ import CONFIG from '../config/'
export default (token: string): any => {
if (!token) return null
let sessionId = null
const email = null
try {
const decoded = jwt.verify(token, CONFIG.JWT_SECRET)
sessionId = decoded.sub
// email = decoded.email
return {
token,
sessionId,
email,
}
} catch (err) {
return null

View File

@ -5,13 +5,9 @@ import jwt from 'jsonwebtoken'
import CONFIG from '../config/'
// Generate an Access Token
export default function encode(data: any): string {
const { user, sessionId } = data
const { email, language, firstName, lastName } = user
const token = jwt.sign({ email, language, firstName, lastName, sessionId }, CONFIG.JWT_SECRET, {
export default function encode(sessionId: string): string {
const token = jwt.sign({ sessionId }, CONFIG.JWT_SECRET, {
expiresIn: CONFIG.JWT_EXPIRES_IN,
// issuer: CONFIG.GRAPHQL_URI,
// audience: CONFIG.CLIENT_URI,
subject: sessionId.toString(),
})
return token

View File

@ -47,8 +47,12 @@ $this->assign('header', $header);
<div class="cell c3"><?= new FrozenTime($entry['date']) ?></div>
<div class="cell c0"><?= h($entry['comment']) ?></div>
<div class="cell c3">
<?= $this->element('printEuro', ['number' => $entry['amount']]); ?>
<?php if($entry['amount2']) echo ' + ' . $this->element('printEuro', ['number' => $entry['amount2']]) ?>
<?php if(intval($entry['gdt_entry_type_id']) == 7) : ?>
<?= $this->element('printGDT', ['number' => $entry['amount']*100.0]); ?>
<?php else : ?>
<?= $this->element('printEuro', ['number' => $entry['amount']*100.0]); ?>
<?php if($entry['amount2']) echo ' + ' . $this->element('printEuro', ['number' => $entry['amount2']*100.0]) ?>
<?php endif; ?>
</div>
<div class="cell c2">
<?= $this->Number->format($entry['factor']) ?>
@ -89,8 +93,12 @@ $this->assign('header', $header);
<!--<div class="cell c0"><?= h($elopageTransaction['email']) ?></div>-->
<div class="cell c3"><?= new FrozenTime($gdtEntry['date']) ?></div>
<div class="cell c3">
<?= $this->element('printEuro', ['number' => $gdtEntry['amount']]) ?>
<?php if($gdtEntry['amount2']) echo ' + ' . $this->element('printEuro', ['number' => $gdtEntry['amount2']]) ?>
<?php if(intval($gdtEntry['gdt_entry_type_id']) == 7) : ?>
<?= $this->element('printGDT', ['number' => $gdtEntry['amount']*100.0]); ?>
<?php else : ?>
<?= $this->element('printEuro', ['number' => $gdtEntry['amount']*100.0]); ?>
<?php if($gdtEntry['amount2']) echo ' + ' . $this->element('printEuro', ['number' => $gdtEntry['amount2']*100.0]) ?>
<?php endif; ?>
</div>
<div class="cell c2">
<?= $this->Number->format($gdtEntry['factor']) ?>

View File

@ -22,7 +22,8 @@ loginServer.db.user = root
loginServer.db.password =
loginServer.db.port = 3306
frontend.checkEmailPath = http://localhost/reset
frontend.checkEmailPath = vue/checkEmail
frontend.resetPasswordPath = vue/reset
email.disable = true

View File

@ -72,7 +72,6 @@
"vue-good-table": "^2.21.3",
"vue-i18n": "^8.22.4",
"vue-jest": "^3.0.7",
"vue-jwt-decode": "^0.1.0",
"vue-loading-overlay": "^3.4.2",
"vue-moment": "^4.1.0",
"vue-qrcode": "^0.3.5",

View File

@ -2,7 +2,14 @@ import gql from 'graphql-tag'
export const login = gql`
query($email: String!, $password: String!) {
login(email: $email, password: $password)
login(email: $email, password: $password) {
email
username
firstName
lastName
language
description
}
}
`

View File

@ -20,7 +20,11 @@ const authLink = new ApolloLink((operation, forward) => {
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
},
})
return forward(operation)
return forward(operation).map((response) => {
const newToken = operation.getContext().response.headers.get('token')
if (newToken) store.commit('token', newToken)
return response
})
})
const apolloClient = new ApolloClient({

View File

@ -1,7 +1,6 @@
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import VueJwtDecode from 'vue-jwt-decode'
Vue.use(Vuex)
@ -30,15 +29,13 @@ export const mutations = {
}
export const actions = {
login: ({ dispatch, commit }, token) => {
const decoded = VueJwtDecode.decode(token)
commit('token', token)
commit('email', decoded.email)
commit('language', decoded.language)
commit('username', decoded.username)
commit('firstName', decoded.firstName)
commit('lastName', decoded.lastName)
commit('description', decoded.description)
login: ({ dispatch, commit }, data) => {
commit('email', data.email)
commit('language', data.language)
commit('username', data.username)
commit('firstName', data.firstName)
commit('lastName', data.lastName)
commit('description', data.description)
},
logout: ({ commit, state }) => {
commit('token', null)

View File

@ -1,15 +1,4 @@
import { mutations, actions } from './store'
import VueJwtDecode from 'vue-jwt-decode'
jest.mock('vue-jwt-decode')
VueJwtDecode.decode.mockReturnValue({
email: 'user@example.org',
language: 'de',
username: 'peter',
firstName: 'Peter',
lastName: 'Lustig',
description: 'Nickelbrille',
})
const { language, email, token, username, firstName, lastName, description } = mutations
const { login, logout } = actions
@ -77,46 +66,48 @@ describe('Vuex store', () => {
describe('login', () => {
const commit = jest.fn()
const state = {}
const commitedData = 'token'
const commitedData = {
email: 'user@example.org',
language: 'de',
username: 'peter',
firstName: 'Peter',
lastName: 'Lustig',
description: 'Nickelbrille',
}
it('calls seven commits', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(7)
})
it('commits token', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(1, 'token', 'token')
expect(commit).toHaveBeenCalledTimes(6)
})
it('commits email', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(2, 'email', 'user@example.org')
expect(commit).toHaveBeenNthCalledWith(1, 'email', 'user@example.org')
})
it('commits language', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(3, 'language', 'de')
expect(commit).toHaveBeenNthCalledWith(2, 'language', 'de')
})
it('commits username', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(4, 'username', 'peter')
expect(commit).toHaveBeenNthCalledWith(3, 'username', 'peter')
})
it('commits firstName', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(5, 'firstName', 'Peter')
expect(commit).toHaveBeenNthCalledWith(4, 'firstName', 'Peter')
})
it('commits lastName', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(6, 'lastName', 'Lustig')
expect(commit).toHaveBeenNthCalledWith(5, 'lastName', 'Lustig')
})
it('commits description', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(7, 'description', 'Nickelbrille')
expect(commit).toHaveBeenNthCalledWith(6, 'description', 'Nickelbrille')
})
})

View File

@ -1,31 +0,0 @@
sudo apt install libsodium-dev
# get dependencies
git submodule update --init --recursive
cd dependencies/mariadb-connector-c
mkdir build
cd build
cmake -DWITH_SSL=OFF ..
cd ../../../
# get more dependencies with conan (need conan from https://conan.io/)
mkdir build && cd build
# // not used anymore
# conan remote add inexor https://api.bintray.com/conan/inexorgame/inexor-conan
# not needed, but bincrafter
# conan install .. -s build_type=Debug
conan install ..
# build Makefile with cmake
cmake ..
make grpc
# under windows build at least release for protoc.exe and grpc c++ plugin
cd ../
./unix_parse_proto.sh
cd build
make

147
login_server/README.md Normal file
View File

@ -0,0 +1,147 @@
# Build Login-Server yourself
## Linux (Ubuntu) Packets
install build essentials
```bash
sudo apt install -y gcovr build-essential gettext libcurl4-openssl-dev libssl-dev libsodium-dev libboost-dev
```
## CMake
CMake is used for build file generation and the Login-Server needs at least version v3.18.2
You can build and install it from source.
The Version in apt is sadly to old.
```bash
git clone https://github.com/Kitware/CMake.git --branch v3.18.2
cd CMake
./bootstrap --parallel=$(nproc) && make -j$(nproc) && sudo make install
```
## dependencies
load git submodules if you haven't done it yet
```bash
git submodule update --init --recursive
```
## build tools
build protoc and page compiler needed for generating some additional code
```bash
cd scripts
./prepare_build.sh
```
## build
build login-server in debug mode
```bash
cd scripts
./build_debug.sh
```
## multilanguage text
Login-Server uses gettext translations found after build in src/LOCALE
On Linux Login-Server expect the *.po files in folder /etc/grd_login/LOCALE
on windows next to Binary in Folder LOCALE.
So please copy them over by yourself on first run or after change.
If you like to update some translations your find a messages.pot in src/LOCALE.
Use it together with poedit and don't forget to copy over *.po files after change to /etc/grd_login/LOCALE
To update messages.pot run
```bash
./scripts/compile_pot.sh
```
This will be also called by ./scripts/build_debug.sh
## database
Login-Server needs a db to run, it is tested with mariadb
table definitions are found in folder ./skeema/gradido_login
Currently at least one group must be present in table groups.
For example:
```sql
INSERT INTO `groups` (`id`, `alias`, `name`, `url`, `host`, `home`, `description`) VALUES
(1, 'docker', 'docker gradido group', 'localhost', 'localhost', '/', 'gradido test group for docker with blockchain db');
```
## configuration
Login-Server needs a configuration file to able to run.
On Linux it expect it to find the file /etc/grd_login/grd_login.properties
and /etc/grd_login/grd_login_test.properties for unittest
Example configuration (ini-format)
```ini
# Port for Web-Interface
HTTPServer.port = 1200
# Port for json-Interface (used by new backend)
JSONServer.port = 1201
# default group id for new users, if no group was choosen
Gradido.group_id = 1
# currently not used
crypto.server_admin_public = f909a866baec97c5460b8d7a93b72d3d4d20cc45d9f15d78bd83944eb9286b7f
# Server admin Passphrase
# nerve execute merit pool talk hockey basic win cargo spin disagree ethics swear price purchase say clutch decrease slow half forest reform cheese able
#
# TODO: auto-generate in docker build step
# expect valid hex 32 character long (16 Byte)
# salt for hashing user password, should be moved into db generated and saved per user, used for hardening against hash-tables
crypto.server_key = a51ef8ac7ef1abf162fb7a65261acd7a
# TODO: auto-generate in docker build step
# salt for hashing user encryption key, expect valid hex, as long as you like, used in sha512
crypto.app_secret = 21ffbbc616fe
# for url forwarding to old frontend, path of community server
phpServer.url = http://localhost/
# host for community server api calls
phpServer.host = localhost
# port for community server api calls
phpServer.port = 80
# Path for Login-Server Web-Interface used for link-generation
loginServer.path = http://localhost/account
# default language for new users and if no one is logged in
loginServer.default_locale = de
# db setup tested with mariadb, should also work with mysql
loginServer.db.host = localhost
loginServer.db.name = gradido_login
loginServer.db.user = root
loginServer.db.password =
loginServer.db.port = 3306
# check email path for new frontend for link generation in emails
frontend.checkEmailPath = http://localhost/vue/reset
# disable email all together
email.disable = true
# setup email smtp server for sending emails
#email.username =
#email.sender =
#email.admin_receiver =
#email.password =
#email.smtp.url =
#email.smtp.port =
# server setup types: test, staging or production
# used mainly to decide if using http or https for links
# test use http and staging and production uses https
ServerSetupType=test
dev.default_group = docker
# Session timeout in minutes
session.timeout = 15
# Disabling security features for faster develop and testing
unsecure.allow_passwort_via_json_request = 1
unsecure.allow_auto_sign_transactions = 1
unsecure.allow_cors_all = 1
# default disable, passwords must contain a number, a lower character, a high character, special character, and be at least 8 characters long
unsecure.allow_all_passwords = 1
```

@ -0,0 +1 @@
Subproject commit 0b8d13a1d4cd9be16ed8a2230577aa9c296aa1ca

View File

@ -1,12 +1,9 @@
#!/bin/sh
cd ../scripts
chmod +x compile_pot.sh
./compile_pot.sh
cd ../build
cmake -DCMAKE_BUILD_TYPE=Debug ..
./compile_pot.sh
make -j$(nproc) Gradido_LoginServer
chmod +x ./bin/Gradido_LoginServer

View File

@ -9,9 +9,9 @@ fi
mkdir build
cd build
cmake -DWITH_SSL=OFF ..
cd ../../
cd ../../../
if [! -d "./build" ] ; then
if [ ! -d "./build" ] ; then
mkdir build
fi
cd build

View File

@ -0,0 +1,8 @@
#!/bin/bash
cd ../build
cmake -DCMAKE_BUILD_TYPE=Debug -DCOLLECT_COVERAGE_DATA=ON -DCOVERAGE_TOOL=gcovr .. && \
make -j$(nproc) Gradido_LoginServer_Test
make coverage

View File

@ -105,12 +105,13 @@ Poco::JSON::Object* JsonSendEmail::handle(Poco::Dynamic::Var params)
return stateSuccess();
}
auto receiver_user_id = receiver_user->getModel()->getID();
std::string checkEmailUrl = receiver_user->getGroupBaseUrl() + ServerConfig::g_frontend_checkEmailPath;
std::string linkInEmail = "";
if (emailVerificationCodeType == model::table::EMAIL_OPT_IN_RESET_PASSWORD)
{
linkInEmail = receiver_user->getGroupBaseUrl() + ServerConfig::g_frontend_resetPasswordPath;
session = sm->getNewSession();
if (emailType == model::EMAIL_USER_RESET_PASSWORD) {
auto r = session->sendResetPasswordEmail(receiver_user, true, checkEmailUrl);
auto r = session->sendResetPasswordEmail(receiver_user, true, linkInEmail);
if (1 == r) {
return stateWarning("email already sended");
}
@ -120,7 +121,7 @@ Poco::JSON::Object* JsonSendEmail::handle(Poco::Dynamic::Var params)
}
else if (emailType == model::EMAIL_CUSTOM_TEXT) {
auto email_verification_code_object = controller::EmailVerificationCode::loadOrCreate(receiver_user_id, model::table::EMAIL_OPT_IN_RESET_PASSWORD);
email_verification_code_object->setBaseUrl(checkEmailUrl);
email_verification_code_object->setBaseUrl(linkInEmail);
auto email = new model::Email(email_verification_code_object, receiver_user, emailCustomText, emailCustomSubject);
em->addEmail(email);
}
@ -131,12 +132,13 @@ Poco::JSON::Object* JsonSendEmail::handle(Poco::Dynamic::Var params)
}
else
{
linkInEmail = receiver_user->getGroupBaseUrl() + ServerConfig::g_frontend_checkEmailPath;
if (session->getNewUser()->getModel()->getRole() != model::table::ROLE_ADMIN) {
return stateError("admin needed");
}
auto email_verification_code_object = controller::EmailVerificationCode::loadOrCreate(receiver_user_id, emailVerificationCodeType);
email_verification_code_object->setBaseUrl(checkEmailUrl);
email_verification_code_object->setBaseUrl(linkInEmail);
model::Email* email = nullptr;
if (emailType == model::EMAIL_CUSTOM_TEXT) {
email = new model::Email(email_verification_code_object, receiver_user, emailCustomText, emailCustomSubject);

View File

@ -51,6 +51,7 @@ namespace ServerConfig {
std::string g_php_serverPath;
std::string g_php_serverHost;
std::string g_frontend_checkEmailPath;
std::string g_frontend_resetPasswordPath;
int g_phpServerPort;
Poco::Mutex g_TimeMutex;
int g_FakeLoginSleepTime = 820;
@ -238,8 +239,9 @@ namespace ServerConfig {
if ("" != app_secret_string) {
g_CryptoAppSecret = DataTypeConverter::hexToBin(app_secret_string);
}
std::string defaultCheckEmailPath = g_serverPath + "/checkEmail";
std::string defaultCheckEmailPath = "/account/checkEmail";
g_frontend_checkEmailPath = cfg.getString("frontend.checkEmailPath", defaultCheckEmailPath);
g_frontend_resetPasswordPath = cfg.getString("frontend.resetPasswordPath", defaultCheckEmailPath);
//g_CryptoAppSecret
// unsecure flags

View File

@ -67,6 +67,7 @@ namespace ServerConfig {
extern std::string g_php_serverPath;
extern std::string g_php_serverHost;
extern std::string g_frontend_checkEmailPath;
extern std::string g_frontend_resetPasswordPath;
extern int g_phpServerPort;
extern Poco::Mutex g_TimeMutex;
extern int g_FakeLoginSleepTime;