├── start_backend.sh
├── project
├── build.properties
└── plugins.sbt
├── start_frontend.sh
├── frontend
├── src
│ ├── redux
│ │ ├── store
│ │ │ ├── configureStore.js
│ │ │ ├── configureStore.prod.js
│ │ │ └── configureStore.dev..js
│ │ ├── reducers
│ │ │ ├── index.js
│ │ │ ├── walletReducer.js
│ │ │ └── loginSignupReducer.js
│ │ └── actions
│ │ │ ├── ActionTypes.js
│ │ │ ├── sendMoneyActions.js
│ │ │ ├── loadWalletActions.js
│ │ │ ├── signupActions.js
│ │ │ ├── requestMoneyActions.js
│ │ │ └── loginActions.js
│ ├── index.html
│ ├── public
│ │ └── style
│ │ │ └── styles.css
│ ├── index.js
│ └── containers
│ │ ├── App.js
│ │ ├── Login.js
│ │ ├── SignUp.js
│ │ └── Dashboard.js
├── .babelrc
├── tools
│ └── server.js
├── webpack.config.js
├── .eslintrc
└── package.json
├── backend
└── src
│ └── main
│ ├── scala
│ └── com
│ │ └── cpuheater
│ │ └── starter
│ │ ├── util
│ │ ├── FutureSupport.scala
│ │ ├── db
│ │ │ └── DbSupport.scala
│ │ └── service
│ │ │ └── model
│ │ │ └── package.scala
│ │ ├── json
│ │ └── StarterProtocol.scala
│ │ ├── StarterConfig.scala
│ │ ├── model
│ │ └── package.scala
│ │ ├── route
│ │ ├── RouteSupport.scala
│ │ ├── WalletRoute.scala
│ │ └── UserRoute.scala
│ │ ├── persistence
│ │ ├── WalletDao.scala
│ │ └── UserDao.scala
│ │ ├── service
│ │ ├── UserService.scala
│ │ └── WalletService.scala
│ │ └── StarterApp.scala
│ └── resources
│ ├── logback.xml
│ ├── application.conf
│ └── init.sql
├── .gitignore
├── README.md
└── LICENSE
/start_backend.sh:
--------------------------------------------------------------------------------
1 | sbt "project backend" "run"
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.15
2 |
--------------------------------------------------------------------------------
/start_frontend.sh:
--------------------------------------------------------------------------------
1 | cd ./frontend
2 |
3 | npm install
4 |
5 | npm start
6 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3")
2 |
--------------------------------------------------------------------------------
/frontend/src/redux/store/configureStore.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./configureStore.prod');
--------------------------------------------------------------------------------
/frontend/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { routerReducer } from 'react-router-redux';
3 | import session from './loginSignupReducer';
4 | import wallet from './walletReducer';
5 |
6 | export default combineReducers({
7 | session,
8 | wallet,
9 | routing: routerReducer
10 | });
11 |
--------------------------------------------------------------------------------
/frontend/src/redux/actions/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const LOG_IN_SUCCESS = 'LOG_IN_SUCCESS';
2 | export const LOG_OUT = 'LOG_OUT';
3 | export const SIGN_UP_SUCCESS = 'SIGN_UP_SUCCESS';
4 | export const LOAD_WALLET_SUCCESS = "LOAD_WALLET_SUCCESS";
5 | export const SEND_MONEY_SUCCESS = "SEND_MONEY_SUCCESS";
6 | export const REQUEST_MONEY_SUCCESS = "REQUEST_MONEY_SUCCESS";
7 |
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/util/FutureSupport.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.util
2 |
3 | import scala.concurrent.{Await, Future}
4 | import scala.concurrent.duration._
5 |
6 |
7 | trait FutureSupport {
8 |
9 | implicit class FutureAwait[T](f: Future[T]){
10 | def await() : T = Await.result(f, 5 seconds)
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ATM
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/backend/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | target
3 | /.idea
4 | /.idea_modules
5 | /.classpath
6 | /.project
7 | /.settings
8 | /RUNNING_PID
9 | /target
10 | /*/target
11 | /frontend/dist
12 | /*/node_modules
13 | /project/project/target/*
14 | *.iml
15 | .idea
16 | .project
17 | /*/.project
18 | .classpath
19 | /*/.classpath
20 | .settings
21 | /*/.settings
22 | *-db.h2.db
23 | *-db.lock.db
24 | Thumbs.db
25 | *.sublime-project
26 | *.sublime-workspace
27 |
28 |
--------------------------------------------------------------------------------
/frontend/src/redux/reducers/walletReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/ActionTypes';
2 |
3 |
4 | export default function walletReducer(state = null, action){
5 | switch(action.type){
6 | case types.LOAD_WALLET_SUCCESS:
7 | return action.wallet;
8 | case types.SEND_MONEY_SUCCESS:
9 | return action.wallet;
10 | case types.REQUEST_MONEY_SUCCESS:
11 | return action.wallet;
12 | default:
13 | return state;
14 | }
15 | }
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/util/db/DbSupport.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.util.db
2 |
3 | import com.cpuheater.starter.StarterConfig
4 | import com.cpuheater.starter.StarterConfig
5 | import com.cpuheater.starter.StarterConfig.db
6 | import com.typesafe.scalalogging.LazyLogging
7 | import scalikejdbc.{AutoSession, WrappedResultSet}
8 | import scalikejdbc._
9 |
10 | trait DbSupport extends LazyLogging {
11 |
12 | Class.forName("org.h2.Driver")
13 | ConnectionPool.singleton(db.url, StarterConfig.db.user, StarterConfig.db.password)
14 |
15 | implicit val session = AutoSession
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/util/service/model/package.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.util.service
2 |
3 | import scalaz.\/
4 |
5 |
6 | package object model {
7 |
8 | sealed trait ServiceFailure
9 |
10 | sealed trait ServiceSuccess[T]
11 |
12 | case class Ok[T](data: T) extends ServiceSuccess[T]
13 |
14 | case object ServiceClientFailure extends ServiceFailure
15 |
16 | case object AuthenticationFailure extends ServiceFailure
17 |
18 | case object NotFound extends ServiceFailure
19 |
20 | type ServiceResult[T] = ServiceFailure \/ ServiceSuccess[T]
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/json/StarterProtocol.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.json
2 |
3 | import com.cpuheater.starter.model._
4 | import spray.json.{DeserializationException, JsString, JsValue, JsonFormat, _}
5 |
6 |
7 | object StarterProtocol extends DefaultJsonProtocol{
8 |
9 | implicit val sendMoneyJson = jsonFormat1(SendMoney)
10 |
11 | implicit val walletJson = jsonFormat1(WalletRest)
12 |
13 | implicit val userJson = jsonFormat2(UserRest)
14 |
15 | implicit val newUserJson = jsonFormat2(NewUserRest)
16 |
17 | implicit val requestMoneyJson = jsonFormat1(RequestRest)
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/redux/actions/sendMoneyActions.js:
--------------------------------------------------------------------------------
1 | import * as types from './ActionTypes';
2 | import axios from 'axios';
3 |
4 | export function sendMoney({id, amount}) {
5 | return function(dispatch){
6 | return axios.post(`backend/wallets/${id}/sendmoney`, {amount})
7 | .then(function (response) {
8 | dispatch(sendMoneySuccess(response.data));
9 | })
10 | .catch(function (error) {
11 | // write error handling
12 | });
13 | };
14 | }
15 |
16 |
17 | export function sendMoneySuccess(wallet) {
18 | return {
19 | type: types.SEND_MONEY_SUCCESS,
20 | wallet
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/redux/actions/loadWalletActions.js:
--------------------------------------------------------------------------------
1 | import * as types from './ActionTypes';
2 | import axios from 'axios';
3 |
4 | export function loadWallet(userId) {
5 | return function(dispatch){
6 | return axios.get('backend/wallets/'+userId)
7 | .then(function (response) {
8 | dispatch(loadWalletSuccess(response.data));
9 | })
10 | .catch(function (error) {
11 | debugger
12 | // write error handling
13 | });
14 | };
15 | }
16 |
17 |
18 | export function loadWalletSuccess(wallet) {
19 | return {
20 | type: types.LOAD_WALLET_SUCCESS,
21 | wallet: wallet
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/redux/actions/signupActions.js:
--------------------------------------------------------------------------------
1 | import * as types from './ActionTypes';
2 | import axios from 'axios';
3 | import {browserHistory} from 'react-router';
4 |
5 | function signupSuccess(user) {
6 | return { type: types.SIGN_UP_SUCCESS, user};
7 | }
8 |
9 | export function signup(credentials) {
10 |
11 | return function(dispatch){
12 | return axios.post('backend/signup', credentials)
13 | .then(function (response) {
14 | dispatch(signupSuccess(response.data));
15 | browserHistory.push('/');
16 | })
17 | .catch(function (error) {
18 | // write error handling
19 | });
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/redux/store/configureStore.prod.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import { browserHistory } from 'react-router';
3 | import { routerMiddleware } from 'react-router-redux';
4 | import rootReducer from '../reducers/index';
5 | import thunk from 'redux-thunk';
6 |
7 | export default function configureStore() {
8 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
9 | const store = createStore(
10 | rootReducer,
11 | composeEnhancers(
12 | applyMiddleware(
13 | routerMiddleware(browserHistory), thunk
14 | )
15 | )
16 | );
17 | return store;
18 | }
19 |
--------------------------------------------------------------------------------
/backend/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | starter {
2 |
3 | http {
4 | port: 9000
5 | interface: "localhost"
6 | }
7 |
8 | db {
9 | url = "jdbc:h2:~/starter;INIT=runscript from 'classpath:init.sql'"
10 | user = "sa"
11 | password = ""
12 | }
13 | }
14 |
15 |
16 | spray.can.server {
17 | request-timeout = 120 s
18 | idle-timeout = 180 s
19 | }
20 |
21 | spray.can.client {
22 | idle-timeout = 180 s
23 | request-timeout = 120 s
24 | parsing {
25 | max-content-length = 64m
26 | }
27 | }
28 |
29 |
30 | akka.http {
31 | loglevel = "DEBUG"
32 | loggers = ["akka.event.slf4j.Slf4jLogger"]
33 |
34 | client {
35 | max-connections: 1000
36 | }
37 | }
38 |
39 |
40 |
--------------------------------------------------------------------------------
/frontend/src/redux/actions/requestMoneyActions.js:
--------------------------------------------------------------------------------
1 | import * as types from './ActionTypes';
2 | import axios from 'axios';
3 |
4 | export function requestMoney({id, amount}) {
5 | return function(dispatch){
6 | return axios.post(`backend/wallets/${id}/requestmoney`, {amount})
7 | .then(function (response) {
8 | dispatch(requestMoneySuccess(response.data));
9 | })
10 | .catch(function (error) {
11 | // write error handling
12 | });
13 | };
14 | }
15 |
16 |
17 | export function requestMoneySuccess(wallet) {
18 | return {
19 | type: types.REQUEST_MONEY_SUCCESS,
20 | wallet: wallet
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/StarterConfig.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter
2 |
3 | import com.typesafe.config.ConfigFactory
4 |
5 | object StarterConfig {
6 |
7 | private val config = ConfigFactory.load()
8 | private val starterConfig = config.getConfig("starter")
9 |
10 | object http {
11 | private val httpConfig = starterConfig.getConfig("http")
12 | val port = httpConfig.getInt("port")
13 | val interface = httpConfig.getString("interface")
14 | }
15 |
16 | object db {
17 | private val dbConfig = starterConfig.getConfig("db")
18 | val user = dbConfig.getString("user")
19 | val password = dbConfig.getString("password")
20 | val url = dbConfig.getString("url")
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/redux/store/configureStore.dev..js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import { browserHistory } from 'react-router';
3 | import { routerMiddleware } from 'react-router-redux';
4 | import rootReducer from '../reducers/index';
5 | import reduxImmutableStateInvariant from 'redux-immutable-state-invariant';
6 |
7 |
8 | export default function configureStore() {
9 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
10 | const store = createStore(
11 | rootReducer,
12 | composeEnhancers(
13 | applyMiddleware(
14 | routerMiddleware(browserHistory),
15 | reduxImmutableStateInvariant()
16 | )
17 | )
18 | );
19 | return store;
20 | }
21 |
--------------------------------------------------------------------------------
/backend/src/main/resources/init.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS USER(ID BIGINT auto_increment primary key, EMAIL VARCHAR(255), PASSWORD VARCHAR(255));
2 |
3 | CREATE TABLE IF NOT EXISTS WALLET(ID BIGINT primary key, BALANCE DECIMAL);
4 |
5 |
6 | INSERT INTO USER(EMAIL, PASSWORD) SELECT 'adam@cpuheater.com', 'pass' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM USER WHERE id = 1);
7 | INSERT INTO USER(EMAIL, PASSWORD) SELECT 'ewa@cpuheater.com', 'pass' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM USER WHERE id = 2);
8 |
9 |
10 | INSERT INTO WALLET(ID, BALANCE) SELECT 1, 500 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM WALLET WHERE id = 1);
11 | INSERT INTO WALLET(ID, BALANCE) SELECT 2, 2000 FROM DUAL WHERE NOT EXISTS (SELECT 2 FROM WALLET WHERE id = 2);
12 |
13 |
--------------------------------------------------------------------------------
/frontend/src/redux/reducers/loginSignupReducer.js:
--------------------------------------------------------------------------------
1 | import {browserHistory} from 'react-router';
2 | import * as types from '../actions/ActionTypes';
3 |
4 | export default function loginReducer(state = false, action) {
5 | switch(action.type) {
6 | case types.LOG_IN_SUCCESS:
7 | sessionStorage.setItem('user', JSON.stringify(action.user));
8 | return !!sessionStorage.user;
9 | case types.SIGN_UP_SUCCESS:
10 | sessionStorage.setItem('user', JSON.stringify(action.user));
11 | return !!sessionStorage.user;
12 | case types.LOG_OUT:
13 | browserHistory.push('/login');
14 | return !!sessionStorage.user;
15 | default:
16 | return state;
17 | }
18 | }
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/model/package.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter
2 |
3 | package object model {
4 |
5 | trait DbModel
6 |
7 | case class User(id: Long, email: String, password: String) extends DbModel
8 |
9 | case class NewUser(email: String, password: String) extends DbModel
10 |
11 | case class Wallet(id: Long, balance: BigDecimal) extends DbModel
12 |
13 |
14 | sealed trait Rest
15 |
16 | case class UserRest(id: Long, email: String) extends Rest
17 |
18 | case class NewUserRest(email: String,
19 | password: String) extends Rest
20 |
21 | case class RequestRest(amount: BigDecimal) extends Rest
22 |
23 | case class SendMoney(amount: BigDecimal) extends Rest
24 |
25 | case class WalletRest(balance: BigDecimal) extends Rest
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/redux/actions/loginActions.js:
--------------------------------------------------------------------------------
1 | import * as types from './ActionTypes';
2 | import axios from 'axios';
3 | import {browserHistory} from 'react-router';
4 |
5 | function loginSuccess(user) {
6 | return { type: types.LOG_IN_SUCCESS, user};
7 | }
8 |
9 |
10 | export function login(credentials) {
11 |
12 | return function(dispatch){
13 | return axios.post('backend/login', credentials)
14 | .then(function (response) {
15 | dispatch(loginSuccess(response.data));
16 | browserHistory.push('/');
17 | })
18 | .catch(function (error) {
19 | // write error handling
20 | });
21 | };
22 | }
23 |
24 |
25 | export function logout() {
26 | sessionStorage.removeItem('user');
27 | return {type: types.LOG_OUT}
28 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Akka-Http and React starter app #
2 | A starter application written in Scala and ES6.
3 | It uses akka-http as a backend and react as frontend.
4 |
5 |
6 | Backend
7 |
8 | - Akka-Http
9 | - h2 embedded database to store data
10 | - scalikejdbc to access database
11 | - scalaz OptionT, Either
12 |
13 |
14 | Frontend
15 |
16 | - react
17 | - redux state container
18 | - react router declarative routing for react
19 | - babel for ES6 and ES7 magic
20 | - webpack for bundling
21 | - http-proxy-middleware to proxy
22 | - redux thunk - used in async actions
23 | - axios promise based HTTP client
24 | - webpack-hot-middleware for hot reloading
25 |
26 | ## Run
27 |
28 | Start frontend server:
29 |
30 | ```
31 | $ ./start_frontend.sh
32 | ```
33 |
34 | Start backend server:
35 |
36 | ```
37 | $ ./start_backend.sh
38 | ```
39 |
--------------------------------------------------------------------------------
/frontend/tools/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import webpack from 'webpack';
3 | import path from 'path';
4 | import config from '../webpack.config';
5 | import open from 'open';
6 |
7 | var proxy = require('http-proxy-middleware');
8 |
9 |
10 | var options = {
11 | target: 'http://localhost:9000',
12 | changeOrigin: true,
13 | logLevel: 'debug',
14 | pathRewrite: {
15 | '^/backend/' : '/'
16 | }
17 | };
18 |
19 | var backendProxy = proxy(options);
20 |
21 |
22 |
23 | const port = 3000;
24 | const app = express();
25 | const compiler = webpack(config);
26 |
27 | app.use(require('webpack-dev-middleware')(compiler, {
28 | noInfo: true,
29 | publicPath: config.output.publicPath
30 | }));
31 |
32 | app.use(require('webpack-hot-middleware')(compiler));
33 |
34 | app.use('/backend', backendProxy);
35 |
36 | app.get('*', function(req, res) {
37 | res.sendFile(path.join( __dirname, '../src/index.html'));
38 | });
39 |
40 |
41 |
42 | app.listen(port, function(err) {
43 | if (err) {
44 | console.log(err);
45 | } else {
46 | open(`http://localhost:${port}`);
47 | }
48 | });
49 |
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/route/RouteSupport.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.route
2 |
3 | import akka.http.scaladsl.model.{HttpHeader, StatusCodes}
4 | import akka.http.scaladsl.server.{Directives}
5 | import com.typesafe.scalalogging.LazyLogging
6 | import spray.json.JsonFormat
7 | import scalaz.{-\/, \/-}
8 | import com.cpuheater.starter.util.service.model._
9 | import scala.concurrent.{ExecutionContext, Future}
10 |
11 |
12 | trait RouteSupport extends LazyLogging with Directives {
13 |
14 | protected def toRestResponse[T: JsonFormat](serviceResponse: Future[ServiceResult[T]])(implicit ec: ExecutionContext) =
15 | serviceResponse.map{
16 | _ match {
17 | case \/-(Ok(data)) => (StatusCodes.OK, List.empty[HttpHeader], Some(Left(data)))
18 | case -\/(NotFound) => (StatusCodes.NotFound, List.empty[HttpHeader], None)
19 | case -\/(AuthenticationFailure) => (StatusCodes.Unauthorized, List.empty[HttpHeader], Some(Right("")))
20 | case -\/(ServiceClientFailure) => (StatusCodes.UnprocessableEntity, List.empty[HttpHeader], Some(Right("")))
21 | }
22 | }
23 |
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/frontend/src/public/style/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: white;
3 | font-size: 14px;
4 | color: #333;
5 | }
6 |
7 | [data-reactroot] {height: 100% !important; }
8 |
9 | a,
10 | a:focus,
11 | a:hover {
12 | color: #333;
13 | }
14 |
15 | .navbar-inverse {
16 | background-color: #333;
17 | border-color: #333;
18 | }
19 |
20 | .navbar {
21 | margin-bottom: 5px;
22 | }
23 |
24 |
25 | .header,
26 | .marketing,
27 | .footer {
28 | padding-right: 15px;
29 | padding-left: 15px;
30 | }
31 |
32 |
33 | .header {
34 | padding-bottom: 20px;
35 | }
36 |
37 | .header h3 {
38 | margin-top: 0;
39 | margin-bottom: 0;
40 | line-height: 40px;
41 | }
42 |
43 |
44 | .header {
45 | padding-bottom: 10px;
46 | border-bottom: none;
47 | }
48 |
49 | .nav>li>a {
50 | padding-right: 5px;
51 | padding-left: 5px;
52 | font-size: 14px;
53 | color: #333;
54 | font-weight: bold;
55 | }
56 |
57 | .nav>li {
58 | margin-left: 20px;
59 | }
60 |
61 | .nav > li {
62 | float: left;
63 | }
64 |
65 |
66 | .nav>li>a:focus, .nav>li>a:hover {
67 | text-decoration: none;
68 | background-color: transparent;
69 | color: cornflowerblue;
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/persistence/WalletDao.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.persistence
2 |
3 | import com.cpuheater.starter.model.{Wallet}
4 | import scalikejdbc.{AutoSession, WrappedResultSet, _}
5 | import scala.concurrent.{ExecutionContext, Future}
6 |
7 | object WalletDao {
8 |
9 | def create(wallet: Wallet)(implicit ec: ExecutionContext, session: AutoSession) : Future[Wallet] = {
10 | val id = sql"""insert into WALLET(ID, BALANCE) values (${wallet.id}, ${wallet.balance})""".update.apply()
11 | Future.successful(Wallet(id = id, balance = wallet.balance))
12 | }
13 |
14 |
15 | def update(wallet: Wallet)(implicit ec: ExecutionContext, session: AutoSession) : Future[Wallet] = {
16 | val id = sql"""UPDATE WALLET SET BALANCE=${wallet.balance} WHERE ID=${wallet.id};""".update().apply()
17 | Future.successful(wallet)
18 | }
19 |
20 |
21 | def get(id: Long)(implicit ec: ExecutionContext, session: AutoSession) : Future[Option[Wallet]] = {
22 | val wallet = sql"select * from WALLET where id = ${id}".map(rs => WalletDB(rs)).first().apply()
23 | Future.successful(wallet)
24 | }
25 |
26 | private object WalletDB extends SQLSyntaxSupport[Wallet] {
27 | override val tableName = "WALLET"
28 | def apply(rs: WrappedResultSet): Wallet =
29 | Wallet(id = rs.long("id"), balance = rs.bigDecimal("balance"))
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import 'rxjs';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { Provider } from 'react-redux';
5 | import { Router, Route, browserHistory, IndexRoute } from 'react-router';
6 | import { syncHistoryWithStore } from 'react-router-redux';
7 | import configureStore from './redux/store/configureStore';
8 | import App from './containers/App';
9 | import Dashboard from './containers/Dashboard';
10 | import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
11 | import './public/style/styles.css';
12 | import Login from './containers/Login';
13 | import Signup from './containers/SignUp';
14 |
15 | const store = configureStore();
16 | const history = syncHistoryWithStore(
17 | browserHistory,
18 | store
19 | );
20 |
21 | ReactDOM.render(
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ,
31 | document.getElementById('app')
32 | );
33 |
34 |
35 | function requireAuth(nextState, replace) {
36 | if (!sessionStorage.user) {
37 | replace({
38 | pathname: '/login',
39 | state: { nextPathname: nextState.location.pathname }
40 | })
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/webpack.config.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import path from 'path';
3 |
4 | export default {
5 | debug: true,
6 | devtool: 'inline-source-map',
7 | noInfo: false,
8 | entry: [
9 | 'eventsource-polyfill',
10 | 'webpack-hot-middleware/client?reload=true',
11 | path.resolve(__dirname, 'src/index')
12 | ],
13 | target: 'web',
14 | output: {
15 | path: __dirname + '/dist',
16 | publicPath: '/',
17 | filename: 'bundle.js'
18 | },
19 | devServer: {
20 | contentBase: path.resolve(__dirname, 'src')
21 | },
22 | plugins: [
23 | new webpack.HotModuleReplacementPlugin(),
24 | new webpack.NoErrorsPlugin()
25 | ],
26 | module: {
27 | loaders: [
28 | {
29 | test: /\.(js|jsx)$/,
30 | include: path.join(__dirname, 'src'),
31 | loader: 'babel'
32 | },
33 | {
34 | test: /\.(mp3)$/i,
35 | loaders: [
36 | 'file?hash=sha512&digest=hex&name=[hash].[ext]'
37 | ]
38 | },
39 | {test: /(\.css)$/, loaders: ['style', 'css']},
40 | {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file'},
41 | { test: /\.png$/, loader: "url-loader?mimetype=image/png" },
42 | {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream'},
43 | {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/svg+xml'},
44 | {test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff&name=fonts/[hash].[ext]" }
45 | ]
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/frontend/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component} from 'react';
2 | import { connect } from 'react-redux';
3 | import { logout } from '../redux/actions/loginActions';
4 |
5 | class App extends Component {
6 |
7 | constructor(props, context){
8 | super(props, context);
9 | this.logout = this.logout.bind(this);
10 | }
11 |
12 | logout() {
13 | const { dispatch } = this.props;
14 | dispatch(logout());
15 | }
16 |
17 | render() {
18 | return (
19 |
20 |
21 |
22 | {
23 | !!!sessionStorage.user &&
24 |
28 | }
29 | {
30 | sessionStorage.user &&
31 |
34 | }
35 |
36 |
37 |
38 | {this.props.children}
39 |
40 |
41 | )
42 | }
43 | };
44 |
45 |
46 | function mapStateToProps(state, ownProps) {
47 | const { session} = state;
48 | return {
49 | session
50 | }
51 | }
52 |
53 | export default connect(mapStateToProps)(App)
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/persistence/UserDao.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.persistence
2 |
3 | import com.cpuheater.starter.model.{NewUser, User}
4 | import scalikejdbc.{AutoSession, WrappedResultSet, _}
5 | import scala.concurrent.{ExecutionContext, Future}
6 |
7 | object UserDao {
8 |
9 | def create(user: NewUser)(implicit ec: ExecutionContext, session: AutoSession) : Future[User] = {
10 | val id = sql"""insert into USER
11 | (email, password) values (${user.email}, ${user.password})""".updateAndReturnGeneratedKey.apply()
12 | Future.successful(User(id= id, email = user.email, password = user.password))
13 | }
14 |
15 |
16 | def get(id: Long)(implicit ec: ExecutionContext, session: AutoSession) : Future[Option[User]] = {
17 | val user = sql"select * from USER where id = ${id}".map(rs => UserDB(rs)).first().apply()
18 | Future.successful(user)
19 | }
20 |
21 | def getByEmailAndPassword(email: String, password: String)(implicit ec: ExecutionContext, session: AutoSession) : Future[Option[User]] = {
22 | val user = sql"select * from USER where email = ${email} and password = ${password}".map(rs => UserDB(rs)).first().apply()
23 | Future.successful(user)
24 | }
25 |
26 |
27 | def getAll()(implicit ec: ExecutionContext, session: AutoSession) : Future[List[User]] = {
28 | val list = sql"select * from USER".map(rs => UserDB(rs)).list.apply()
29 | Future.successful(list)
30 | }
31 |
32 | private object UserDB extends SQLSyntaxSupport[User] {
33 | override val tableName = "USER"
34 | def apply(rs: WrappedResultSet): User =
35 | User(id = rs.long("id"), email = rs.string("email"), password = rs.string("password"))
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/route/WalletRoute.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.route
2 |
3 | import akka.http.scaladsl.marshalling.ToResponseMarshallable
4 |
5 | import scala.concurrent.ExecutionContext
6 | import akka.http.scaladsl.server.Directives
7 | import akka.actor.ActorSystem
8 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
9 | import com.cpuheater.starter.model.{RequestRest, SendMoney}
10 | import com.cpuheater.starter.service.WalletService
11 | import com.typesafe.scalalogging.LazyLogging
12 | import com.cpuheater.starter.json.StarterProtocol._
13 | import com.cpuheater.starter.service.WalletService
14 | import scalikejdbc.AutoSession
15 |
16 | trait WalletRoute extends Directives with LazyLogging with RouteSupport {
17 |
18 | protected implicit def ec: ExecutionContext
19 | protected implicit def session: AutoSession
20 |
21 | private val walletService = WalletService
22 |
23 | val walletRoute = {
24 | post {
25 | path("wallets" / LongNumber / "sendmoney") { (id) =>
26 | entity(as[SendMoney]) { deposit =>
27 | complete {
28 | ToResponseMarshallable(toRestResponse(walletService.requestMoney(id, deposit)))
29 | }
30 | }
31 | }
32 | }~
33 | get {
34 | path("wallets" / LongNumber) { (id) =>
35 | complete {
36 | ToResponseMarshallable(toRestResponse(walletService.get(id)))
37 | }
38 | }
39 | } ~
40 | post {
41 | path("wallets" / LongNumber / "requestmoney") { (id) =>
42 | entity(as[RequestRest]) { withdrawal =>
43 | complete {
44 | ToResponseMarshallable(toRestResponse(walletService.requestMoney(id, withdrawal)))
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/route/UserRoute.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.route
2 |
3 | import akka.http.scaladsl.marshalling.ToResponseMarshallable
4 | import akka.stream.ActorMaterializer
5 |
6 | import scala.concurrent.ExecutionContext
7 | import akka.http.scaladsl.server.{Directives, Route, RouteResult}
8 | import akka.actor.{ActorRef, ActorSystem}
9 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
10 | import akka.http.scaladsl.model.HttpRequest
11 | import com.cpuheater.starter.model.{NewUserRest, UserRest}
12 | import com.cpuheater.starter.service.UserService
13 | import com.typesafe.scalalogging.LazyLogging
14 | import com.cpuheater.starter.json.StarterProtocol._
15 | import com.cpuheater.starter.service.UserService
16 | import scalikejdbc.AutoSession
17 |
18 | trait UserRoute extends Directives with LazyLogging with RouteSupport {
19 |
20 | protected implicit def ec: ExecutionContext
21 | protected implicit def session: AutoSession
22 |
23 | private val userService = UserService
24 |
25 | val userRoute = {
26 | extractRequest { request: HttpRequest =>
27 | get {
28 | path("users" / LongNumber) { (id) =>
29 | complete {
30 | ToResponseMarshallable(toRestResponse(userService.get(id)))
31 | }
32 | }
33 | } ~
34 | post {
35 | path("login") {
36 | entity(as[NewUserRest]) { (user) =>
37 | complete {
38 | ToResponseMarshallable(toRestResponse(userService.login(user)))
39 | }
40 | }
41 | }
42 | } ~
43 | post {
44 | path("signup") {
45 | entity(as[NewUserRest]) { (user) =>
46 | complete {
47 | ToResponseMarshallable(toRestResponse(userService.create(user)))
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
56 |
57 |
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/service/UserService.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.service
2 |
3 | import com.cpuheater.starter.util.service.model.{AuthenticationFailure,Ok, ServiceResult}
4 | import com.cpuheater.starter.model._
5 | import com.cpuheater.starter.persistence.{WalletDao, UserDao}
6 | import com.typesafe.scalalogging.LazyLogging
7 | import scalikejdbc.AutoSession
8 |
9 | import scalaz.Scalaz._
10 | import scalaz._
11 | import scala.concurrent.{ExecutionContext, Future}
12 | import spray.json._
13 |
14 | import scalaz.OptionT.optionT
15 |
16 | object UserService extends LazyLogging {
17 |
18 | def create(newUserRest: NewUserRest)(implicit ec: ExecutionContext, session: AutoSession): Future[ServiceResult[UserRest]] = {
19 | val newUser = NewUser(email = newUserRest.email, password = newUserRest.password)
20 | UserDao.create(newUser).map{
21 | user =>
22 | Ok(UserRest(id = user.id, email = user.email)).right
23 | }
24 |
25 | }
26 |
27 | def login(user: NewUserRest)(implicit ec: ExecutionContext, session: AutoSession): Future[ServiceResult[UserRest]] = {
28 | val maybeUser = (for {
29 | user <- optionT(UserDao.getByEmailAndPassword(user.email, user.password))
30 | } yield user).run
31 |
32 | maybeUser.map{
33 | case Some(user) =>
34 | Ok(UserRest(id = user.id,
35 | email = user.email)).right
36 | case None =>
37 | AuthenticationFailure.left
38 | }
39 |
40 | }
41 |
42 | def get(id: Long)(implicit ec: ExecutionContext, session: AutoSession): Future[ServiceResult[UserRest]] = {
43 | val maybeUser = (for {
44 | user <- optionT(UserDao.get(id))
45 | } yield user).run
46 |
47 | maybeUser.map{
48 | case Some(user) =>
49 | Ok(UserRest(id = user.id, email = user.email)).right
50 | case None =>
51 | AuthenticationFailure.left
52 | }
53 |
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/StarterApp.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter
2 |
3 | import akka.actor.ActorSystem
4 | import akka.event.Logging
5 | import akka.http.scaladsl.Http
6 | import akka.http.scaladsl.server.directives.DebuggingDirectives
7 | import akka.util.Timeout
8 | import akka.stream.{ActorMaterializer, ActorMaterializerSettings, Supervision}
9 | import com.cpuheater.starter.persistence.UserDao
10 | import com.cpuheater.starter.route.{UserRoute, WalletRoute}
11 | import com.cpuheater.starter.util.FutureSupport
12 | import com.cpuheater.starter.util.db.DbSupport
13 | import com.typesafe.config.ConfigFactory
14 | import com.typesafe.scalalogging.LazyLogging
15 | import scala.concurrent.duration._
16 |
17 | object StarterApp extends App with UserRoute
18 | with WalletRoute with DbSupport with LazyLogging with FutureSupport{
19 |
20 | val decider: Supervision.Decider = { e =>
21 | logger.error(s"Ups exception while processing a stream$e")
22 | Supervision.Stop
23 | }
24 |
25 | implicit val actorSystem = ActorSystem("StarterApp", ConfigFactory.load)
26 | val materializerSettings = ActorMaterializerSettings(actorSystem).withSupervisionStrategy(decider)
27 | implicit val materializer = ActorMaterializer(materializerSettings)(actorSystem)
28 |
29 | implicit val ec = actorSystem.dispatcher
30 |
31 | val routes = {
32 | logRequestResult("starter") {
33 | userRoute ~ walletRoute
34 | }
35 | }
36 |
37 | implicit val timeout = Timeout(30.seconds)
38 |
39 | val routeLogging = DebuggingDirectives.logRequestResult("RouteLogging", Logging.InfoLevel)(routes)
40 |
41 | Http().bindAndHandle(routeLogging, StarterConfig.http.interface, StarterConfig.http.port)
42 | logger.info("App Started")
43 | logger.info("List of available users:")
44 | val users = UserDao.getAll().await()
45 | users.foreach(user => logger.info(s"User email: ${user.email}, password: ${user.password} "))
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:import/errors",
5 | "plugin:import/warnings"
6 | ],
7 | "plugins": [
8 | "react"
9 | ],
10 | "parserOptions": {
11 | "ecmaVersion": 6,
12 | "sourceType": "module",
13 | "ecmaFeatures": {
14 | "jsx": true
15 | }
16 | },
17 | "env": {
18 | "es6": true,
19 | "browser": true,
20 | "node": true,
21 | "jquery": true,
22 | "mocha": true
23 | },
24 | "rules": {
25 | "quotes": 0,
26 | "no-console": 1,
27 | "no-debugger": 1,
28 | "no-var": 1,
29 | "semi": [1, "always"],
30 | "no-trailing-spaces": 0,
31 | "eol-last": 0,
32 | "no-unused-vars": 0,
33 | "no-underscore-dangle": 0,
34 | "no-alert": 0,
35 | "no-lone-blocks": 0,
36 | "jsx-quotes": 1,
37 | "react/display-name": [ 1, {"ignoreTranspilerName": false }],
38 | "react/forbid-prop-types": [1, {"forbid": ["any"]}],
39 | "react/jsx-boolean-value": 1,
40 | "react/jsx-closing-bracket-location": 0,
41 | "react/jsx-curly-spacing": 1,
42 | "react/jsx-indent-props": 0,
43 | "react/jsx-key": 1,
44 | "react/jsx-max-props-per-line": 0,
45 | "react/jsx-no-bind": 1,
46 | "react/jsx-no-duplicate-props": 1,
47 | "react/jsx-no-literals": 0,
48 | "react/jsx-no-undef": 1,
49 | "react/jsx-pascal-case": 1,
50 | "react/jsx-sort-prop-types": 0,
51 | "react/jsx-sort-props": 0,
52 | "react/jsx-uses-react": 1,
53 | "react/jsx-uses-vars": 1,
54 | "react/no-danger": 1,
55 | "react/no-did-mount-set-state": 1,
56 | "react/no-did-update-set-state": 1,
57 | "react/no-direct-mutation-state": 1,
58 | "react/no-multi-comp": 1,
59 | "react/no-set-state": 0,
60 | "react/no-unknown-property": 1,
61 | "react/prefer-es6-class": 1,
62 | "react/prop-types": 1,
63 | "react/react-in-jsx-scope": 1,
64 | "react/require-extension": 1,
65 | "react/self-closing-comp": 1,
66 | "react/sort-comp": 1,
67 | "react/wrap-multilines": 1
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "atm",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "start": "npm-run-all --parallel open:src lint:watch",
6 | "open:src": "babel-node tools/server.js",
7 | "lint": "node_modules/.bin/esw webpack.config.js src tools",
8 | "lint:watch": "npm run lint -- --watch",
9 | "test:watch": "npm run test -- --watch"
10 | },
11 | "dependencies": {
12 | "axios": "^0.17.1",
13 | "babel-polyfill": "6.8.0",
14 | "bootstrap": "3.3.6",
15 | "jquery": "2.2.3",
16 | "react": "15.3.2",
17 | "react-dom": "15.3.0",
18 | "react-key-handler": "^0.3.0",
19 | "react-redux": "4.4.5",
20 | "react-router": "2.4.0",
21 | "react-router-redux": "4.0.4",
22 | "redux": "3.5.2",
23 | "redux-thunk": "2.0.1",
24 | "reflexbox": "^2.2.3",
25 | "rxjs": "^5.0.0-beta.10",
26 | "toastr": "2.1.2"
27 | },
28 | "devDependencies": {
29 | "babel-cli": "6.8.0",
30 | "babel-core": "6.11.4",
31 | "babel-loader": "6.2.4",
32 | "babel-plugin-react-display-name": "2.0.0",
33 | "babel-preset-es2015": "6.9.0",
34 | "babel-preset-react": "6.11.1",
35 | "babel-preset-react-hmre": "1.1.1",
36 | "babel-register": "6.8.0",
37 | "cheerio": "0.22.0",
38 | "compression": "1.6.1",
39 | "cross-env": "1.0.7",
40 | "css-loader": "0.23.1",
41 | "enzyme": "2.2.0",
42 | "eslint": "2.9.0",
43 | "eslint-plugin-import": "1.6.1",
44 | "eslint-plugin-react": "5.0.1",
45 | "eslint-watch": "2.1.11",
46 | "eventsource-polyfill": "0.9.6",
47 | "expect": "1.19.0",
48 | "express": "4.13.4",
49 | "extract-text-webpack-plugin": "1.0.1",
50 | "file-loader": "0.11.1",
51 | "http-proxy-middleware": "^0.17.4",
52 | "jsdom": "8.5.0",
53 | "mocha": "2.4.5",
54 | "nock": "8.0.0",
55 | "npm-run-all": "1.8.0",
56 | "open": "0.0.5",
57 | "react-addons-test-utils": "15.0.2",
58 | "redux-immutable-state-invariant": "1.2.3",
59 | "redux-mock-store": "1.0.2",
60 | "rimraf": "2.5.2",
61 | "style-loader": "0.13.1",
62 | "url-loader": "0.5.7",
63 | "webpack": "1.13.0",
64 | "webpack-dev-middleware": "1.6.1",
65 | "webpack-hot-middleware": "2.10.0",
66 | "webpack-obfuscator": "^0.9.1"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/frontend/src/containers/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { login } from '../redux/actions/loginActions';
4 |
5 |
6 | class Login extends React.Component {
7 |
8 | constructor(props) {
9 | super(props);
10 | this.state = {email: '', password: ''};
11 | this.login = this.login.bind(this);
12 | this.handleChange = this.handleChange.bind(this);
13 | }
14 |
15 | login(event) {
16 | event.preventDefault();
17 | const { dispatch } = this.props;
18 | dispatch(login(this.state));
19 | }
20 |
21 | handleChange(event) {
22 | const target = event.target;
23 | const value = target.value;
24 | const name = target.name;
25 |
26 | this.setState({
27 | [name]: value
28 | });
29 | }
30 |
31 |
32 | render() {
33 | return (
34 |
59 | );
60 | }
61 | }
62 |
63 | function mapStateToProps(state, ownProps) {
64 | const { session} = state;
65 | return {
66 | session
67 | }
68 | }
69 |
70 | export default connect(mapStateToProps)(Login)
--------------------------------------------------------------------------------
/frontend/src/containers/SignUp.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { signup } from '../redux/actions/signupActions';
4 |
5 |
6 | class SignUp extends React.Component {
7 |
8 | constructor(props) {
9 | super(props);
10 | this.state = {email: '', password: ''};
11 | this.signup = this.signup.bind(this);
12 | this.handleChange = this.handleChange.bind(this);
13 | }
14 |
15 | signup(event) {
16 | event.preventDefault();
17 | const { dispatch } = this.props;
18 | dispatch(signup(this.state));
19 | }
20 |
21 | handleChange(event) {
22 | const target = event.target;
23 | const value = target.value;
24 | const name = target.name;
25 |
26 | this.setState({
27 | [name]: value
28 | });
29 | }
30 |
31 |
32 | render() {
33 | return (
34 |
60 | );
61 | }
62 | }
63 |
64 | function mapStateToProps(state, ownProps) {
65 | const { session} = state;
66 | return {
67 | session
68 | }
69 | }
70 |
71 | export default connect(mapStateToProps)(SignUp)
--------------------------------------------------------------------------------
/backend/src/main/scala/com/cpuheater/starter/service/WalletService.scala:
--------------------------------------------------------------------------------
1 | package com.cpuheater.starter.service
2 |
3 | import com.cpuheater.starter.util.service.model._
4 | import com.cpuheater.starter.model._
5 | import com.cpuheater.starter.persistence.{WalletDao, UserDao}
6 | import scalikejdbc.AutoSession
7 | import scalaz.Scalaz._
8 | import scalaz._
9 | import scala.concurrent.{ExecutionContext, Future}
10 | import spray.json._
11 | import scalaz.OptionT._
12 |
13 | object WalletService {
14 |
15 | def get(userId: Long)(implicit ec: ExecutionContext, session: AutoSession): Future[ServiceResult[WalletRest]] = {
16 |
17 | val maybeWalet = (for {
18 | user <- optionT(UserDao.get(userId))
19 | wallet <- optionT(WalletDao.get(user.id))
20 | } yield wallet).run
21 |
22 |
23 | maybeWalet.map{
24 | case Some(wallet) =>
25 | Ok(WalletRest(balance = wallet.balance)).right
26 | case None =>
27 | NotFound.left
28 | }
29 |
30 | }
31 |
32 | def requestMoney(userId: Long, deposit: SendMoney)(implicit ec: ExecutionContext, session: AutoSession): Future[ServiceResult[WalletRest]] = {
33 | val maybeWallet = (for {
34 | user <- optionT(UserDao.get(userId))
35 | wallet <- optionT(WalletDao.get(user.id))
36 | } yield wallet).run
37 |
38 |
39 | maybeWallet.flatMap{
40 | case Some(wallet) =>
41 | val updatedWallet = wallet.copy(balance = wallet.balance + deposit.amount)
42 | WalletDao.update(updatedWallet).map{
43 | wallet =>
44 | Ok(WalletRest(balance = wallet.balance)).right
45 | }
46 | case None =>
47 | Future.successful(NotFound.left)
48 | }
49 | }
50 |
51 | def requestMoney(userId: Long, withdrawal: RequestRest)(implicit ec: ExecutionContext, session: AutoSession): Future[ServiceResult[WalletRest]] = {
52 | val maybeWallet = (for {
53 | user <- optionT(UserDao.get(userId))
54 | wallet <- optionT(WalletDao.get(user.id))
55 | } yield wallet).run
56 |
57 |
58 | maybeWallet.flatMap{
59 | case Some(wallet) =>
60 | val newBalance = wallet.balance - withdrawal.amount
61 | if(newBalance < 0 ){
62 | Future.successful(ServiceClientFailure.left)
63 | } else {
64 | val updatedWallet = wallet.copy(balance = newBalance)
65 | WalletDao.update(updatedWallet).map{
66 | wallet =>
67 | Ok(WalletRest(balance = wallet.balance)).right
68 | }
69 | }
70 | case None =>
71 | Future.successful(NotFound.left)
72 | }
73 | }
74 |
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/frontend/src/containers/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { loadWallet } from '../redux/actions/loadWalletActions';
4 | import { sendMoney } from '../redux/actions/sendMoneyActions';
5 | import { requestMoney } from '../redux/actions/requestMoneyActions';
6 |
7 | class Dashboard extends React.Component {
8 |
9 | constructor(props) {
10 | super(props);
11 | this.handleChange = this.handleChange.bind(this);
12 | this.sendMoney = this.sendMoney.bind(this);
13 | this.requestMoney = this.requestMoney.bind(this);
14 | this.state = {send: '', request: ''};
15 | }
16 |
17 | componentWillMount() {
18 | const user = JSON.parse(sessionStorage.user);
19 | this.props.loadWallet(user.id);
20 | }
21 |
22 | sendMoney(event) {
23 | event.preventDefault();
24 | const user = JSON.parse(sessionStorage.user);
25 | this.props.sendMoney({amount: parseInt(this.state.send), id: user.id});
26 | }
27 |
28 | requestMoney(event) {
29 | event.preventDefault();
30 | const user = JSON.parse(sessionStorage.user);
31 | this.props.requestMoney({amount: parseInt(this.state.request), id: user.id})
32 | }
33 |
34 | handleChange(event) {
35 | const target = event.target;
36 | const value = target.value;
37 | const name = target.name;
38 |
39 | this.setState({
40 | [name]: value
41 | });
42 | }
43 |
44 | render() {
45 | const {wallet} = this.props;
46 | const user = JSON.parse(sessionStorage.user);
47 | return (
48 |
49 |
50 |
51 |
52 |
{user.email}
53 | Wallet Balance
54 | { !(wallet ==null) &&
55 | {wallet.balance} USD
56 | }
57 |
58 |
59 |
74 |
89 |
90 | );
91 | }
92 | }
93 |
94 | export default connect(
95 | ({ wallet}) => ({
96 | wallet
97 | }),
98 | { loadWallet, sendMoney, requestMoney}
99 | )((Dashboard));
100 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------