Merge branch 'frontend_commit_hash' of github.com:gradido/gradido into frontend_commit_hash

This commit is contained in:
einhornimmond 2021-09-15 14:49:04 +02:00
commit 1e792c32e3
73 changed files with 1003 additions and 442 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ messages.pot
.skeema .skeema
nbproject nbproject
.metadata .metadata
/.env

3
.gitmodules vendored
View File

@ -31,3 +31,6 @@
[submodule "login_server/src/proto"] [submodule "login_server/src/proto"]
path = login_server/src/proto path = login_server/src/proto
url = https://github.com/gradido/gradido_protocol.git 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", "apollo-server-express": "^2.25.2",
"axios": "^0.21.1", "axios": "^0.21.1",
"class-validator": "^0.13.1", "class-validator": "^0.13.1",
"cors": "^2.8.5",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"express": "^4.17.1", "express": "^4.17.1",
"graphql": "^15.5.1", "graphql": "^15.5.1",

23
backend/src/auth/auth.ts Normal file
View File

@ -0,0 +1,23 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
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) => {
if (context.token) {
const decoded = decode(context.token)
if (decoded.sessionId && decoded.sessionId !== 0) {
const result = await apiGet(
`${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
}
}
return false
}

View File

@ -17,9 +17,6 @@ export class GdtTransactionInput {
@ArgsType() @ArgsType()
export class GdtTransactionSessionIdInput { export class GdtTransactionSessionIdInput {
@Field(() => Number)
sessionId: number
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
currentPage?: number currentPage?: number

View File

@ -38,9 +38,6 @@ export class ChangePasswordArgs {
@ArgsType() @ArgsType()
export class UpdateUserInfosArgs { export class UpdateUserInfosArgs {
@Field(() => Number)
sessionId!: number
@Field(() => String) @Field(() => String)
email!: string email!: string

View File

@ -2,9 +2,6 @@ import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType() @ArgsType()
export class TransactionListInput { export class TransactionListInput {
@Field(() => Number)
sessionId: number
@Field(() => Int) @Field(() => Int)
firstPage: number firstPage: number
@ -17,9 +14,6 @@ export class TransactionListInput {
@ArgsType() @ArgsType()
export class TransactionSendArgs { export class TransactionSendArgs {
@Field(() => Number)
sessionId: number
@Field(() => String) @Field(() => String)
email: string email: string

View File

@ -15,6 +15,7 @@ export enum GdtEntryType {
@ObjectType() @ObjectType()
export class GdtEntry { export class GdtEntry {
constructor(json: any) { constructor(json: any) {
this.id = json.id
this.amount = json.amount this.amount = json.amount
this.date = json.date this.date = json.date
this.email = json.email this.email = json.email
@ -27,6 +28,9 @@ export class GdtEntry {
this.gdt = json.gdt this.gdt = json.gdt
} }
@Field(() => Number)
id: number
@Field(() => Number) @Field(() => Number)
amount: number amount: number

View File

@ -1,20 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
import { User } from './User'
// temporaray solution until we have JWT implemented
@ObjectType()
export class LoginResponse {
constructor(json: any) {
this.sessionId = json.session_id
this.user = new User(json.user)
}
@Field(() => Number)
sessionId: number
@Field(() => User)
user: User
}

View File

@ -1,13 +1,17 @@
import { Resolver, Query, /* Mutation, */ Arg } from 'type-graphql' /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import CONFIG from '../../config' import CONFIG from '../../config'
import { Balance } from '../models/Balance' import { Balance } from '../models/Balance'
import { apiGet } from '../../apis/loginAPI' import { apiGet } from '../../apis/loginAPI'
@Resolver() @Resolver()
export class BalanceResolver { export class BalanceResolver {
@Authorized()
@Query(() => Balance) @Query(() => Balance)
async balance(@Arg('sessionId') sessionId: number): Promise<Balance> { async balance(@Ctx() context: any): Promise<Balance> {
const result = await apiGet(CONFIG.COMMUNITY_API_URL + 'getBalance/' + sessionId) const result = await apiGet(CONFIG.COMMUNITY_API_URL + 'getBalance/' + context.sessionId)
if (!result.success) throw new Error(result.data) if (!result.success) throw new Error(result.data)
return new Balance(result.data) return new Balance(result.data)
} }

View File

@ -1,5 +1,7 @@
// import jwt from 'jsonwebtoken' /* eslint-disable @typescript-eslint/no-explicit-any */
import { Resolver, Query, /* Mutation, */ Args } from 'type-graphql' /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Args, Ctx, Authorized } from 'type-graphql'
import CONFIG from '../../config' import CONFIG from '../../config'
import { GdtEntryList } from '../models/GdtEntryList' import { GdtEntryList } from '../models/GdtEntryList'
import { GdtTransactionSessionIdInput } from '../inputs/GdtInputs' import { GdtTransactionSessionIdInput } from '../inputs/GdtInputs'
@ -7,14 +9,16 @@ import { apiGet } from '../../apis/loginAPI'
@Resolver() @Resolver()
export class GdtResolver { export class GdtResolver {
@Authorized()
@Query(() => GdtEntryList) @Query(() => GdtEntryList)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
async listGDTEntries( async listGDTEntries(
@Args() @Args()
{ currentPage = 1, pageSize = 5, order = 'DESC', sessionId }: GdtTransactionSessionIdInput, { currentPage = 1, pageSize = 5, order = 'DESC' }: GdtTransactionSessionIdInput,
@Ctx() context: any,
): Promise<GdtEntryList> { ): Promise<GdtEntryList> {
const result = await apiGet( const result = await apiGet(
`${CONFIG.COMMUNITY_API_URL}listGDTTransactions/${currentPage}/${pageSize}/${order}/${sessionId}`, `${CONFIG.COMMUNITY_API_URL}listGDTTransactions/${currentPage}/${pageSize}/${order}/${context.sessionId}`,
) )
if (!result.success) { if (!result.success) {
throw new Error(result.data) throw new Error(result.data)

View File

@ -1,4 +1,7 @@
import { Resolver, Query, Args } from 'type-graphql' /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Args, Authorized, Ctx } from 'type-graphql'
import CONFIG from '../../config' import CONFIG from '../../config'
import { TransactionList } from '../models/Transaction' import { TransactionList } from '../models/Transaction'
import { TransactionListInput, TransactionSendArgs } from '../inputs/TransactionInput' import { TransactionListInput, TransactionSendArgs } from '../inputs/TransactionInput'
@ -6,23 +9,27 @@ import { apiGet, apiPost } from '../../apis/loginAPI'
@Resolver() @Resolver()
export class TransactionResolver { export class TransactionResolver {
@Authorized()
@Query(() => TransactionList) @Query(() => TransactionList)
async transactionList( async transactionList(
@Args() { sessionId, firstPage = 1, items = 25, order = 'DESC' }: TransactionListInput, @Args() { firstPage = 1, items = 25, order = 'DESC' }: TransactionListInput,
@Ctx() context: any,
): Promise<TransactionList> { ): Promise<TransactionList> {
const result = await apiGet( const result = await apiGet(
`${CONFIG.COMMUNITY_API_URL}listTransactions/${firstPage}/${items}/${order}/${sessionId}`, `${CONFIG.COMMUNITY_API_URL}listTransactions/${firstPage}/${items}/${order}/${context.sessionId}`,
) )
if (!result.success) throw new Error(result.data) if (!result.success) throw new Error(result.data)
return new TransactionList(result.data) return new TransactionList(result.data)
} }
@Authorized()
@Query(() => String) @Query(() => String)
async sendCoins( async sendCoins(
@Args() { sessionId, email, amount, memo }: TransactionSendArgs, @Args() { email, amount, memo }: TransactionSendArgs,
@Ctx() context: any,
): Promise<string> { ): Promise<string> {
const payload = { const payload = {
session_id: sessionId, session_id: context.sessionId,
target_email: email, target_email: email,
amount: amount * 10000, amount: amount * 10000,
memo, memo,

View File

@ -1,11 +1,14 @@
// import jwt from 'jsonwebtoken' /* eslint-disable @typescript-eslint/no-explicit-any */
import { Resolver, Query, Args, Arg } from 'type-graphql' /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Args, Arg, Authorized, Ctx } from 'type-graphql'
import CONFIG from '../../config' import CONFIG from '../../config'
import { CheckUsernameResponse } from '../models/CheckUsernameResponse' import { CheckUsernameResponse } from '../models/CheckUsernameResponse'
import { LoginResponse } from '../models/LoginResponse'
import { LoginViaVerificationCode } from '../models/LoginViaVerificationCode' import { LoginViaVerificationCode } from '../models/LoginViaVerificationCode'
import { SendPasswordResetEmailResponse } from '../models/SendPasswordResetEmailResponse' import { SendPasswordResetEmailResponse } from '../models/SendPasswordResetEmailResponse'
import { UpdateUserInfosResponse } from '../models/UpdateUserInfosResponse' import { UpdateUserInfosResponse } from '../models/UpdateUserInfosResponse'
import { User } from '../models/User'
import encode from '../../jwt/encode'
import { import {
ChangePasswordArgs, ChangePasswordArgs,
CheckUsernameArgs, CheckUsernameArgs,
@ -17,8 +20,8 @@ import { apiPost, apiGet } from '../../apis/loginAPI'
@Resolver() @Resolver()
export class UserResolver { export class UserResolver {
@Query(() => LoginResponse) @Query(() => User)
async login(@Args() { email, password }: UnsecureLoginArgs): Promise<LoginResponse> { async login(@Args() { email, password }: UnsecureLoginArgs, @Ctx() context: any): Promise<User> {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
const result = await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', { email, password }) const result = await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', { email, password })
@ -27,21 +30,9 @@ export class UserResolver {
throw new Error(result.data) throw new Error(result.data)
} }
// temporary solution until we have JWT implemented context.setHeaders.push({ key: 'token', value: encode(result.data.session_id) })
return new LoginResponse(result.data)
// create and return the json web token return new User(result.data.user)
// The expire doesn't help us here. The client needs to track when the token expires on its own,
// since every action prolongs the time the session is valid.
/*
return jwt.sign(
{ result, role: 'todo' },
CONFIG.JWT_SECRET, // * , { expiresIn: CONFIG.JWT_EXPIRES_IN } ,
)
*/
// return (await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', login)).result.data
// const loginResult: LoginResult = await loginAPI.login(data)
// return loginResult.user ? loginResult.user : new User()
} }
@Query(() => LoginViaVerificationCode) @Query(() => LoginViaVerificationCode)
@ -59,9 +50,10 @@ export class UserResolver {
return new LoginViaVerificationCode(result.data) return new LoginViaVerificationCode(result.data)
} }
@Authorized()
@Query(() => String) @Query(() => String)
async logout(@Arg('sessionId') sessionId: number): Promise<string> { async logout(@Ctx() context: any): Promise<string> {
const payload = { session_id: sessionId } const payload = { session_id: context.sessionId }
const result = await apiPost(CONFIG.LOGIN_API_URL + 'logout', payload) const result = await apiPost(CONFIG.LOGIN_API_URL + 'logout', payload)
if (!result.success) { if (!result.success) {
throw new Error(result.data) throw new Error(result.data)
@ -115,11 +107,11 @@ export class UserResolver {
return 'sucess' return 'sucess'
} }
@Authorized()
@Query(() => UpdateUserInfosResponse) @Query(() => UpdateUserInfosResponse)
async updateUserInfos( async updateUserInfos(
@Args() @Args()
{ {
sessionId,
email, email,
firstName, firstName,
lastName, lastName,
@ -129,9 +121,10 @@ export class UserResolver {
password, password,
passwordNew, passwordNew,
}: UpdateUserInfosArgs, }: UpdateUserInfosArgs,
@Ctx() context: any,
): Promise<UpdateUserInfosResponse> { ): Promise<UpdateUserInfosResponse> {
const payload = { const payload = {
session_id: sessionId, session_id: context.sessionId,
email, email,
update: { update: {
'User.first_name': firstName || undefined, 'User.first_name': firstName || undefined,

View File

@ -1,5 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import 'reflect-metadata' import 'reflect-metadata'
import express from 'express' import express from 'express'
import cors from 'cors'
import { buildSchema } from 'type-graphql' import { buildSchema } from 'type-graphql'
import { ApolloServer } from 'apollo-server-express' import { ApolloServer } from 'apollo-server-express'
import { RowDataPacket } from 'mysql2/promise' import { RowDataPacket } from 'mysql2/promise'
@ -8,17 +11,31 @@ import connection from './database/connection'
import CONFIG from './config' import CONFIG from './config'
// TODO move to extern // TODO move to extern
// import { BookResolver } from './graphql/resolvers/BookResolver'
import { UserResolver } from './graphql/resolvers/UserResolver' import { UserResolver } from './graphql/resolvers/UserResolver'
import { BalanceResolver } from './graphql/resolvers/BalanceResolver' import { BalanceResolver } from './graphql/resolvers/BalanceResolver'
import { GdtResolver } from './graphql/resolvers/GdtResolver' import { GdtResolver } from './graphql/resolvers/GdtResolver'
import { TransactionResolver } from './graphql/resolvers/TransactionResolver' import { TransactionResolver } from './graphql/resolvers/TransactionResolver'
import { isAuthorized } from './auth/auth'
// TODO implement // TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity"; // import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
const DB_VERSION = '0001-init_db' const DB_VERSION = '0001-init_db'
const context = (args: any) => {
const authorization = args.req.headers.authorization
let token = null
if (authorization) {
token = authorization.replace(/^Bearer /, '')
}
const context = {
token,
setHeaders: [],
}
return context
}
async function main() { async function main() {
// check for correct database version // check for correct database version
const con = await connection() const con = await connection()
@ -34,6 +51,7 @@ async function main() {
// const connection = await createConnection() // const connection = await createConnection()
const schema = await buildSchema({ const schema = await buildSchema({
resolvers: [UserResolver, BalanceResolver, TransactionResolver, GdtResolver], resolvers: [UserResolver, BalanceResolver, TransactionResolver, GdtResolver],
authChecker: isAuthorized,
}) })
// Graphiql interface // Graphiql interface
@ -45,8 +63,31 @@ async function main() {
// Express Server // Express Server
const server = express() 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 // Apollo Server
const apollo = new ApolloServer({ schema, playground }) const apollo = new ApolloServer({ schema, playground, context, plugins })
apollo.applyMiddleware({ app: server }) apollo.applyMiddleware({ app: server })
// Start Server // Start Server

20
backend/src/jwt/decode.ts Normal file
View File

@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import jwt from 'jsonwebtoken'
import CONFIG from '../config/'
export default (token: string): any => {
if (!token) return null
let sessionId = null
try {
const decoded = jwt.verify(token, CONFIG.JWT_SECRET)
sessionId = decoded.sub
return {
token,
sessionId,
}
} catch (err) {
return null
}
}

14
backend/src/jwt/encode.ts Normal file
View File

@ -0,0 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import jwt from 'jsonwebtoken'
import CONFIG from '../config/'
// Generate an Access Token
export default function encode(sessionId: string): string {
const token = jwt.sign({ sessionId }, CONFIG.JWT_SECRET, {
expiresIn: CONFIG.JWT_EXPIRES_IN,
subject: sessionId.toString(),
})
return token
}

View File

@ -21,7 +21,7 @@ $this->assign('title', __('GDT Kontoübersicht'));
$header = '<h3>' . __('Zur Verfügung: ') . '</h3>'; $header = '<h3>' . __('Zur Verfügung: ') . '</h3>';
if($gdtSum > 0){ if($gdtSum > 0){
$header .= '<h2>'.$this->element('printGDT', ['number' => $gdtSum]).'</h2>'; $header .= '<h2>'.$this->element('printGDT', ['number' => $gdtSum*100.0]).'</h2>';
} }
if($moreEntrysAsShown) { if($moreEntrysAsShown) {
$header .= '<span>'. __('Nur die letzten 100 Einträge werden angezeigt!') . '</span>'; $header .= '<span>'. __('Nur die letzten 100 Einträge werden angezeigt!') . '</span>';
@ -47,8 +47,12 @@ $this->assign('header', $header);
<div class="cell c3"><?= new FrozenTime($entry['date']) ?></div> <div class="cell c3"><?= new FrozenTime($entry['date']) ?></div>
<div class="cell c0"><?= h($entry['comment']) ?></div> <div class="cell c0"><?= h($entry['comment']) ?></div>
<div class="cell c3"> <div class="cell c3">
<?= $this->element('printEuro', ['number' => $entry['amount']]); ?> <?php if(intval($entry['gdt_entry_type_id']) == 7) : ?>
<?php if($entry['amount2']) echo ' + ' . $this->element('printEuro', ['number' => $entry['amount2']]) ?> <?= $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>
<div class="cell c2"> <div class="cell c2">
<?= $this->Number->format($entry['factor']) ?> <?= $this->Number->format($entry['factor']) ?>
@ -56,7 +60,7 @@ $this->assign('header', $header);
<?= $this->Number->format($entry['factor2']) ?> <?= $this->Number->format($entry['factor2']) ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="cell c3"><?= $this->element('printGDT', ['number' => $entry['gdt']]) ?></div> <div class="cell c3"><?= $this->element('printGDT', ['number' => $entry['gdt']*100.0]) ?></div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
@ -89,8 +93,12 @@ $this->assign('header', $header);
<!--<div class="cell c0"><?= h($elopageTransaction['email']) ?></div>--> <!--<div class="cell c0"><?= h($elopageTransaction['email']) ?></div>-->
<div class="cell c3"><?= new FrozenTime($gdtEntry['date']) ?></div> <div class="cell c3"><?= new FrozenTime($gdtEntry['date']) ?></div>
<div class="cell c3"> <div class="cell c3">
<?= $this->element('printEuro', ['number' => $gdtEntry['amount']]) ?> <?php if(intval($gdtEntry['gdt_entry_type_id']) == 7) : ?>
<?php if($gdtEntry['amount2']) echo ' + ' . $this->element('printEuro', ['number' => $gdtEntry['amount2']]) ?> <?= $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>
<div class="cell c2"> <div class="cell c2">
<?= $this->Number->format($gdtEntry['factor']) ?> <?= $this->Number->format($gdtEntry['factor']) ?>
@ -98,7 +106,7 @@ $this->assign('header', $header);
<?= $this->Number->format($gdtEntry['factor2']) ?> <?= $this->Number->format($gdtEntry['factor2']) ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="cell c3"><?= $this->element('printGDT', ['number' => $gdtEntry['gdt']]) ?></div> <div class="cell c3"><?= $this->element('printGDT', ['number' => $gdtEntry['gdt'] * 100.0]) ?></div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>

View File

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

View File

@ -3,8 +3,7 @@
# or NPM_BIN Path and NVM_DIR must be adjusted # or NPM_BIN Path and NVM_DIR must be adjusted
cd /var/www/html/gradido cd /var/www/html/gradido
eval "echo \"$(cat .env.local)\"" > .env eval "echo \"$(cat .env.shell)\"" > .env
eval "echo \"$(cat .env.shell)\"" >> .env
cd frontend cd frontend
NPM_BIN=/root/.nvm/versions/node/v12.19.0/bin/npm NPM_BIN=/root/.nvm/versions/node/v12.19.0/bin/npm

View File

@ -13,7 +13,6 @@ services:
environment: environment:
- NODE_ENV="development" - NODE_ENV="development"
# - DEBUG=true # - DEBUG=true
- NUXT_BUILD=/tmp/nuxt # avoid file permission issues when `rm -rf .nuxt/`
volumes: volumes:
# This makes sure the docker container has its own node modules. # This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine # Therefore it is possible to have a different node version on the host machine

View File

@ -22,13 +22,12 @@ services:
# Envs used in Dockerfile # Envs used in Dockerfile
# - DOCKER_WORKDIR="/app" # - DOCKER_WORKDIR="/app"
# - PORT=3000 # - PORT=3000
- BUILD_DATE # - BUILD_DATE="1970-01-01T00:00:00.00Z"
- BUILD_VERSION # - BUILD_VERSION="0.0.0.0"
- BUILD_COMMIT # - BUILD_COMMIT="0000000"
- NODE_ENV="production" - NODE_ENV="production"
# Application only envs env_file:
#- HOST=0.0.0.0 # This is nuxt specific, alternative value is HOST=webapp - ./.env
#env_file:
# - ./frontend/.env # - ./frontend/.env
######################################################### #########################################################

View File

@ -0,0 +1,53 @@
# Roles
User Roles also handled by blockchain and node servers
My Goal is to save not only the gradido transactions in blockchain but also
who is allowed to create new gradidos,
who allow joining new user to the group,
who allow connect to another group and
how the community decide which one is allowed to do this things.
## Why?
If this would be handled only by community-server everyone could be easly
overwrite this rules by using a modified client to send his transactions direct over
hedera or iota bypassing the community-server rules.
With hedera it is possible to only allow sending messages to a topic with the correct admin key,
but then is the admin the single point of failure. Also must the key saved on server to allow everyone
sending gradidos or the transactions will only be proccessed, when admin is logging in.
If we don't use blockchain technologie at all, we have a big single point of failure.
The Community-Server and everyone who has direct access to server and the admins of course.
But it would be much much simpler of course :)
In summary it is to make sure that the community is in power and no one can take over.
## How?
There is a special type of transactions with which users determine who can determine what.
This transaction control which signatures are neccessary for things like creation and so one.
For this I think different types are needed.
- *one*: The founder of group (or someone other choosen) decide everything, this is default from start
- *some*: a number of user must sign, set absolute count or relative count
- *most*: more than 1/2 or 3/4 must sign
- *all*: all member must sign, default for choose which mode will be used
- *one-council*: one member of council
- *some-council*: absolute or relative number of council members must sign
- *most-council*: more than 1/2 or 3/4 from council members must sign
- *all-council*: all members of council must sign
this configuration can be done for different types of action,
so the voting-mode for creation may differ from voting mode for
add new members to community. Also the council members for different actions
may differ.
Also how to vote for council members is an extra type of action.
## Veto
Especially for *some* and *some-council* maybe also for other types.
The users there could vote but haven't yet and not all need to vote,
can make a Veto with Explanation which reset all existing signs of current vote,
and is needed to sign by all which again vote for the case.
## Summary
With that setup all community consense models should be possible except Democracy.
Democracy needs a secret ballot. The votes on blockchain are open (at least if someone knows which
public-key belongs to which user). A secret ballot on blockchain is really hard. By my last
recherche I haven't found on. But maybe this can do the trick: https://secure.vote/

View File

@ -0,0 +1,42 @@
# Wie Colored Coins in Iota funktionieren
## Schöpfung
- Colored Coins werden bei Iota mit einer speziellen Transaktion erzeugt
- Die Farbe des neuen Coins wird durch den Transaktionshash beschrieben
- Die einmal erzeugte Menge von Colored Coins ist fest
- Um die Menge zu erhöhen müssten neue Colored Coin erzeugt werden und die alten damit ausgetauscht werden (z.B. mittels SmartContract)
## Geldmenge
- Colored Coins basierend auf den normalen Iota-Coins somit werden soviele Iota-Coins benötigt wie man Colored-Coins braucht
- 2.779.530.283.000.000 Iota maximale Coinmenge
- Weltbevölkerung 2021: 7.915.559.780
- Pro Kopf Geldmenge Gradido: 53.476
- Benötigte Iota Coins für Gradido mit 4 Nachkommastellen mit 25% Puffer:
- 7.915.559.780 * 534.760.000 * 1.25 = 5.291.155.935.000.000.000
- Vorhandene Iota coins: 2.779.530.283.000.000
- Es sind nicht genügend Iota Coins vorhanden im die ganze Welt mit Gradido auf Basis von Iota versorgen zu können
## Kosten
- Kurs am 30.08.2021: 1 Miota = 0,84
- Bei Verwendung von 4 Nachkommastellen braucht es 10 Miota für 1.000 Colored Coins (Gradido) also Miota im Wert von 8,40€
- Aktuell (30.08.2021) geschöpfte Gradido Cent: 17.001.990.500
- Notwendige Miota: 17.002.0, Wert: 14.286,73 €
- Solange die Benutzer Kontrolle über ihre Keys haben können sie mit einem regulärem Iota Wallet die Gradidos wieder in Iota umwandeln (um z.B. der Vergänglichkeit zu entgehen)
- Mit 2 Nachkommastellen wird die Vergänglichkeit schon bei 100 Gradido und 1 Stunde ungenau
- 1 Stunde, 100 Gradido, Vergänglichkeit: 0,00576, Gradidos nach einer Stunde: 99,99424 GDD
- 1 Minute, 100 Gradido, Vergänglichkeit: 0,000096, Gradidos nach einer Minute: 99,999904 GDD
## Dust-Protection
- Iota erlaubt bei leeren Adressen nur Transaktionen von mindestens 1 Miota
- Nachdem 1 Miota da ist sind bis zu 10 Transaktione < 1 Miota erlaubt, bevor ein weitere Transaktion mit mindestens 1 Miota eingehen muss
- Bei Verwendung von 4 Nachkommastellen entspricht das 100 GDD, bei 2 Nachkommastellen 10.000 GDD
## Lösung
Wir können nur 3 Nachkommastellen verwenden.
### Kosten
- 0,84 € für 1.000 GDD
- 1.428 Euro für alle bisherigen geschöpften Gradidos
### Dust-Protection
- 1.000 GDD entspricht 1 Miota, die erste Schöpfung muss also zwangsläufig 1k Gradido betragen
- Jeder kann nur maximal 10 Transaktionen < 1k GDD empfangen bevor er die GDD weitergesendet haben muss um neue erhalten zu können oder eine neue Schöpfung von 1k GDD bekommen hat

View File

@ -0,0 +1 @@
<mxfile host="7d619c52-f8a9-4b9d-82fe-1aa45d350dea" modified="2021-08-07T11:49:26.921Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.58.2 Chrome/89.0.4389.128 Electron/12.0.13 Safari/537.36" etag="5tat3B3yItVGc6mcvaQ1" version="12.2.4" pages="1"><diagram id="1SuIEZF2QCnWeueF3Zz_" name="Page-1">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</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View File

@ -0,0 +1 @@
<mxfile host="11f51089-224b-4c51-8025-2a587ac1ee54" modified="2021-08-08T15:21:15.636Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.58.2 Chrome/89.0.4389.128 Electron/12.0.13 Safari/537.36" etag="Ph0quYraKNcgZ1LpaOqw" version="12.2.4" pages="1"><diagram id="svTVmLC50Oi8Ao2eC_wi" name="Page-1">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</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View File

@ -0,0 +1 @@
<mxfile host="c8c9d8b1-9d45-405b-9350-947d02163aae" modified="2021-08-13T14:14:22.645Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.59.0 Chrome/91.0.4472.124 Electron/13.1.7 Safari/537.36" etag="M5WsFcVCZVYdDRxEgPr7" version="12.2.4" pages="1"><diagram id="4VYGDKFA-XGvTikjH5B0" name="Page-1">zVlLc+MoEP41Os6U9bRz9GMym9qaSmqTqp3sjUjYYoOEF6HYmV+/DQJLCHnieJRkLjK0oNV099cP7IXLYv+Vo23+jWWYesEk23vhyguCaRLAUxKeNWEaN4QNJ1lD8lvCLfmBNXGiqTXJcGUtFIxRQbY2MWVliVNh0RDnbGcvWzNqf3WLNtgh3KaIutS/SSbyhjoLpi39D0w2ufmyn1w0bwpkFuuTVDnK2K5DCr944ZIzJppRsV9iKnVn9NLsuzzy9iAYx6U4aYNW/BOitT7c1fXdXAsnns2JYRcoFyYLkHgriSllNTBZ7HIi8O0WpZK4A3MDLRcFhZkPQ1cgLeMT5gLvOyQt4FfMCiz4MyzRb8MLrSztLWa6a1XvG33mHbXPNA1pa28OnFuFwEDr5Ih+Ekc/4NIZydgdR2WFUkFY6WiLs7rMsGQxeRcNzWwF+ZMTNZSMoaGpoyHXe8psLmGn3AZVFUltFeA9Ed8743upuc+xnq32WpFq8mwmJQj6XS0EI+m53hibebtVzay9N5gTOC3mmnjUDhWreYptfxCIb7BZpjGEMytquNbqmCMesIahcUyRIE92rBkykf7CDSMg8TFnCKKekZvj6E3d0NDj44c9r7roMWp04DBSDnM49Wk+NHNR9td8dbW6/uwFCSokYihoaYEoga8b0kYczNZxNsCMsN2L44r8QA9qgbT0VoqsDhEvvHgl+daCVU2a8ZvPbEoYU7yWrCQQCUT/uSYLJr9eAaJJubmTk9WnaBwkB7OoB+WZC2V/wHmCEaBseHTMQEklcKnyKzxeYZREvXrgMFIUi5FeWj5U8qcgFFeClZDRg0tI2WvCCwiefbuaxLPlLMVV9XJgfUDp40aF4utaUFIa646RknrYCEI34gZDZhoj4poA+ysR97Rg91sHtrCf308NbFEQfp5Mkwswmnr6w6YcP8wFoWO4GzCUDCJtMVEddXwQAPMS0VvBuCpQPxAA8cv+7ydvVJMF0Xv5/0CyNzb8TTDhJOnpudm+Xza8IQzcnsPki9w3+eKfGlY8Um8ZevOIy8zRrADm7SJD3BrCn6woVJbJCIbnN8gTgJRKfkyW6ZMrJpDsHykmaQ6DEqmfDBfS9GoPYKAC78FSN7uaZ4odQEgeAJhzxYsfNh2SlxcsFZEfOD3Ai0Z+QTaSXY7kkzVrHo2ozWJUK0kKpBplpAQLLzun3raHfrHgqQRnj3jJKJPFbSmlCxdrQmmPpEsYIMROQRMca10YaGhNFbRykkn9QIVlNTxjxJfIdsckdPPrJHZh5ffd9qwA45ajxhCgo9LSfvJfLXv1RlOfqkZVc+lajCtjmgXGfGvpEouldIF/cSZNrxtKGFk95WSHaZpj5XfSl1tHcz1LOWrHWRoxj/jLGd2pY7gB8x4vluLYTtzRicnCH6OoDd2i9tXZwvSnbU9633kz3J8OpokXU445sZVyZh+ZYXqZPj67nZzYjKK3aydNRfczk787CCIbBLE/AILZgBnH6BjCETqGzn3L4Xrl3uvergzftZyJg5mLg/BDK60eDpJ+bDoXB3G/YhsRB0P9BmSlkqm8s8w5K0kqHeByxKLCXJ6kuFSXa871SQFVAz3WvgxVEr8EvCjqV8ixC7zJT/zmFcCDaXt13xis/f8j/PI/</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -2,3 +2,4 @@ LOGIN_API_URL=http://localhost/login_api/
COMMUNITY_API_URL=http://localhost/api/ COMMUNITY_API_URL=http://localhost/api/
ALLOW_REGISTER=true ALLOW_REGISTER=true
GRAPHQL_URI=http://localhost:4000/graphql GRAPHQL_URI=http://localhost:4000/graphql
//BUILD_COMMIT=0000000

View File

@ -15,7 +15,6 @@ describe('LanguageSwitch', () => {
let wrapper let wrapper
const state = { const state = {
sessionId: 1234,
email: 'he@ho.he', email: 'he@ho.he',
language: null, language: null,
} }
@ -123,7 +122,6 @@ describe('LanguageSwitch', () => {
expect(updateUserInfosQueryMock).toBeCalledWith( expect(updateUserInfosQueryMock).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1234,
email: 'he@ho.he', email: 'he@ho.he',
locale: 'en', locale: 'en',
}, },
@ -136,7 +134,6 @@ describe('LanguageSwitch', () => {
expect(updateUserInfosQueryMock).toBeCalledWith( expect(updateUserInfosQueryMock).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1234,
email: 'he@ho.he', email: 'he@ho.he',
locale: 'de', locale: 'de',
}, },

View File

@ -32,13 +32,13 @@ export default {
localeChanged(locale) localeChanged(locale)
}, },
async saveLocale(locale) { async saveLocale(locale) {
// if (this.$i18n.locale === locale) return
this.setLocale(locale) this.setLocale(locale)
if (this.$store.state.sessionId && this.$store.state.email) { if (this.$store.state.email) {
this.$apollo this.$apollo
.query({ .query({
query: updateUserInfos, query: updateUserInfos,
variables: { variables: {
sessionId: this.$store.state.sessionId,
email: this.$store.state.email, email: this.$store.state.email,
locale: locale, locale: locale,
}, },

View File

@ -0,0 +1,31 @@
import { mount } from '@vue/test-utils'
import Transaction from './Transaction'
const localVue = global.localVue
describe('Transaction', () => {
let wrapper
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$n: jest.fn((n) => n),
$d: jest.fn((d) => d),
}
const Wrapper = () => {
return mount(Transaction, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div.gdt-transaction-list-item').exists()).toBeTruthy()
})
})
})

View File

@ -0,0 +1,124 @@
<template>
<div>
<div class="list-group">
<div class="list-group-item gdt-transaction-list-item" v-b-toggle="'a' + date + ''">
<!-- icon -->
<div class="text-right" style="position: absolute">
<b-icon
:icon="getLinesByType(gdtEntryType).icon"
:class="getLinesByType(gdtEntryType).iconclasses"
></b-icon>
</div>
<!-- collaps Button -->
<div class="text-right" style="width: 96%; position: absolute">
<b-button class="btn-sm">
<b>i</b>
</b-button>
</div>
<!-- type -->
<b-row>
<div class="col-6 text-right">
{{ getLinesByType(gdtEntryType).description }}
</div>
<div class="col-6">
{{ getLinesByType(gdtEntryType).descriptiontext }}
</div>
</b-row>
<!-- credit -->
<b-row>
<div class="col-6 text-right">
{{ $t('gdt.credit') }}
</div>
<div class="col-6">
{{ getLinesByType(gdtEntryType).credittext }}
</div>
</b-row>
<!-- Message-->
<b-row v-if="comment && gdtEntryType !== 7">
<div class="col-6 text-right">
{{ $t('form.memo') }}
</div>
<div class="col-6">
{{ comment }}
</div>
</b-row>
<!-- date-->
<b-row class="gdt-list-row text-header">
<div class="col-6 text-right">
{{ $t('form.date') }}
</div>
<div class="col-6">
{{ $d($moment(date), 'long') }} {{ $i18n.locale === 'de' ? 'Uhr' : '' }}
</div>
</b-row>
</div>
<!-- collaps trancaction info-->
<b-collapse :id="'a' + date + ''" class="pb-4">
<transaction-collapse
:amount="amount"
:gdtEntryType="gdtEntryType"
:factor="factor"
:gdt="gdt"
></transaction-collapse>
</b-collapse>
</div>
</div>
</template>
<script>
import TransactionCollapse from './TransactionCollapse.vue'
export default {
name: 'Transaction',
components: {
TransactionCollapse,
},
props: {
amount: { type: Number },
date: { type: String },
comment: { type: String },
gdtEntryType: { type: Number, default: 1 },
factor: { type: Number },
gdt: { type: Number },
},
methods: {
getLinesByType(givenType) {
if (givenType === 2 || givenType === 3 || givenType === 5 || givenType === 6) givenType = 1
const linesByType = {
1: {
icon: 'heart',
iconclasses: 'gradido-global-color-accent m-mb-1 font2em',
description: this.$t('gdt.contribution'),
descriptiontext: this.$n(this.amount, 'decimal') + ' €',
credittext: this.$n(this.gdt, 'decimal') + ' GDT',
},
4: {
icon: 'person-check',
iconclasses: 'gradido-global-color-accent m-mb-1 font2em',
description: this.$t('gdt.recruited-member'),
descriptiontext: '5%',
credittext: this.$n(this.amount, 'decimal') + ' GDT',
},
7: {
icon: 'gift',
iconclasses: 'gradido-global-color-accent m-mb-1 font2em',
description: this.$t('gdt.gdt-received'),
descriptiontext: this.comment,
credittext: this.$n(this.gdt, 'decimal') + ' GDT',
},
}
const type = linesByType[givenType]
if (type) return type
throw new Error('no lines for this type: ' + givenType)
},
},
}
</script>

View File

@ -0,0 +1,152 @@
import { mount } from '@vue/test-utils'
import TransactionCollapse from './TransactionCollapse'
const localVue = global.localVue
describe('TransactionCollapse', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$n: jest.fn((n) => n),
}
const Wrapper = (propsData) => {
return mount(TransactionCollapse, { localVue, mocks, propsData })
}
describe('mount with gdtEntryType: 1', () => {
beforeEach(() => {
const propsData = {
amount: 100,
gdt: 110,
factor: 22,
gdtEntryType: 1,
}
wrapper = Wrapper(propsData)
})
it('renders the component', () => {
expect(wrapper.find('div.gdt-transaction-collapse').exists()).toBeTruthy()
})
it('checks the prop gdtEntryType ', () => {
expect(wrapper.props().gdtEntryType).toBe(1)
})
it('renders the component collapse-header', () => {
expect(wrapper.find('.gdt-list-collapse-header-text')).toBeTruthy()
})
it('renders the component collapse-headline', () => {
expect(wrapper.find('#collapse-headline').text()).toBe('gdt.calculation')
})
it('renders the component collapse-first', () => {
expect(wrapper.find('#collapse-first').text()).toBe('gdt.factor')
})
it('renders the component collapse-second', () => {
expect(wrapper.find('#collapse-second').text()).toBe('gdt.formula')
})
it('renders the component collapse-firstMath', () => {
expect(wrapper.find('#collapse-firstMath').text()).toBe('22 GDT pro €')
})
it('renders the component collapse-secondMath', () => {
expect(wrapper.find('#collapse-secondMath').text()).toBe('100 € * 22 GDT / € = 110 GDT')
})
})
describe('mount with gdtEntryType: 7', () => {
beforeEach(() => {
const propsData = {
amount: 100,
gdt: 2200,
factor: 22,
gdtEntryType: 7,
}
wrapper = Wrapper(propsData)
})
it('renders the component', () => {
expect(wrapper.find('div.gdt-transaction-collapse').exists()).toBeTruthy()
})
it('checks the prop gdtEntryType ', () => {
expect(wrapper.props().gdtEntryType).toBe(7)
})
it('renders the component collapse-header', () => {
expect(wrapper.find('.gdt-list-collapse-header-text')).toBeTruthy()
})
it('renders the component collapse-headline', () => {
expect(wrapper.find('#collapse-headline').text()).toBe('gdt.conversion-gdt-euro')
})
it('renders the component collapse-first', () => {
expect(wrapper.find('#collapse-first').text()).toBe('gdt.raise')
})
it('renders the component collapse-second', () => {
expect(wrapper.find('#collapse-second').text()).toBe('gdt.conversion')
})
it('renders the component collapse-firstMath', () => {
expect(wrapper.find('#collapse-firstMath').text()).toBe('2200 %')
})
it('renders the component collapse-secondMath', () => {
expect(wrapper.find('#collapse-secondMath').text()).toBe('100 GDT * 2200 % = 2200 GDT')
})
})
describe('mount with gdtEntryType: 4', () => {
beforeEach(() => {
const propsData = {
amount: 100,
gdt: 2200,
factor: 22,
gdtEntryType: 4,
}
wrapper = Wrapper(propsData)
})
it('renders the component', () => {
expect(wrapper.find('div.gdt-transaction-collapse').exists()).toBeTruthy()
})
it('checks the prop gdtEntryType ', () => {
expect(wrapper.props().gdtEntryType).toBe(4)
})
it('renders the component collapse-header', () => {
expect(wrapper.find('.gdt-list-collapse-header-text')).toBeTruthy()
})
it('renders the component collapse-headline', () => {
expect(wrapper.find('#collapse-headline').text()).toBe('gdt.publisher')
})
it('renders the component collapse-first', () => {
expect(wrapper.find('#collapse-first').text()).toBe('')
})
it('renders the component collapse-second', () => {
expect(wrapper.find('#collapse-second').text()).toBe('')
})
it('renders the component collapse-firstMath', () => {
expect(wrapper.find('#collapse-firstMath').text()).toBe('')
})
it('renders the component collapse-secondMath', () => {
expect(wrapper.find('#collapse-secondMath').text()).toBe('')
})
})
})

View File

@ -0,0 +1,76 @@
<template>
<div class="gdt-transaction-collapse">
<b-row class="gdt-list-collapse-header-text text-center pb-3">
<div id="collapse-headline" class="col h4">
{{ getLinesByType(gdtEntryType).headline }}
</div>
</b-row>
<b-row class="gdt-list-collapse-box--all">
<div class="col-6 text-right collapse-col-left">
<div id="collapse-first">{{ getLinesByType(gdtEntryType).first }}</div>
<div id="collapse-second">{{ getLinesByType(gdtEntryType).second }}</div>
</div>
<div class="col-6 collapse-col-right">
<div id="collapse-firstMath">{{ getLinesByType(gdtEntryType).firstMath }}</div>
<div id="collapse-secondMath">
{{ getLinesByType(gdtEntryType).secondMath }}
</div>
</div>
</b-row>
</div>
</template>
<script>
export default {
name: 'TransactionCollapse',
props: {
amount: { type: Number },
gdtEntryType: { type: Number, default: 1 },
factor: { type: Number },
gdt: { type: Number },
},
methods: {
getLinesByType(givenType) {
const linesByType = {
1: {
headline: this.$t('gdt.calculation'),
first: this.$t('gdt.factor'),
firstMath: this.factor + ' GDT pro €',
second: this.$t('gdt.formula'),
secondMath:
this.$n(this.amount, 'decimal') +
' € * ' +
this.factor +
' GDT / € = ' +
this.$n(this.gdt, 'decimal') +
' GDT',
},
4: {
headline: this.$t('gdt.publisher'),
first: null,
firstMath: null,
second: null,
secondMath: null,
},
7: {
headline: this.$t('gdt.conversion-gdt-euro'),
first: this.$t('gdt.raise'),
firstMath: this.factor * 100 + ' % ',
second: this.$t('gdt.conversion'),
secondMath:
this.$n(this.amount, 'decimal') +
' GDT * ' +
this.factor * 100 +
' % = ' +
this.$n(this.gdt, 'decimal') +
' GDT',
},
}
const type = linesByType[givenType]
if (type) return type
throw new Error('no additional transaction info for this type: ' + givenType)
},
},
}
</script>

View File

@ -1,13 +1,20 @@
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env) // ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env).
// The whole contents is exposed to the client
// Load Package Details for some default values // Load Package Details for some default values
const pkg = require('../../package') const pkg = require('../../package')
const version = {
APP_VERSION: pkg.version,
BUILD_COMMIT: process.env.BUILD_COMMIT || null,
// self reference of `version.BUILD_COMMIT` is not possible at this point, hence the duplicate code
BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT || '0000000').substr(0, 7),
}
const environment = { const environment = {
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
DEBUG: process.env.NODE_ENV !== 'production' || false, DEBUG: process.env.NODE_ENV !== 'production' || false,
PRODUCTION: process.env.NODE_ENV === 'production' || false, PRODUCTION: process.env.NODE_ENV === 'production' || false,
ALLOW_REGISTER: process.env.ALLOW_REGISTER !== 'false',
} }
const server = { const server = {
@ -16,13 +23,15 @@ const server = {
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql', GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql',
} }
// eslint-disable-next-line no-console const options = {
console.log('hash: %o', process.env.VUE_APP_BUILD_COMMIT) ALLOW_REGISTER: process.env.ALLOW_REGISTER !== 'false',
}
const CONFIG = { const CONFIG = {
...version,
...environment, ...environment,
...server, ...server,
APP_VERSION: pkg.version, ...options,
COMMIT_HASH: COMMIT_HASH:
process.env.VUE_APP_BUILD_COMMIT === 'undefined' process.env.VUE_APP_BUILD_COMMIT === 'undefined'
? '00000000' ? '00000000'

View File

@ -3,22 +3,19 @@ import gql from 'graphql-tag'
export const login = gql` export const login = gql`
query($email: String!, $password: String!) { query($email: String!, $password: String!) {
login(email: $email, password: $password) { login(email: $email, password: $password) {
sessionId
user {
email email
username
firstName firstName
lastName lastName
language language
username
description description
} }
} }
}
` `
export const logout = gql` export const logout = gql`
query($sessionId: Float!) { query {
logout(sessionId: $sessionId) logout
} }
` `
@ -39,7 +36,6 @@ export const loginViaEmailVerificationCode = gql`
export const updateUserInfos = gql` export const updateUserInfos = gql`
query( query(
$sessionId: Float!
$email: String! $email: String!
$firstName: String $firstName: String
$lastName: String $lastName: String
@ -50,7 +46,6 @@ export const updateUserInfos = gql`
$locale: String $locale: String
) { ) {
updateUserInfos( updateUserInfos(
sessionId: $sessionId
email: $email email: $email
firstName: $firstName firstName: $firstName
lastName: $lastName lastName: $lastName
@ -66,8 +61,8 @@ export const updateUserInfos = gql`
` `
export const transactionsQuery = gql` export const transactionsQuery = gql`
query($sessionId: Float!, $firstPage: Int = 1, $items: Int = 25, $order: String = "DESC") { query($firstPage: Int = 1, $items: Int = 25, $order: String = "DESC") {
transactionList(sessionId: $sessionId, firstPage: $firstPage, items: $items, order: $order) { transactionList(firstPage: $firstPage, items: $items, order: $order) {
gdtSum gdtSum
count count
balance balance
@ -103,8 +98,8 @@ export const resgisterUserQuery = gql`
` `
export const sendCoins = gql` export const sendCoins = gql`
query($sessionId: Float!, $email: String!, $amount: Float!, $memo: String!) { query($email: String!, $amount: Float!, $memo: String!) {
sendCoins(sessionId: $sessionId, email: $email, amount: $amount, memo: $memo) sendCoins(email: $email, amount: $amount, memo: $memo)
} }
` `
@ -125,10 +120,11 @@ export const checkUsername = gql`
` `
export const listGDTEntriesQuery = gql` export const listGDTEntriesQuery = gql`
query($currentPage: Int!, $pageSize: Int!, $sessionId: Float!) { query($currentPage: Int!, $pageSize: Int!) {
listGDTEntries(currentPage: $currentPage, pageSize: $pageSize, sessionId: $sessionId) { listGDTEntries(currentPage: $currentPage, pageSize: $pageSize) {
count count
gdtEntries { gdtEntries {
id
amount amount
date date
comment comment

View File

@ -18,6 +18,7 @@
"de": "Deutsch", "de": "Deutsch",
"en": "English" "en": "English"
}, },
"select_language": "Bitte wähle eine Sprache für die App und Newsletter",
"decay": { "decay": {
"decay": "Vergänglichkeit", "decay": "Vergänglichkeit",
"decay_since_last_transaction":"Vergänglichkeit seit der letzten Transaktion", "decay_since_last_transaction":"Vergänglichkeit seit der letzten Transaktion",
@ -136,15 +137,6 @@
"send_gradido":"Gradido versenden", "send_gradido":"Gradido versenden",
"add_work":"neuer Gemeinschaftsbeitrag" "add_work":"neuer Gemeinschaftsbeitrag"
}, },
"profil": {
"activity": {
"new":"Neue Gemeinschaftsstunden eintragen",
"list":"Meine Gemeinschaftsstunden Liste"
},
"user-data": {
"change-success": "Deine Daten wurden gespeichert."
}
},
"navbar" : { "navbar" : {
"my-profil":"Mein Profil", "my-profil":"Mein Profil",
"settings":"Einstellungen", "settings":"Einstellungen",
@ -182,8 +174,8 @@
"formula":"Berechungsformel", "formula":"Berechungsformel",
"no-transactions":"Du hast zur Zeit keine Transaktionen", "no-transactions":"Du hast zur Zeit keine Transaktionen",
"publisher":"Dein geworbenes Mitglied hat einen Beitrag bezahlt", "publisher":"Dein geworbenes Mitglied hat einen Beitrag bezahlt",
"gdt-receive":"Aktion", "action":"Aktion",
"your-share":"Geworbenes Mitglied", "recruited-member":"Geworbenes Mitglied",
"contribution":"Beitrag" "contribution":"Beitrag"
} }
} }

View File

@ -18,6 +18,7 @@
"de": "Deutsch", "de": "Deutsch",
"en": "English" "en": "English"
}, },
"select_language": "Please choose a language for the app and newsletter",
"decay": { "decay": {
"decay": "Decay", "decay": "Decay",
"decay_since_last_transaction":"Decay since the last transaction", "decay_since_last_transaction":"Decay since the last transaction",
@ -136,16 +137,6 @@
"send_gradido":"Send Gradido", "send_gradido":"Send Gradido",
"add_work":"New Community Contribution" "add_work":"New Community Contribution"
}, },
"profil": {
"transactions":"transactions",
"activity": {
"new":"Register new community hours",
"list":"My Community Hours List"
},
"user-data": {
"change-success": "Your data has been saved."
}
},
"navbar" : { "navbar" : {
"my-profil":"My profile", "my-profil":"My profile",
"settings":"Settings", "settings":"Settings",
@ -183,8 +174,8 @@
"formula": "Calculation formula", "formula": "Calculation formula",
"no-transactions":"You currently have no transactions", "no-transactions":"You currently have no transactions",
"publisher":"A member you referred has paid a contribution", "publisher":"A member you referred has paid a contribution",
"gdt-receive":"GDT receive", "action":"Action",
"your-share":"Your share", "recruited-member":"Recruited Member",
"contribution":"Contribution" "contribution":"Contribution"
} }
} }

View File

@ -3,7 +3,7 @@ import DashboardPlugin from './plugins/dashboard-plugin'
import App from './App.vue' import App from './App.vue'
import i18n from './i18n.js' import i18n from './i18n.js'
import { loadAllRules } from './validation-rules' import { loadAllRules } from './validation-rules'
import ApolloClient from 'apollo-boost' import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
import CONFIG from './config' import CONFIG from './config'
@ -11,7 +11,25 @@ import { store } from './store/store'
import router from './routes/router' import router from './routes/router'
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
const authLink = new ApolloLink((operation, forward) => {
const token = store.state.token
operation.setContext({
headers: {
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
},
})
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({ const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
uri: CONFIG.GRAPHQL_URI, uri: CONFIG.GRAPHQL_URI,
}) })
@ -26,7 +44,7 @@ Vue.config.productionTip = false
loadAllRules(i18n) loadAllRules(i18n)
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.sessionId) { if (to.meta.requiresAuth && !store.state.token) {
next({ path: '/login' }) next({ path: '/login' })
} else { } else {
next() next()

View File

@ -1,6 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate' import createPersistedState from 'vuex-persistedstate'
Vue.use(Vuex) Vue.use(Vuex)
export const mutations = { export const mutations = {
@ -10,9 +11,6 @@ export const mutations = {
email: (state, email) => { email: (state, email) => {
state.email = email state.email = email
}, },
sessionId: (state, sessionId) => {
state.sessionId = sessionId
},
username: (state, username) => { username: (state, username) => {
state.username = username state.username = username
}, },
@ -25,43 +23,45 @@ export const mutations = {
description: (state, description) => { description: (state, description) => {
state.description = description state.description = description
}, },
token: (state, token) => {
state.token = token
},
} }
export const actions = { export const actions = {
login: ({ dispatch, commit }, data) => { login: ({ dispatch, commit }, data) => {
commit('sessionId', data.sessionId) commit('email', data.email)
commit('email', data.user.email) commit('language', data.language)
commit('language', data.user.language) commit('username', data.username)
commit('username', data.user.username) commit('firstName', data.firstName)
commit('firstName', data.user.firstName) commit('lastName', data.lastName)
commit('lastName', data.user.lastName) commit('description', data.description)
commit('description', data.user.description)
}, },
logout: ({ commit, state }) => { logout: ({ commit, state }) => {
commit('sessionId', null) commit('token', null)
commit('email', null) commit('email', null)
commit('username', '') commit('username', '')
commit('firstName', '') commit('firstName', '')
commit('lastName', '') commit('lastName', '')
commit('description', '') commit('description', '')
sessionStorage.clear() localStorage.clear()
}, },
} }
export const store = new Vuex.Store({ export const store = new Vuex.Store({
plugins: [ plugins: [
createPersistedState({ createPersistedState({
storage: window.sessionStorage, storage: window.localStorage,
}), }),
], ],
state: { state: {
sessionId: null,
email: '', email: '',
language: null, language: null,
firstName: '', firstName: '',
lastName: '', lastName: '',
username: '', username: '',
description: '', description: '',
token: null,
}, },
getters: {}, getters: {},
// Syncronous mutation of the state // Syncronous mutation of the state

View File

@ -1,6 +1,6 @@
import { mutations, actions } from './store' import { mutations, actions } from './store'
const { language, email, sessionId, username, firstName, lastName, description } = mutations const { language, email, token, username, firstName, lastName, description } = mutations
const { login, logout } = actions const { login, logout } = actions
describe('Vuex store', () => { describe('Vuex store', () => {
@ -21,11 +21,11 @@ describe('Vuex store', () => {
}) })
}) })
describe('sessionId', () => { describe('token', () => {
it('sets the state of sessionId', () => { it('sets the state of token', () => {
const state = { sessionId: null } const state = { token: null }
sessionId(state, '1234') token(state, '1234')
expect(state.sessionId).toEqual('1234') expect(state.token).toEqual('1234')
}) })
}) })
@ -67,55 +67,47 @@ describe('Vuex store', () => {
const commit = jest.fn() const commit = jest.fn()
const state = {} const state = {}
const commitedData = { const commitedData = {
sessionId: 1234, email: 'user@example.org',
user: { language: 'de',
email: 'someone@there.is', username: 'peter',
language: 'en',
username: 'user',
firstName: 'Peter', firstName: 'Peter',
lastName: 'Lustig', lastName: 'Lustig',
description: 'Nickelbrille', description: 'Nickelbrille',
},
} }
it('calls seven commits', () => { it('calls seven commits', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(7) expect(commit).toHaveBeenCalledTimes(6)
})
it('commits sessionId', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(1, 'sessionId', 1234)
}) })
it('commits email', () => { it('commits email', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(2, 'email', 'someone@there.is') expect(commit).toHaveBeenNthCalledWith(1, 'email', 'user@example.org')
}) })
it('commits language', () => { it('commits language', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(3, 'language', 'en') expect(commit).toHaveBeenNthCalledWith(2, 'language', 'de')
}) })
it('commits username', () => { it('commits username', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(4, 'username', 'user') expect(commit).toHaveBeenNthCalledWith(3, 'username', 'peter')
}) })
it('commits firstName', () => { it('commits firstName', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(5, 'firstName', 'Peter') expect(commit).toHaveBeenNthCalledWith(4, 'firstName', 'Peter')
}) })
it('commits lastName', () => { it('commits lastName', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(6, 'lastName', 'Lustig') expect(commit).toHaveBeenNthCalledWith(5, 'lastName', 'Lustig')
}) })
it('commits description', () => { it('commits description', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(7, 'description', 'Nickelbrille') expect(commit).toHaveBeenNthCalledWith(6, 'description', 'Nickelbrille')
}) })
}) })
@ -128,9 +120,9 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenCalledTimes(6) expect(commit).toHaveBeenCalledTimes(6)
}) })
it('commits sessionId', () => { it('commits token', () => {
logout({ commit, state }) logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(1, 'sessionId', null) expect(commit).toHaveBeenNthCalledWith(1, 'token', null)
}) })
it('commits email', () => { it('commits email', () => {
@ -159,7 +151,7 @@ describe('Vuex store', () => {
}) })
// how to get this working? // how to get this working?
it.skip('calls sessionStorage.clear()', () => { it.skip('calls localStorage.clear()', () => {
const clearStorageMock = jest.fn() const clearStorageMock = jest.fn()
global.sessionStorage = jest.fn(() => { global.sessionStorage = jest.fn(() => {
return { return {

View File

@ -16,11 +16,11 @@
App version {{ version }} App version {{ version }}
</a> </a>
<a <a
v-if="shortHash !== '00000000'" v-if="hash"
:href="'https://github.com/gradido/gradido/commit/' + hash" :href="'https://github.com/gradido/gradido/commit/' + hash"
target="_blank" target="_blank"
> >
{{ shortHash }} ({{ shortHash }})
</a> </a>
</div> </div>
</b-col> </b-col>
@ -66,8 +66,8 @@ export default {
return { return {
year: new Date().getFullYear(), year: new Date().getFullYear(),
version: CONFIG.APP_VERSION, version: CONFIG.APP_VERSION,
hash: CONFIG.COMMIT_HASH, hash: CONFIG.BUILD_COMMIT,
shortHash: CONFIG.COMMIT_HASH.substr(0, 8), shortHash: CONFIG.BUILD_COMMIT_SHORT,
} }
}, },
} }

View File

@ -41,7 +41,6 @@ describe('DashboardLayoutGdd', () => {
}, },
$store: { $store: {
state: { state: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
}, },
dispatch: storeDispatchMock, dispatch: storeDispatchMock,
@ -128,16 +127,17 @@ describe('DashboardLayoutGdd', () => {
describe('logout', () => { describe('logout', () => {
beforeEach(async () => { beforeEach(async () => {
await apolloMock.mockResolvedValue({
data: {
logout: 'success',
},
})
await wrapper.findComponent({ name: 'sidebar' }).vm.$emit('logout') await wrapper.findComponent({ name: 'sidebar' }).vm.$emit('logout')
await flushPromises() await flushPromises()
}) })
it('calls the API', async () => { it('calls the API', async () => {
expect(apolloMock).toBeCalledWith( await expect(apolloMock).toBeCalled()
expect.objectContaining({
variables: { sessionId: 1 },
}),
)
}) })
it('dispatches logout to store', () => { it('dispatches logout to store', () => {
@ -196,7 +196,6 @@ describe('DashboardLayoutGdd', () => {
expect(apolloMock).toBeCalledWith( expect(apolloMock).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1,
firstPage: 2, firstPage: 2,
items: 5, items: 5,
}, },

View File

@ -92,7 +92,6 @@ export default {
this.$apollo this.$apollo
.query({ .query({
query: logout, query: logout,
variables: { sessionId: this.$store.state.sessionId },
}) })
.then(() => { .then(() => {
this.$sidebar.displaySidebar(false) this.$sidebar.displaySidebar(false)
@ -111,7 +110,6 @@ export default {
.query({ .query({
query: transactionsQuery, query: transactionsQuery,
variables: { variables: {
sessionId: this.$store.state.sessionId,
firstPage: pagination.firstPage, firstPage: pagination.firstPage,
items: pagination.items, items: pagination.items,
}, },

View File

@ -16,12 +16,12 @@ describe('AccountOverview', () => {
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$n: jest.fn((n) => String(n)),
$store: { $store: {
state: { state: {
sessionId: 1, email: 'sender@example.org',
}, },
}, },
$n: jest.fn((n) => String(n)),
$apollo: { $apollo: {
query: sendMock, query: sendMock,
}, },
@ -93,7 +93,6 @@ describe('AccountOverview', () => {
expect(sendMock).toBeCalledWith( expect(sendMock).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
amount: 23.45, amount: 23.45,
memo: 'Make the best of it!', memo: 'Make the best of it!',

View File

@ -107,10 +107,7 @@ export default {
this.$apollo this.$apollo
.query({ .query({
query: sendCoins, query: sendCoins,
variables: { variables: this.transactionData,
sessionId: this.$store.state.sessionId,
...this.transactionData,
},
}) })
.then(() => { .then(() => {
this.error = false this.error = false

View File

@ -8,11 +8,6 @@ describe('GddSend', () => {
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$store: {
state: {
sessionId: 1234,
},
},
$i18n: { $i18n: {
locale: jest.fn(() => 'en'), locale: jest.fn(() => 'en'),
}, },

View File

@ -57,11 +57,6 @@ describe('GdtTransactionList', () => {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$n: jest.fn((n) => n), $n: jest.fn((n) => n),
$d: jest.fn((d) => d), $d: jest.fn((d) => d),
$store: {
state: {
sessionId: 1,
},
},
$toasted: { $toasted: {
error: toastErrorMock, error: toastErrorMock,
}, },
@ -89,7 +84,6 @@ describe('GdtTransactionList', () => {
expect(apolloMock).toBeCalledWith( expect(apolloMock).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1,
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
}, },

View File

@ -17,134 +17,14 @@
} in transactionsGdt" } in transactionsGdt"
:key="transactionId" :key="transactionId"
> >
<div class="list-group-item gdt-transaction-list-item" v-b-toggle="'a' + date + ''"> <transaction
<!-- Icon --> :amount="amount"
<div class="text-right" style="position: absolute"> :date="date"
<b-icon :comment="comment"
v-if="gdtEntryType" :gdtEntryType="gdtEntryType"
:icon="getIcon(gdtEntryType).icon" :factor="factor"
:class="getIcon(gdtEntryType).class" :gdt="gdt"
></b-icon> ></transaction>
</div>
<!-- Collaps Button -->
<div class="text-right" style="width: 96%; position: absolute">
<b-button class="btn-sm">
<b>i</b>
</b-button>
</div>
<!-- Betrag -->
<!-- 7 nur GDT erhalten -->
<b-row v-if="gdtEntryType === 7">
<div class="col-6 text-right">
<div>{{ $t('gdt.gdt-receive') }}</div>
<div>{{ $t('gdt.credit') }}</div>
</div>
<div class="col-6">
<div>{{ comment }}</div>
<div>{{ $n(gdt, 'decimal') }} GDT</div>
</div>
</b-row>
<!--4 publisher -->
<b-row v-else-if="gdtEntryType === 4">
<div class="col-6 text-right">
<div>{{ $t('gdt.your-share') }}</div>
<div>{{ $t('gdt.credit') }}</div>
</div>
<div class="col-6">
<div>5%</div>
<div>{{ $n(amount, 'decimal') }} GDT</div>
</div>
</b-row>
<!-- 1, 2, 3, 5, 6 spenden in euro -->
<b-row v-else>
<div class="col-6 text-right">
<div>{{ $t('gdt.contribution') }}</div>
<div>{{ $t('gdt.credit') }}</div>
</div>
<div class="col-6">
<div>{{ $n(amount, 'decimal') }} </div>
<div>{{ $n(gdt, 'decimal') }} GDT</div>
</div>
</b-row>
<!-- Betrag ENDE-->
<!-- Nachricht-->
<b-row v-if="comment && gdtEntryType !== 7">
<div class="col-6 text-right">
{{ $t('form.memo') }}
</div>
<div class="col-6">
{{ comment }}
</div>
</b-row>
<!-- Datum-->
<b-row v-if="date" class="gdt-list-row text-header">
<div class="col-6 text-right">
{{ $t('form.date') }}
</div>
<div class="col-6">
{{ $d($moment(date), 'long') }} {{ $i18n.locale === 'de' ? 'Uhr' : '' }}
</div>
</b-row>
</div>
<!-- Collaps START -->
<b-collapse v-if="gdtEntryType" :id="'a' + date + ''" class="pb-4">
<div style="border: 0px; background-color: #f1f1f1" class="p-2 pb-4 mb-4">
<!-- Überschrift -->
<b-row class="gdt-list-clooaps-header-text text-center pb-3">
<div class="col h4" v-if="gdtEntryType === 7">
{{ $t('gdt.conversion-gdt-euro') }}
</div>
<div class="col h4" v-else-if="gdtEntryType === 4">
{{ $t('gdt.publisher') }}
</div>
<div class="col h4" v-else>{{ $t('gdt.calculation') }}</div>
</b-row>
<!-- 7 nur GDT erhalten -->
<b-row class="gdt-list-clooaps-box-7" v-if="gdtEntryType == 7">
<div class="col-6 text-right clooaps-col-left">
<div>{{ $t('gdt.raise') }}</div>
<div>{{ $t('gdt.conversion') }}</div>
</div>
<div class="col-6 clooaps-col-right">
<div>{{ factor * 100 }} %</div>
<div>
{{ $n(amount, 'decimal') }} GDT * {{ factor * 100 }} % =
{{ $n(gdt, 'decimal') }} GDT
</div>
</div>
</b-row>
<!-- 4 publisher -->
<b-row class="gdt-list-clooaps-box-4" v-else-if="gdtEntryType === 4">
<div class="col-6 text-right clooaps-col-left"></div>
<div class="col-6 clooaps-col-right"></div>
</b-row>
<!-- 1, 2, 3, 5, 6 spenden in euro -->
<b-row class="gdt-list-clooaps-box--all" v-else>
<div class="col-6 text-right clooaps-col-left">
<div>{{ $t('gdt.factor') }}</div>
<div>{{ $t('gdt.formula') }}</div>
</div>
<div class="col-6 clooaps-col-right">
<div>{{ factor }} GDT pro </div>
<div>
{{ $n(amount, 'decimal') }} * {{ factor }} GDT / =
{{ $n(gdt, 'decimal') }} GDT
</div>
</div>
</b-row>
</div>
</b-collapse>
<!-- Collaps ENDE -->
</div> </div>
</div> </div>
<pagination-buttons <pagination-buttons
@ -162,17 +42,13 @@
<script> <script>
import { listGDTEntriesQuery } from '../../../graphql/queries' import { listGDTEntriesQuery } from '../../../graphql/queries'
import PaginationButtons from '../../../components/PaginationButtons' import PaginationButtons from '../../../components/PaginationButtons'
import Transaction from '../../../components/Transaction.vue'
const iconsByType = {
1: { icon: 'heart', classes: 'gradido-global-color-accent' },
4: { icon: 'person-check', classes: 'gradido-global-color-accent' },
7: { icon: 'gift', classes: 'gradido-global-color-accent' },
}
export default { export default {
name: 'gdt-transaction-list', name: 'gdt-transaction-list',
components: { components: {
PaginationButtons, PaginationButtons,
Transaction,
}, },
data() { data() {
return { return {
@ -199,7 +75,6 @@ export default {
.query({ .query({
query: listGDTEntriesQuery, query: listGDTEntriesQuery,
variables: { variables: {
sessionId: this.$store.state.sessionId,
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.pageSize, pageSize: this.pageSize,
}, },
@ -215,18 +90,6 @@ export default {
this.$toasted.error(error.message) this.$toasted.error(error.message)
}) })
}, },
getIcon(givenType) {
const type = iconsByType[givenType]
if (type)
return {
icon: type.icon,
class: type.classes + ' m-mb-1 font2em',
}
this.throwError('no icon to given type: ' + givenType)
},
throwError(msg) {
throw new Error(msg)
},
showNext() { showNext() {
this.currentPage++ this.currentPage++
this.updateGdt() this.updateGdt()

View File

@ -6,12 +6,7 @@ const localVue = global.localVue
const loginQueryMock = jest.fn().mockResolvedValue({ const loginQueryMock = jest.fn().mockResolvedValue({
data: { data: {
login: { login: 'token',
sessionId: 1,
user: {
name: 'Peter Lustig',
},
},
}, },
}) })
@ -159,10 +154,7 @@ describe('Login', () => {
describe('login success', () => { describe('login success', () => {
it('dispatches server response to store', () => { it('dispatches server response to store', () => {
expect(mockStoreDispach).toBeCalledWith('login', { expect(mockStoreDispach).toBeCalledWith('login', 'token')
sessionId: 1,
user: { name: 'Peter Lustig' },
})
}) })
it('redirects to overview page', () => { it('redirects to overview page', () => {

View File

@ -80,6 +80,13 @@ describe('Register', () => {
it('has password repeat input fields', () => { it('has password repeat input fields', () => {
expect(wrapper.find('input[name="form.passwordRepeat"]').exists()).toBeTruthy() expect(wrapper.find('input[name="form.passwordRepeat"]').exists()).toBeTruthy()
}) })
it('has Language selected field', () => {
expect(wrapper.find('#selectedLanguage').exists()).toBeTruthy()
})
it('selected Language value de', async () => {
wrapper.find('#selectedLanguage').findAll('option').at(1).setSelected()
expect(wrapper.find('#selectedLanguage').element.value).toBe('de')
})
it('has 1 checkbox input fields', () => { it('has 1 checkbox input fields', () => {
expect(wrapper.find('#registerCheckbox').exists()).toBeTruthy() expect(wrapper.find('#registerCheckbox').exists()).toBeTruthy()
@ -121,9 +128,16 @@ describe('Register', () => {
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net') wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper.find('input[name="form.password"]').setValue('Aa123456') wrapper.find('input[name="form.password"]').setValue('Aa123456')
wrapper.find('input[name="form.passwordRepeat"]').setValue('Aa123456') wrapper.find('input[name="form.passwordRepeat"]').setValue('Aa123456')
wrapper.find('#selectedLanguage').findAll('option').at(1).setSelected()
wrapper.find('input[name="site.signup.agree"]').setChecked(true) wrapper.find('input[name="site.signup.agree"]').setChecked(true)
}) })
it('reset selected value language', async () => {
await wrapper.find('button.ml-2').trigger('click')
await flushPromises()
expect(wrapper.find('#selectedLanguage').element.value).toBe('')
})
it('resets the firstName field after clicking the reset button', async () => { it('resets the firstName field after clicking the reset button', async () => {
await wrapper.find('button.ml-2').trigger('click') await wrapper.find('button.ml-2').trigger('click')
await flushPromises() await flushPromises()
@ -168,6 +182,7 @@ describe('Register', () => {
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net') wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper.find('input[name="form.password"]').setValue('Aa123456') wrapper.find('input[name="form.password"]').setValue('Aa123456')
wrapper.find('input[name="form.passwordRepeat"]').setValue('Aa123456') wrapper.find('input[name="form.passwordRepeat"]').setValue('Aa123456')
wrapper.find('#selectedLanguage').findAll('option').at(1).setSelected()
}) })
describe('server sends back error', () => { describe('server sends back error', () => {
@ -214,6 +229,7 @@ describe('Register', () => {
firstName: 'Max', firstName: 'Max',
lastName: 'Mustermann', lastName: 'Mustermann',
password: 'Aa123456', password: 'Aa123456',
language: 'de',
}, },
}), }),
) )

View File

@ -84,6 +84,18 @@
:register="register" :register="register"
></input-password-confirmation> ></input-password-confirmation>
<b-row>
<b-col cols="12">
{{ $t('language') }}
<b-form-select
id="selectedLanguage"
v-model="selected"
:options="options"
class="mb-3"
></b-form-select>
</b-col>
</b-row>
<b-row class="my-4"> <b-row class="my-4">
<b-col cols="12"> <b-col cols="12">
<b-form-checkbox <b-form-checkbox
@ -109,7 +121,10 @@
</span> </span>
</b-alert> </b-alert>
<div class="text-center" v-if="namesFilled && emailFilled && form.agree"> <div
class="text-center"
v-if="namesFilled && emailFilled && form.agree && languageFilled"
>
<div class="text-center"> <div class="text-center">
<b-button class="ml-2" @click="resetForm()">{{ $t('form.reset') }}</b-button> <b-button class="ml-2" @click="resetForm()">{{ $t('form.reset') }}</b-button>
<b-button type="submit" variant="primary">{{ $t('signup') }}</b-button> <b-button type="submit" variant="primary">{{ $t('signup') }}</b-button>
@ -147,6 +162,12 @@ export default {
passwordRepeat: '', passwordRepeat: '',
}, },
}, },
selected: null,
options: [
{ value: null, text: this.$t('select_language') },
{ value: 'de', text: this.$t('languages.de') },
{ value: 'en', text: this.$t('languages.en') },
],
submitted: false, submitted: false,
showError: false, showError: false,
messageError: '', messageError: '',
@ -168,8 +189,7 @@ export default {
}, },
agree: false, agree: false,
} }
this.form.password.password = '' this.selected = null
this.form.password.passwordRepeat = ''
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.observer.reset() this.$refs.observer.reset()
}) })
@ -183,6 +203,7 @@ export default {
firstName: this.form.firstname, firstName: this.form.firstname,
lastName: this.form.lastname, lastName: this.form.lastname,
password: this.form.password.password, password: this.form.password.password,
language: this.selected,
}, },
}) })
.then(() => { .then(() => {
@ -191,6 +212,7 @@ export default {
this.form.lastname = '' this.form.lastname = ''
this.form.password.password = '' this.form.password.password = ''
this.form.password.passwordRepeat = '' this.form.password.passwordRepeat = ''
this.selected = null
this.$router.push('/thx/register') this.$router.push('/thx/register')
}) })
.catch((error) => { .catch((error) => {
@ -206,6 +228,7 @@ export default {
this.form.lastname = '' this.form.lastname = ''
this.form.password.password = '' this.form.password.password = ''
this.form.password.passwordRepeat = '' this.form.password.passwordRepeat = ''
this.selected = null
}, },
}, },
computed: { computed: {
@ -220,6 +243,9 @@ export default {
emailFilled() { emailFilled() {
return this.form.email !== '' return this.form.email !== ''
}, },
languageFilled() {
return this.selected !== null
},
}, },
} }
</script> </script>

View File

@ -17,7 +17,6 @@ describe('UserCard_FormUserData', () => {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$store: { $store: {
state: { state: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
firstName: 'Peter', firstName: 'Peter',
lastName: 'Lustig', lastName: 'Lustig',
@ -118,7 +117,6 @@ describe('UserCard_FormUserData', () => {
expect(mockAPIcall).toBeCalledWith( expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
firstName: 'Petra', firstName: 'Petra',
lastName: 'Lustiger', lastName: 'Lustiger',
@ -167,7 +165,6 @@ describe('UserCard_FormUserData', () => {
expect(mockAPIcall).toBeCalledWith( expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
firstName: 'Petra', firstName: 'Petra',
lastName: 'Lustiger', lastName: 'Lustiger',

View File

@ -85,7 +85,6 @@ export default {
data() { data() {
return { return {
showUserData: true, showUserData: true,
sessionId: this.$store.state.sessionId,
form: { form: {
firstName: this.$store.state.firstName, firstName: this.$store.state.firstName,
lastName: this.$store.state.lastName, lastName: this.$store.state.lastName,
@ -118,7 +117,6 @@ export default {
.query({ .query({
query: updateUserInfos, query: updateUserInfos,
variables: { variables: {
sessionId: this.$store.state.sessionId,
email: this.$store.state.email, email: this.$store.state.email,
firstName: this.form.firstName, firstName: this.form.firstName,
lastName: this.form.lastName, lastName: this.form.lastName,

View File

@ -14,7 +14,6 @@ describe('UserCard_FormUserMail', () => {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$store: { $store: {
state: { state: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
firstName: 'Peter', firstName: 'Peter',
lastName: 'Lustig', lastName: 'Lustig',
@ -76,7 +75,6 @@ describe('UserCard_FormUserMail', () => {
expect(mockAPIcall).toHaveBeenCalledWith( expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
newEmail: 'test@example.org', newEmail: 'test@example.org',
}, },
@ -106,7 +104,6 @@ describe('UserCard_FormUserMail', () => {
expect(mockAPIcall).toHaveBeenCalledWith( expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
newEmail: 'test@example.org', newEmail: 'test@example.org',
}, },

View File

@ -48,7 +48,6 @@ export default {
.query({ .query({
query: updateUserInfos, query: updateUserInfos,
variables: { variables: {
sessionId: this.$store.state.sessionId,
email: this.$store.state.email, email: this.$store.state.email,
newEmail: this.newEmail, newEmail: this.newEmail,
}, },

View File

@ -17,7 +17,6 @@ describe('UserCard_FormUserPasswort', () => {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$store: { $store: {
state: { state: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
}, },
}, },
@ -175,7 +174,6 @@ describe('UserCard_FormUserPasswort', () => {
expect(changePasswordProfileMock).toHaveBeenCalledWith( expect(changePasswordProfileMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
password: '1234', password: '1234',
passwordNew: 'Aa123456', passwordNew: 'Aa123456',

View File

@ -81,7 +81,6 @@ export default {
.query({ .query({
query: updateUserInfos, query: updateUserInfos,
variables: { variables: {
sessionId: this.$store.state.sessionId,
email: this.$store.state.email, email: this.$store.state.email,
password: this.form.password, password: this.form.password,
passwordNew: this.form.newPassword.password, passwordNew: this.form.newPassword.password,

View File

@ -25,7 +25,6 @@ describe('UserCard_FormUsername', () => {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$store: { $store: {
state: { state: {
sessionId: 1,
email: 'user@example.org', email: 'user@example.org',
username: '', username: '',
}, },
@ -111,7 +110,6 @@ describe('UserCard_FormUsername', () => {
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
email: 'user@example.org', email: 'user@example.org',
sessionId: 1,
username: 'username', username: 'username',
}, },
}), }),
@ -151,7 +149,6 @@ describe('UserCard_FormUsername', () => {
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
email: 'user@example.org', email: 'user@example.org',
sessionId: 1,
username: 'username', username: 'username',
}, },
}), }),

View File

@ -90,7 +90,6 @@ export default {
.query({ .query({
query: updateUserInfos, query: updateUserInfos,
variables: { variables: {
sessionId: this.$store.state.sessionId,
email: this.$store.state.email, email: this.$store.state.email,
username: this.form.username, username: this.form.username,
}, },

View File

@ -13,11 +13,6 @@ describe('UserProfileTransactionList', () => {
$i18n: { $i18n: {
locale: jest.fn(() => 'en'), locale: jest.fn(() => 'en'),
}, },
$store: {
state: {
sessionId: 1,
},
},
} }
const stubs = { const stubs = {

View File

@ -1,5 +1,6 @@
const path = require('path') const path = require('path')
const dotenv = require('dotenv-webpack') const webpack = require('webpack')
const Dotenv = require('dotenv-webpack')
process.env.VUE_APP_BUILD_COMMIT = process.env.BUILD_COMMIT process.env.VUE_APP_BUILD_COMMIT = process.env.BUILD_COMMIT
@ -25,8 +26,17 @@ module.exports = {
assets: path.join(__dirname, 'src/assets'), assets: path.join(__dirname, 'src/assets'),
}, },
}, },
// eslint-disable-next-line new-cap plugins: [
plugins: [new dotenv()], new Dotenv(),
new webpack.DefinePlugin({
// Those are Environment Variables transmitted via Docker
// 'process.env.DOCKER_WORKDIR': JSON.stringify(process.env.DOCKER_WORKDIR),
// 'process.env.BUILD_DATE': JSON.stringify(process.env.BUILD_DATE),
// 'process.env.BUILD_VERSION': JSON.stringify(process.env.BUILD_VERSION),
'process.env.BUILD_COMMIT': JSON.stringify(process.env.BUILD_COMMIT),
// 'process.env.PORT': JSON.stringify(process.env.PORT),
}),
],
}, },
css: { css: {
// Enable CSS source maps. // Enable CSS source maps.

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 #!/bin/sh
cd ../scripts
chmod +x compile_pot.sh chmod +x compile_pot.sh
./compile_pot.sh
cd ../build cd ../build
cmake -DCMAKE_BUILD_TYPE=Debug .. cmake -DCMAKE_BUILD_TYPE=Debug ..
./compile_pot.sh
make -j$(nproc) Gradido_LoginServer make -j$(nproc) Gradido_LoginServer
chmod +x ./bin/Gradido_LoginServer chmod +x ./bin/Gradido_LoginServer

View File

@ -9,7 +9,7 @@ fi
mkdir build mkdir build
cd build cd build
cmake -DWITH_SSL=OFF .. cmake -DWITH_SSL=OFF ..
cd ../../ cd ../../../
if [ ! -d "./build" ] ; then if [ ! -d "./build" ] ; then
mkdir build mkdir 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(); return stateSuccess();
} }
auto receiver_user_id = receiver_user->getModel()->getID(); 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) if (emailVerificationCodeType == model::table::EMAIL_OPT_IN_RESET_PASSWORD)
{ {
linkInEmail = receiver_user->getGroupBaseUrl() + ServerConfig::g_frontend_resetPasswordPath;
session = sm->getNewSession(); session = sm->getNewSession();
if (emailType == model::EMAIL_USER_RESET_PASSWORD) { 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) { if (1 == r) {
return stateWarning("email already sended"); 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) { 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); 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); auto email = new model::Email(email_verification_code_object, receiver_user, emailCustomText, emailCustomSubject);
em->addEmail(email); em->addEmail(email);
} }
@ -131,12 +132,13 @@ Poco::JSON::Object* JsonSendEmail::handle(Poco::Dynamic::Var params)
} }
else else
{ {
linkInEmail = receiver_user->getGroupBaseUrl() + ServerConfig::g_frontend_checkEmailPath;
if (session->getNewUser()->getModel()->getRole() != model::table::ROLE_ADMIN) { if (session->getNewUser()->getModel()->getRole() != model::table::ROLE_ADMIN) {
return stateError("admin needed"); return stateError("admin needed");
} }
auto email_verification_code_object = controller::EmailVerificationCode::loadOrCreate(receiver_user_id, emailVerificationCodeType); 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; model::Email* email = nullptr;
if (emailType == model::EMAIL_CUSTOM_TEXT) { if (emailType == model::EMAIL_CUSTOM_TEXT) {
email = new model::Email(email_verification_code_object, receiver_user, emailCustomText, emailCustomSubject); 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_serverPath;
std::string g_php_serverHost; std::string g_php_serverHost;
std::string g_frontend_checkEmailPath; std::string g_frontend_checkEmailPath;
std::string g_frontend_resetPasswordPath;
int g_phpServerPort; int g_phpServerPort;
Poco::Mutex g_TimeMutex; Poco::Mutex g_TimeMutex;
int g_FakeLoginSleepTime = 820; int g_FakeLoginSleepTime = 820;
@ -238,8 +239,9 @@ namespace ServerConfig {
if ("" != app_secret_string) { if ("" != app_secret_string) {
g_CryptoAppSecret = DataTypeConverter::hexToBin(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_checkEmailPath = cfg.getString("frontend.checkEmailPath", defaultCheckEmailPath);
g_frontend_resetPasswordPath = cfg.getString("frontend.resetPasswordPath", defaultCheckEmailPath);
//g_CryptoAppSecret //g_CryptoAppSecret
// unsecure flags // unsecure flags

View File

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