initial commit

This commit is contained in:
Patrick Nagurny
2018-10-19 15:31:41 -04:00
commit e2dd29259f
203 changed files with 44839 additions and 0 deletions

302
core/api/account.go Normal file
View File

@@ -0,0 +1,302 @@
package api
import (
"encoding/json"
"github.com/ant0ine/go-json-rest/rest"
"github.com/openaccounting/oa-server/core/model"
"github.com/openaccounting/oa-server/core/model/types"
"io/ioutil"
"net/http"
"strconv"
"time"
)
/**
* @api {get} /orgs/:orgId/accounts Get Accounts by Org id
* @apiVersion 1.0.0
* @apiName GetOrgAccounts
* @apiGroup Account
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccess {String} id Id of the Account.
* @apiSuccess {String} orgId Id of the Org.
* @apiSuccess {Date} inserted Date Account was created
* @apiSuccess {Date} updated Date Account was updated
* @apiSuccess {String} name Name of the Account.
* @apiSuccess {String} parent Id of the parent Account.
* @apiSuccess {String} currency Three letter currency code.
* @apiSuccess {Number} precision How many digits the currency goes out to.
* @apiSuccess {Boolean} debitBalance True if Account has a debit balance.
* @apiSuccess {Number} balance Current Account balance in this Account's currency
* @apiSuccess {Number} nativeBalance Current Account balance in the Org's currency
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* [
* {
* "id": "22222222222222222222222222222222",
* "orgId": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "name": "Cash",
* "parent": "11111111111111111111111111111111",
* "currency": "USD",
* "precision": 2,
* "debitBalance": true,
* "balance": 10000,
* "nativeBalance": 10000
* }
* ]
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func GetOrgAccounts(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
// TODO how do we make date an optional parameter
// instead of resorting to this hack?
date := time.Date(2100, time.January, 1, 0, 0, 0, 0, time.UTC)
dateParam := r.URL.Query().Get("date")
if dateParam != "" {
dateParamNumeric, err := strconv.ParseInt(dateParam, 10, 64)
if err != nil {
rest.Error(w, "invalid date", 400)
return
}
date = time.Unix(0, dateParamNumeric*1000000)
}
accounts, err := model.Instance.GetAccountsWithBalances(orgId, user.Id, "", date)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&accounts)
}
/**
* @api {post} /orgs/:orgId/accounts Create a new Account
* @apiVersion 1.0.0
* @apiName PostAccount
* @apiGroup Account
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} id Id 32 character hex string
* @apiParam {String} name Name of the Account.
* @apiParam {String} parent Id of the parent Account.
* @apiParam {String} currency Three letter currency code.
* @apiParam {Number} precision How many digits the currency goes out to.
* @apiParam {Boolean} debitBalance True if account has a debit balance.
* @apiParam {Number} balance Current Account balance in this Account's currency
* @apiParam {Number} nativeBalance Current Account balance in the Org's currency
*
* @apiSuccess {String} id Id of the Account.
* @apiSuccess {String} orgId Id of the Org.
* @apiSuccess {Date} inserted Date Account was created
* @apiSuccess {Date} updated Date Account was updated
* @apiSuccess {String} name Name of the Account.
* @apiSuccess {String} parent Id of the parent Account.
* @apiSuccess {String} currency Three letter currency code.
* @apiSuccess {Number} precision How many digits the currency goes out to.
* @apiSuccess {Boolean} debitBalance True if account has a debit balance.
* @apiSuccess {Number} balance Current Account balance in this Account's currency
* @apiSuccess {Number} nativeBalance Current Account balance in the Org's currency
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "22222222222222222222222222222222",
* "orgId": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "name": "Cash",
* "parent": "11111111111111111111111111111111",
* "currency": "USD",
* "precision": 2,
* "debitBalance": true,
* "balance": 10000,
* "nativeBalance": 10000
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PostAccount(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
content, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(content) == 0 {
rest.Error(w, "JSON payload is empty", http.StatusInternalServerError)
return
}
account := types.NewAccount()
err = json.Unmarshal(content, &account)
if err != nil {
// Maybe it's an array of accounts?
PostAccounts(w, r, content)
return
}
account.OrgId = orgId
err = model.Instance.CreateAccount(account, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&account)
}
func PostAccounts(w rest.ResponseWriter, r *rest.Request, content []byte) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
accounts := make([]*types.Account, 0)
err := json.Unmarshal(content, &accounts)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, account := range accounts {
account.OrgId = orgId
err = model.Instance.CreateAccount(account, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
w.WriteJson(accounts)
}
/**
* @api {put} /orgs/:orgId/accounts/:accountId Modify an Account
* @apiVersion 1.0.0
* @apiName PutAccount
* @apiGroup Account
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} id Id 32 character hex string
* @apiParam {String} name Name of the Account.
* @apiParam {String} parent Id of the parent Account.
* @apiParam {String} currency Three letter currency code.
* @apiParam {Number} precision How many digits the currency goes out to.
* @apiParam {Boolean} debitBalance True if Account has a debit balance.
* @apiParam {Number} balance Current Account balance in this Account's currency
* @apiParam {Number} nativeBalance Current Account balance in the Org's currency
*
* @apiSuccess {String} id Id of the Account.
* @apiSuccess {String} orgId Id of the Org.
* @apiSuccess {Date} inserted Date Account was created
* @apiSuccess {Date} updated Date Account was updated
* @apiSuccess {String} name Name of the Account.
* @apiSuccess {String} parent Id of the parent Account.
* @apiSuccess {String} currency Three letter currency code.
* @apiSuccess {Number} precision How many digits the currency goes out to.
* @apiSuccess {Boolean} debitBalance True if Account has a debit balance.
* @apiSuccess {Number} balance Current Account balance in this Account's currency
* @apiSuccess {Number} nativeBalance Current Account balance in the Org's currency
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "22222222222222222222222222222222",
* "orgId": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "name": "Cash",
* "parent": "11111111111111111111111111111111",
* "currency": "USD",
* "precision": 2,
* "debitBalance": true,
* "balance": 10000,
* "nativeBalance": 10000
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PutAccount(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
accountId := r.PathParam("accountId")
account := types.Account{}
err := r.DecodeJsonPayload(&account)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
account.Id = accountId
account.OrgId = orgId
err = model.Instance.UpdateAccount(&account, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&account)
}
/**
* @api {delete} /orgs/:orgId/accounts/:accountId Delete an Account
* @apiVersion 1.0.0
* @apiName DeleteAccount
* @apiGroup Account
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func DeleteAccount(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
accountId := r.PathParam("accountId")
err := model.Instance.DeleteAccount(accountId, user.Id, orgId)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

77
core/api/api.go Normal file
View File

@@ -0,0 +1,77 @@
package api
import (
"github.com/ant0ine/go-json-rest/rest"
)
/**
* @apiDefine NotAuthorizedError
*
* @apiError NotAuthorized API request does not have proper credentials
*
* @apiErrorExample Error-Response:
* HTTP/1.1 403 Not Authorized
*/
/**
* @apiDefine InternalServerError
*
* @apiError InternalServer An internal error occurred
*
* @apiErrorExample Error-Response:
* HTTP/1.1 500 Internal Server Error
* {
* "error": "id required"
* }
*
*/
func Init() (*rest.Api, error) {
rest.ErrorFieldName = "error"
app := rest.NewApi()
logger := &LoggerMiddleware{}
var stack = []rest.Middleware{
logger,
&rest.RecorderMiddleware{},
&rest.TimerMiddleware{},
&rest.PoweredByMiddleware{},
&rest.RecoverMiddleware{},
&rest.GzipMiddleware{},
&rest.ContentTypeCheckerMiddleware{},
}
app.Use(stack...)
app.Use(&rest.CorsMiddleware{
RejectNonCorsRequests: false,
OriginValidator: func(origin string, request *rest.Request) bool {
//return origin == "http://localhost:4200"
return true
},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{
"Accept", "Content-Type", "X-Custom-Header", "Origin", "Authorization", "Accept-Version"},
AccessControlAllowCredentials: true,
AccessControlMaxAge: 3600,
})
auth := &AuthMiddleware{
Realm: "openaccounting",
}
version := &VersionMiddleware{}
app.Use(auth)
app.Use(version)
router, err := GetRouter(auth)
if err != nil {
return nil, err
}
app.SetApp(router)
return app, nil
}

188
core/api/apikey.go Normal file
View File

@@ -0,0 +1,188 @@
package api
import (
"github.com/ant0ine/go-json-rest/rest"
"github.com/openaccounting/oa-server/core/model"
"github.com/openaccounting/oa-server/core/model/types"
"net/http"
)
/**
* @api {get} /apikeys Get API keys
* @apiVersion 1.0.0
* @apiName GetApiKeys
* @apiGroup ApiKey
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccess {String} id Id of the ApiKey.
* @apiSuccess {Date} inserted Date ApiKey was created
* @apiSuccess {Date} updated Date Last activity for the ApiKey
* @apiSuccess {String} userId Id of the User
* @apiSuccess {String} label Label
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* [
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "userId": "22222222222222222222222222222222",
* "label": "Shopping Cart"
* }
* ]
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func GetApiKeys(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
keys, err := model.Instance.GetApiKeys(user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(keys)
}
/**
* @api {post} /apikeys Create a new API key
* @apiVersion 1.0.0
* @apiName PostApiKey
* @apiGroup ApiKey
*
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
* @apiHeader {String} Authorization HTTP Basic Auth
*
* @apiParam {String} id 32 character hex string
* @apiParam {String} label Label
*
* @apiSuccess {String} id Id of the ApiKey.
* @apiSuccess {Date} inserted Date ApiKey was created
* @apiSuccess {Date} updated Date Last activity for the ApiKey
* @apiSuccess {String} userId Id of the User
* @apiSuccess {String} label Label
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "userId": "22222222222222222222222222222222",
* "label": "Shopping Cart"
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PostApiKey(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
key := &types.ApiKey{}
err := r.DecodeJsonPayload(key)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
key.UserId = user.Id
err = model.Instance.CreateApiKey(key)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(key)
}
/**
* @api {put} /apikeys Modify an API key
* @apiVersion 1.0.0
* @apiName PutApiKey
* @apiGroup ApiKey
*
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
* @apiHeader {String} Authorization HTTP Basic Auth
*
* @apiParam {String} id 32 character hex string
* @apiParam {String} label Label
*
* @apiSuccess {String} id Id of the ApiKey.
* @apiSuccess {Date} inserted Date ApiKey was created
* @apiSuccess {Date} updated Date Last activity for the ApiKey
* @apiSuccess {String} userId Id of the User
* @apiSuccess {String} label Label
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "userId": "22222222222222222222222222222222",
* "label": "Shopping Cart"
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PutApiKey(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
key := &types.ApiKey{}
keyId := r.PathParam("apiKeyId")
err := r.DecodeJsonPayload(key)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
key.Id = keyId
key.UserId = user.Id
err = model.Instance.UpdateApiKey(key)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(key)
}
/**
* @api {delete} /apikeys/:apiKeyId Delete an API key
* @apiVersion 1.0.0
* @apiName DeleteApiKey
* @apiGroup ApiKey
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func DeleteApiKey(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
id := r.PathParam("apiKeyId")
err := model.Instance.DeleteApiKey(id, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

93
core/api/auth.go Normal file
View File

@@ -0,0 +1,93 @@
package api
import (
"encoding/base64"
"errors"
"github.com/ant0ine/go-json-rest/rest"
"github.com/openaccounting/oa-server/core/auth"
"log"
"net/http"
"strings"
)
type AuthMiddleware struct {
// Realm name to display to the user. Required.
Realm string
}
// MiddlewareFunc makes AuthMiddleware implement the Middleware interface.
func (mw *AuthMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc {
if mw.Realm == "" {
log.Fatal("Realm is required")
}
return func(writer rest.ResponseWriter, request *rest.Request) {
authHeader := request.Header.Get("Authorization")
if authHeader == "" {
request.Env["USER"] = nil
handler(writer, request)
return
}
emailOrKey, password, err := mw.decodeBasicAuthHeader(authHeader)
if err != nil {
rest.Error(writer, "Invalid authentication", http.StatusBadRequest)
return
}
// authenticate via session, apikey or user
user, err := auth.Instance.Authenticate(emailOrKey, password)
if err == nil {
request.Env["USER"] = user
handler(writer, request)
return
}
log.Println("Unauthorized " + emailOrKey)
mw.unauthorized(writer)
return
}
}
func (mw *AuthMiddleware) unauthorized(writer rest.ResponseWriter) {
writer.Header().Set("WWW-Authenticate", "Basic realm="+mw.Realm)
rest.Error(writer, "Not Authorized", http.StatusUnauthorized)
}
func (mw *AuthMiddleware) decodeBasicAuthHeader(header string) (user string, password string, err error) {
parts := strings.SplitN(header, " ", 2)
if !(len(parts) == 2 && parts[0] == "Basic") {
return "", "", errors.New("Invalid authentication")
}
decoded, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return "", "", errors.New("Invalid base64")
}
creds := strings.SplitN(string(decoded), ":", 2)
if len(creds) != 2 {
return "", "", errors.New("Invalid authentication")
}
return creds[0], creds[1], nil
}
func (mw *AuthMiddleware) RequireAuth(handler rest.HandlerFunc) rest.HandlerFunc {
return func(writer rest.ResponseWriter, request *rest.Request) {
if request.Env["USER"] == nil {
mw.unauthorized(writer)
return
}
handler(writer, request)
}
}

89
core/api/logger.go Normal file
View File

@@ -0,0 +1,89 @@
package api
import (
"github.com/ant0ine/go-json-rest/rest"
"github.com/openaccounting/oa-server/core/model/types"
"log"
"net"
"os"
"strconv"
"time"
)
type LoggerMiddleware struct {
Logger *log.Logger
}
func (mw *LoggerMiddleware) MiddlewareFunc(h rest.HandlerFunc) rest.HandlerFunc {
// set the default Logger
if mw.Logger == nil {
mw.Logger = log.New(os.Stderr, "", 0)
}
return func(w rest.ResponseWriter, r *rest.Request) {
h(w, r)
message := getIp(r)
message = message + " " + getUser(r)
message = message + " " + getTime(r)
message = message + " " + getRequest(r)
message = message + " " + getStatus(r)
message = message + " " + getBytes(r)
message = message + " " + getUserAgent(r)
mw.Logger.Print(message)
}
}
func getIp(r *rest.Request) string {
remoteAddr := r.RemoteAddr
if remoteAddr != "" {
if ip, _, err := net.SplitHostPort(remoteAddr); err == nil {
return ip
}
}
return ""
}
func getUser(r *rest.Request) string {
if r.Env["USER"] != nil {
user := r.Env["USER"].(*types.User)
return user.Email
}
return "-"
}
func getTime(r *rest.Request) string {
if r.Env["START_TIME"] != nil {
return r.Env["START_TIME"].(*time.Time).Format("02/Jan/2006:15:04:05 -0700")
}
return "-"
}
func getRequest(r *rest.Request) string {
return r.Method + " " + r.URL.RequestURI()
}
func getStatus(r *rest.Request) string {
if r.Env["STATUS_CODE"] != nil {
return strconv.Itoa(r.Env["STATUS_CODE"].(int))
}
return "-"
}
func getBytes(r *rest.Request) string {
if r.Env["BYTES_WRITTEN"] != nil {
return strconv.FormatInt(r.Env["BYTES_WRITTEN"].(int64), 10)
}
return "-"
}
func getUserAgent(r *rest.Request) string {
if r.UserAgent() != "" {
return r.UserAgent()
}
return "-"
}

394
core/api/org.go Normal file
View File

@@ -0,0 +1,394 @@
package api
import (
"github.com/ant0ine/go-json-rest/rest"
"github.com/openaccounting/oa-server/core/model"
"github.com/openaccounting/oa-server/core/model/types"
"net/http"
)
/**
* @api {get} /org/:orgId Get Org by id
* @apiVersion 1.0.0
* @apiName GetOrg
* @apiGroup Org
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccess {String} id Id of the Org.
* @apiSuccess {Date} inserted Date Org was created
* @apiSuccess {Date} updated Date Org was updated
* @apiSuccess {String} name Name of the Org.
* @apiSuccess {String} currency Three letter currency code.
* @apiSuccess {Number} precision How many digits the currency goes out to.
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "name": "MyOrg",
* "currency": "USD",
* "precision": 2,
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func GetOrg(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
org, err := model.Instance.GetOrg(orgId, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&org)
}
/**
* @api {get} /orgs Get a User's Orgs
* @apiVersion 1.0.0
* @apiName GetOrgs
* @apiGroup Org
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccess {String} id Id of the Org.
* @apiSuccess {Date} inserted Date Org was created
* @apiSuccess {Date} updated Date Org was updated
* @apiSuccess {String} name Name of the Org.
* @apiSuccess {String} currency Three letter currency code.
* @apiSuccess {Number} precision How many digits the currency goes out to.
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* [
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "name": "MyOrg",
* "currency": "USD",
* "precision": 2,
* }
* ]
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func GetOrgs(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgs, err := model.Instance.GetOrgs(user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&orgs)
}
/**
* @api {post} /orgs Create a new Org
* @apiVersion 1.0.0
* @apiName PostOrg
* @apiGroup Org
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} id Id 32 character hex string
* @apiParam {String} name Name of the Org.
* @apiParam {String} currency Three letter currency code.
* @apiParam {Number} precision How many digits the currency goes out to.
*
* @apiSuccess {String} id Id of the Org.
* @apiSuccess {Date} inserted Date Org was created
* @apiSuccess {Date} updated Date Org was updated
* @apiSuccess {String} name Name of the Org.
* @apiSuccess {String} currency Three letter currency code.
* @apiSuccess {Number} precision How many digits the currency goes out to.
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "name": "MyOrg",
* "currency": "USD",
* "precision": 2,
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PostOrg(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
org := types.Org{Precision: 2}
err := r.DecodeJsonPayload(&org)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = model.Instance.CreateOrg(&org, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&org)
}
/**
* @api {put} /orgs/:orgId Modify an Org
* @apiVersion 1.0.0
* @apiName PutOrg
* @apiGroup Org
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} name Name of the Org.
*
* @apiSuccess {String} id Id of the Org.
* @apiSuccess {Date} inserted Date Org was created
* @apiSuccess {Date} updated Date Org was updated
* @apiSuccess {String} name Name of the Org.
* @apiSuccess {String} currency Three letter currency code.
* @apiSuccess {Number} precision How many digits the currency goes out to.
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "name": "MyOrg",
* "currency": "USD",
* "precision": 2,
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PutOrg(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
org := types.Org{}
err := r.DecodeJsonPayload(&org)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
org.Id = orgId
err = model.Instance.UpdateOrg(&org, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&org)
}
/**
* @api {post} /orgs/:orgId/invites Invite a user to an Org
* @apiVersion 1.0.0
* @apiName PostInvite
* @apiGroup Org
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} email Email address of user
*
* @apiSuccess {String} id Id of the Invite
* @apiSuccess {orgId} id Id of the Org
* @apiSuccess {Date} inserted Date Invite was created
* @apiSuccess {Date} updated Date Invite was updated/accepted
* @apiSuccess {String} email Email address of user
* @apiSuccess {String} accepted true if user has accepted
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "a1b2c3d4",
* "orgId": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "email": "johndoe@email.com",
* "accepted": false
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PostInvite(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
invite := types.Invite{}
err := r.DecodeJsonPayload(&invite)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
invite.OrgId = orgId
err = model.Instance.CreateInvite(&invite, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&invite)
}
/**
* @api {put} /orgs/:orgId/invites/:inviteId Accept an invitation
* @apiVersion 1.0.0
* @apiName PutInvite
* @apiGroup Org
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} accepted true
*
* @apiSuccess {String} id Id of the Invite
* @apiSuccess {orgId} id Id of the Org
* @apiSuccess {Date} inserted Date Invite was created
* @apiSuccess {Date} updated Date Invite was updated/accepted
* @apiSuccess {String} email Email address of user
* @apiSuccess {String} accepted true if user has accepted
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "a1b2c3d4",
* "orgId": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "email": "johndoe@email.com",
* "accepted": true
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PutInvite(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
//orgId := r.PathParam("orgId")
inviteId := r.PathParam("inviteId")
invite := types.Invite{}
err := r.DecodeJsonPayload(&invite)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
invite.Id = inviteId
err = model.Instance.AcceptInvite(&invite, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&invite)
}
/**
* @api {get} /orgs/:orgId/invites Get Org invites
* @apiVersion 1.0.0
* @apiName GetInvites
* @apiGroup Org
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccess {String} id Id of the Invite
* @apiSuccess {orgId} id Id of the Org
* @apiSuccess {Date} inserted Date Invite was created
* @apiSuccess {Date} updated Date Invite was updated/accepted
* @apiSuccess {String} email Email address of user
* @apiSuccess {String} accepted true if user has accepted
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* [
* {
* "id": "a1b2c3d4",
* "orgId": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "email": "johndoe@email.com",
* "accepted": true
* }
* ]
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func GetInvites(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
invites, err := model.Instance.GetInvites(orgId, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&invites)
}
/**
* @api {delete} /orgs/:orgId/invites/:inviteId Delete Invite
* @apiVersion 1.0.0
* @apiName DeleteInvite
* @apiGroup Org
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func DeleteInvite(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
inviteId := r.PathParam("inviteId")
err := model.Instance.DeleteInvite(inviteId, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

183
core/api/price.go Normal file
View File

@@ -0,0 +1,183 @@
package api
import (
"github.com/ant0ine/go-json-rest/rest"
"github.com/openaccounting/oa-server/core/model"
"github.com/openaccounting/oa-server/core/model/types"
"net/http"
"strconv"
"time"
)
/**
* @api {get} /org/:orgId/prices Get prices nearest in time or by currency
* @apiVersion 1.0.0
* @apiName GetPrices
* @apiGroup Price
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {Number} nearestDate Milliseconds since epoch
* @apiParam {String} currency Currency code
*
* @apiSuccess {String} id Id of the Price.
* @apiSuccess {String} orgId Id of the Org.
* @apiSuccess {String} currency Currency code.
* @apiSuccess {Date} date Date of the Price.
* @apiSuccess {Date} inserted Date when Price was posted.
* @apiSuccess {Date} updated Date when Price was updated.
* @apiSuccess {Number} price Price of currency measured in native Org currency.
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* [
* {
* "id": "11111111111111111111111111111111",
* "orgId": "11111111111111111111111111111111",
* "currency": "EUR",
* "date": "2018-09-11T18:05:04.420Z",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "price": 1.16
* }
* ]
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func GetPrices(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
// TODO how do we make date an optional parameter
// instead of resorting to this hack?
nearestDate := time.Date(2100, time.January, 1, 0, 0, 0, 0, time.UTC)
nearestDateParam := r.URL.Query().Get("nearestDate")
currencyParam := r.URL.Query().Get("currency")
// If currency was specified, get all prices for that currency
if currencyParam != "" {
prices, err := model.Instance.GetPricesByCurrency(orgId, currencyParam, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(prices)
return
}
if nearestDateParam != "" {
nearestDateParamNumeric, err := strconv.ParseInt(nearestDateParam, 10, 64)
if err != nil {
rest.Error(w, "invalid date", 400)
return
}
nearestDate = time.Unix(0, nearestDateParamNumeric*1000000)
}
// Get prices nearest in time
prices, err := model.Instance.GetPricesNearestInTime(orgId, nearestDate, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(prices)
}
/**
* @api {post} /orgs/:orgId/prices Create a new Price
* @apiVersion 1.0.0
* @apiName PostPrice
* @apiGroup Price
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} id Id 32 character hex string.
* @apiParam {String} orgId Id of the Org.
* @apiParam {String} currency Currency code.
* @apiParam {Date} date Date of the Price.
* @apiParam {Number} price Price of currency measured in native Org currency.
*
* @apiSuccess {String} id Id of the Price.
* @apiSuccess {String} orgId Id of the Org.
* @apiSuccess {String} currency Currency code.
* @apiSuccess {Date} date Date of the Price.
* @apiSuccess {Date} inserted Date when Price was posted.
* @apiSuccess {Date} updated Date when Price was updated.
* @apiSuccess {Number} price Price of currency measured in native Org currency.
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "orgId": "11111111111111111111111111111111",
* "currency": "EUR",
* "date": "2018-09-11T18:05:04.420Z",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "price": 1.16
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PostPrice(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
price := types.Price{}
err := r.DecodeJsonPayload(&price)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
price.OrgId = orgId
err = model.Instance.CreatePrice(&price, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&price)
}
/**
* @api {delete} /orgs/:orgId/prices/:priceId Delete a Price
* @apiVersion 1.0.0
* @apiName DeletePrice
* @apiGroup Price
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func DeletePrice(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
priceId := r.PathParam("priceId")
err := model.Instance.DeletePrice(priceId, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

48
core/api/routes.go Normal file
View File

@@ -0,0 +1,48 @@
package api
import (
"github.com/ant0ine/go-json-rest/rest"
"github.com/openaccounting/oa-server/core/ws"
)
func GetRouter(auth *AuthMiddleware) (rest.App, error) {
return rest.MakeRouter(
rest.Get("/api/user", auth.RequireAuth(GetUser)),
rest.Put("/api/user", PutUser),
rest.Post("/api/user/verify", VerifyUser),
rest.Post("/api/user/reset-password", ResetPassword),
rest.Post("/api/users", PostUser),
rest.Post("/api/orgs", auth.RequireAuth(PostOrg)),
rest.Get("/api/orgs", auth.RequireAuth(GetOrgs)),
rest.Get("/api/orgs/:orgId", auth.RequireAuth(GetOrg)),
rest.Put("/api/orgs/:orgId", auth.RequireAuth(PutOrg)),
rest.Get("/api/orgs/:orgId/ledgers", auth.RequireAuth(GetOrgAccounts)),
rest.Post("/api/orgs/:orgId/ledgers", auth.RequireAuth(PostAccount)),
rest.Put("/api/orgs/:orgId/ledgers/:accountId", auth.RequireAuth(PutAccount)),
rest.Delete("/api/orgs/:orgId/ledgers/:accountId", auth.RequireAuth(DeleteAccount)),
rest.Get("/api/orgs/:orgId/ledgers/:accountId/transactions", auth.RequireAuth(GetTransactionsByAccount)),
rest.Get("/api/orgs/:orgId/accounts", auth.RequireAuth(GetOrgAccounts)),
rest.Post("/api/orgs/:orgId/accounts", auth.RequireAuth(PostAccount)),
rest.Put("/api/orgs/:orgId/accounts/:accountId", auth.RequireAuth(PutAccount)),
rest.Delete("/api/orgs/:orgId/accounts/:accountId", auth.RequireAuth(DeleteAccount)),
rest.Get("/api/orgs/:orgId/accounts/:accountId/transactions", auth.RequireAuth(GetTransactionsByAccount)),
rest.Get("/api/orgs/:orgId/transactions", auth.RequireAuth(GetTransactionsByOrg)),
rest.Post("/api/orgs/:orgId/transactions", auth.RequireAuth(PostTransaction)),
rest.Put("/api/orgs/:orgId/transactions/:transactionId", auth.RequireAuth(PutTransaction)),
rest.Delete("/api/orgs/:orgId/transactions/:transactionId", auth.RequireAuth(DeleteTransaction)),
rest.Get("/api/orgs/:orgId/prices", auth.RequireAuth(GetPrices)),
rest.Post("/api/orgs/:orgId/prices", auth.RequireAuth(PostPrice)),
rest.Delete("/api/orgs/:orgId/prices/:priceId", auth.RequireAuth(DeletePrice)),
rest.Get("/ws", ws.Handler),
rest.Post("/api/sessions", auth.RequireAuth(PostSession)),
rest.Delete("/api/sessions/:sessionId", auth.RequireAuth(DeleteSession)),
rest.Get("/api/apikeys", auth.RequireAuth(GetApiKeys)),
rest.Post("/api/apikeys", auth.RequireAuth(PostApiKey)),
rest.Put("/api/apikeys/:apiKeyId", auth.RequireAuth(PutApiKey)),
rest.Delete("/api/apikeys/:apiKeyId", auth.RequireAuth(DeleteApiKey)),
rest.Get("/api/orgs/:orgId/invites", auth.RequireAuth(GetInvites)),
rest.Post("/api/orgs/:orgId/invites", auth.RequireAuth(PostInvite)),
rest.Put("/api/orgs/:orgId/invites/:inviteId", auth.RequireAuth(PutInvite)),
rest.Delete("/api/orgs/:orgId/invites/:inviteId", auth.RequireAuth(DeleteInvite)),
)
}

87
core/api/session.go Normal file
View File

@@ -0,0 +1,87 @@
package api
import (
"github.com/ant0ine/go-json-rest/rest"
"github.com/openaccounting/oa-server/core/model"
"github.com/openaccounting/oa-server/core/model/types"
"net/http"
)
/**
* @api {post} /sessions Create a new Session
* @apiVersion 1.0.0
* @apiName PostSession
* @apiGroup Session
*
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
* @apiHeader {String} Authorization HTTP Basic Auth
*
* @apiParam {String} id 32 character hex string
*
* @apiSuccess {String} id Id of the Session.
* @apiSuccess {Date} inserted Date Session was created
* @apiSuccess {Date} updated Date Last activity for the Session
* @apiSuccess {String} userId Id of the User
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "userId": "22222222222222222222222222222222"
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PostSession(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
session := &types.Session{}
err := r.DecodeJsonPayload(session)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.UserId = user.Id
err = model.Instance.CreateSession(session)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(session)
}
/**
* @api {delete} /sessions/:sessionId Log out of a Session
* @apiVersion 1.0.0
* @apiName DeleteSession
* @apiGroup Session
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func DeleteSession(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
sessionId := r.PathParam("sessionId")
err := model.Instance.DeleteSession(sessionId, user.Id)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

362
core/api/transaction.go Normal file
View File

@@ -0,0 +1,362 @@
package api
import (
"github.com/ant0ine/go-json-rest/rest"
"github.com/openaccounting/oa-server/core/model"
"github.com/openaccounting/oa-server/core/model/types"
"net/http"
)
/**
* @api {get} /orgs/:orgId/accounts/:accountId/transactions Get Transactions by Account Id
* @apiVersion 1.0.0
* @apiName GetAccountTransactions
* @apiGroup Transaction
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccess {String} id Id of the Transaction.
* @apiSuccess {String} orgId Id of the Org.
* @apiSuccess {String} userId Id of the User who created the Transaction.
* @apiSuccess {Date} date Date of the Transaction
* @apiSuccess {Date} inserted Date Transaction was created
* @apiSuccess {Date} updated Date Transaction was updated
* @apiSuccess {String} description Description of Transaction
* @apiSuccess {String} data Extra data field
* @apiSuccess {Object[]} splits Array of Transaction Splits
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* [
* {
* "id": "11111111111111111111111111111111",
* "orgId": "11111111111111111111111111111111",
* "userId": "11111111111111111111111111111111",
* "date": "2018-06-08T20:12:29.720Z",
* "inserted": "2018-06-08T20:12:29.720Z",
* "updated": "2018-06-08T20:12:29.720Z",
* "description": "Treat friend to lunch",
* "data:": "{\"key\": \"value\"}",
* "splits": [
* {
* "accountId": "11111111111111111111111111111111",
* "amount": -2000,
* "nativeAmount": -2000
* },
* {
* "accountId": "22222222222222222222222222222222",
* "amount": 1000,
* "nativeAmount": 1000
* },
* {
* "accountId": "33333333333333333333333333333333",
* "amount": 1000,
* "nativeAmount": 1000
* }
* ]
* }
* ]
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func GetTransactionsByAccount(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
accountId := r.PathParam("accountId")
queryOptions, err := types.QueryOptionsFromURLQuery(r.URL.Query())
if err != nil {
rest.Error(w, "invalid query options", 400)
return
}
sTxs, err := model.Instance.GetTransactionsByAccount(orgId, user.Id, accountId, queryOptions)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&sTxs)
}
/**
* @api {get} /orgs/:orgId/transactions Get Transactions by Org Id
* @apiVersion 1.0.0
* @apiName GetOrgTransactions
* @apiGroup Transaction
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccess {String} id Id of the Transaction.
* @apiSuccess {String} orgId Id of the Org.
* @apiSuccess {String} userId Id of the User who created the Transaction.
* @apiSuccess {Date} date Date of the Transaction
* @apiSuccess {Date} inserted Date Transaction was created
* @apiSuccess {Date} updated Date Transaction was updated
* @apiSuccess {String} description Description of Transaction
* @apiSuccess {String} data Extra data field
* @apiSuccess {Object[]} splits Array of Transaction Splits
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* [
* {
* "id": "11111111111111111111111111111111",
* "orgId": "11111111111111111111111111111111",
* "userId": "11111111111111111111111111111111",
* "date": "2018-06-08T20:12:29.720Z",
* "inserted": "2018-06-08T20:12:29.720Z",
* "updated": "2018-06-08T20:12:29.720Z",
* "description": "Treat friend to lunch",
* "data:": "{\"key\": \"value\"}",
* "splits": [
* {
* "accountId": "11111111111111111111111111111111",
* "amount": -2000,
* "nativeAmount": -2000
* },
* {
* "accountId": "22222222222222222222222222222222",
* "amount": 1000,
* "nativeAmount": 1000
* },
* {
* "accountId": "33333333333333333333333333333333",
* "amount": 1000,
* "nativeAmount": 1000
* }
* ]
* }
* ]
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func GetTransactionsByOrg(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
queryOptions, err := types.QueryOptionsFromURLQuery(r.URL.Query())
if err != nil {
rest.Error(w, "invalid query options", 400)
return
}
sTxs, err := model.Instance.GetTransactionsByOrg(orgId, user.Id, queryOptions)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(&sTxs)
}
/**
* @api {post} /orgs/:orgId/transactions Create a new Transaction
* @apiVersion 1.0.0
* @apiName PostTransaction
* @apiGroup Transaction
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} id Id 32 character hex string
* @apiParam {Date} date Date of the Transaction
* @apiParam {String} description Description of Transaction
* @apiParam {String} data Extra data field
* @apiParam {Object[]} splits Array of Transaction Splits. nativeAmounts must add up to 0.
* @apiParam {String} splits.accountId Id of Account
* @apiParam {Number} splits.amount Amount of split in Account currency
* @apiParam {Number} splits.nativeAmount Amount of split in Org currency
*
* @apiSuccess {String} id Id of the Transaction.
* @apiSuccess {String} orgId Id of the Org.
* @apiSuccess {String} userId Id of the User who created the Transaction.
* @apiSuccess {Date} date Date of the Transaction
* @apiSuccess {Date} inserted Date Transaction was created
* @apiSuccess {Date} updated Date Transaction was updated
* @apiSuccess {String} description Description of Transaction
* @apiSuccess {String} data Extra data field
* @apiSuccess {Object[]} splits Array of Transaction Splits
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "orgId": "11111111111111111111111111111111",
* "userId": "11111111111111111111111111111111",
* "date": "2018-06-08T20:12:29.720Z",
* "inserted": "2018-06-08T20:12:29.720Z",
* "updated": "2018-06-08T20:12:29.720Z",
* "description": "Treat friend to lunch",
* "data:": "{\"key\": \"value\"}",
* "splits": [
* {
* "accountId": "11111111111111111111111111111111",
* "amount": -2000,
* "nativeAmount": -2000
* },
* {
* "accountId": "22222222222222222222222222222222",
* "amount": 1000,
* "nativeAmount": 1000
* },
* {
* "accountId": "33333333333333333333333333333333",
* "amount": 1000,
* "nativeAmount": 1000
* }
* ]
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PostTransaction(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
sTx := types.Transaction{}
err := r.DecodeJsonPayload(&sTx)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sTx.OrgId = orgId
sTx.UserId = user.Id
err = model.Instance.CreateTransaction(&sTx)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(sTx)
}
/**
* @api {put} /orgs/:orgId/transactions/:transactionId Modify a Transaction
* @apiVersion 1.0.0
* @apiName PutTransaction
* @apiGroup Transaction
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} id 32 character hex string
* @apiParam {Date} date Date of the Transaction
* @apiParam {String} description Description of Transaction
* @apiParam {String} data Extra data field
* @apiParam {Object[]} splits Array of Transaction Splits. nativeAmounts must add up to 0.
* @apiParam {String} splits.accountId Id of Account
* @apiParam {Number} splits.amount Amount of split in Account currency
* @apiParam {Number} splits.nativeAmount Amount of split in Org currency
*
* @apiSuccess {String} id Id of the Transaction.
* @apiSuccess {String} orgId Id of the Org.
* @apiSuccess {String} userId Id of the User who created the Transaction.
* @apiSuccess {Date} date Date of the Transaction
* @apiSuccess {Date} inserted Date Transaction was created
* @apiSuccess {Date} updated Date Transaction was updated
* @apiSuccess {String} description Description of Transaction
* @apiSuccess {String} data Extra data field
* @apiSuccess {Object[]} splits Array of Transaction Splits
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "orgId": "11111111111111111111111111111111",
* "userId": "11111111111111111111111111111111",
* "date": "2018-06-08T20:12:29.720Z",
* "inserted": "2018-06-08T20:12:29.720Z",
* "updated": "2018-06-08T20:12:29.720Z",
* "description": "Treat friend to lunch",
* "data:": "{\"key\": \"value\"}",
* "splits": [
* {
* "accountId": "11111111111111111111111111111111",
* "amount": -2000,
* "nativeAmount": -2000
* },
* {
* "accountId": "22222222222222222222222222222222",
* "amount": 1000,
* "nativeAmount": 1000
* },
* {
* "accountId": "33333333333333333333333333333333",
* "amount": 1000,
* "nativeAmount": 1000
* }
* ]
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func PutTransaction(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
transactionId := r.PathParam("transactionId")
sTx := types.Transaction{}
err := r.DecodeJsonPayload(&sTx)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sTx.OrgId = orgId
sTx.UserId = user.Id
err = model.Instance.UpdateTransaction(transactionId, &sTx)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(sTx)
}
/**
* @api {delete} /orgs/:orgId/transactions/:transactionId Delete a Transaction
* @apiVersion 1.0.0
* @apiName DeleteTransaction
* @apiGroup Transaction
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func DeleteTransaction(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
orgId := r.PathParam("orgId")
transactionId := r.PathParam("transactionId")
err := model.Instance.DeleteTransaction(transactionId, user.Id, orgId)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

265
core/api/user.go Normal file
View File

@@ -0,0 +1,265 @@
package api
import (
"github.com/ant0ine/go-json-rest/rest"
"github.com/openaccounting/oa-server/core/model"
"github.com/openaccounting/oa-server/core/model/types"
"net/http"
)
type VerifyUserParams struct {
Code string `json:"code"`
}
type ConfirmResetPasswordParams struct {
Code string `json:"code"`
Password string `json:"password"`
}
type ResetPasswordParams struct {
Email string `json:"email"`
}
/**
* @api {get} /user Get Authenticated User
* @apiVersion 1.0.0
* @apiName GetUser
* @apiGroup User
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiSuccess {String} id Id of the User.
* @apiSuccess {Date} inserted Date User was created
* @apiSuccess {Date} updated Date User was updated
* @apiSuccess {String} firstName First name of the User.
* @apiSuccess {String} lastName Last name of the User.
* @apiSuccess {String} email Email of the User.
* @apiSuccess {Boolean} agreeToTerms Agree to terms
* @apiSuccess {Boolean} emailVerified True if email has been verified.
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "firstName": "John",
* "lastName": "Doe",
* "email": "johndoe@email.com",
* "agreeToTerms": true,
* "emailVerified": true
* }
*
* @apiUse NotAuthorizedError
* @apiUse InternalServerError
*/
func GetUser(w rest.ResponseWriter, r *rest.Request) {
user := r.Env["USER"].(*types.User)
w.WriteJson(&user)
}
/**
* @api {post} /users Create a new User
* @apiVersion 1.0.0
* @apiName PostUser
* @apiGroup User
*
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} id 32 character hex string
* @apiParam {String} firstName First name of the User.
* @apiParam {String} lastName Last name of the User.
* @apiParam {String} email Email of the User.
* @apiParam {String} password Password of the User.
* @apiParam {Boolean} agreeToTerms True if you agree to terms
*
* @apiSuccess {String} id Id of the User.
* @apiSuccess {Date} inserted Date User was created
* @apiSuccess {Date} updated Date User was updated
* @apiSuccess {String} firstName First name of the User.
* @apiSuccess {String} lastName Last name of the User.
* @apiSuccess {String} email Email of the User.
* @apiSuccess {Boolean} emailVerified True if email has been verified.
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "firstName": "John",
* "lastName": "Doe",
* "email": "johndoe@email.com",
* "agreeToTerms": true,
* "emailVerified": true
* }
*
* @apiUse InternalServerError
*/
func PostUser(w rest.ResponseWriter, r *rest.Request) {
user := &types.User{}
err := r.DecodeJsonPayload(user)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = model.Instance.CreateUser(user)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(user)
}
/**
* @api {put} /user Modify User
* @apiVersion 1.0.0
* @apiName PutUser
* @apiGroup User
*
* @apiHeader {String} Authorization HTTP Basic Auth
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} password New password
* @apiParam {String} code Password reset code. (Instead of Authorization header)
*
* @apiSuccess {String} id Id of the User.
* @apiSuccess {Date} inserted Date User was created
* @apiSuccess {Date} updated Date User was updated
* @apiSuccess {String} firstName First name of the User.
* @apiSuccess {String} lastName Last name of the User.
* @apiSuccess {String} email Email of the User.
* @apiSuccess {Boolean} emailVerified True if email has been verified.
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "id": "11111111111111111111111111111111",
* "inserted": "2018-09-11T18:05:04.420Z",
* "updated": "2018-09-11T18:05:04.420Z",
* "firstName": "John",
* "lastName": "Doe",
* "email": "johndoe@email.com",
* "agreeToTerms": true,
* "emailVerified": true
* }
*
* @apiUse InternalServerError
*/
func PutUser(w rest.ResponseWriter, r *rest.Request) {
if r.Env["USER"] == nil {
// password reset
params := &ConfirmResetPasswordParams{}
err := r.DecodeJsonPayload(params)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user, err := model.Instance.ConfirmResetPassword(params.Password, params.Code)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(user)
return
}
// Otherwise it's an authenticated PUT
user := r.Env["USER"].(*types.User)
newUser := &types.User{}
err := r.DecodeJsonPayload(newUser)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user.Password = newUser.Password
err = model.Instance.UpdateUser(user)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteJson(user)
}
/**
* @api {post} /user/verify Verify user email address
* @apiVersion 1.0.0
* @apiName VerifyUser
* @apiGroup User
*
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} code Email verification code
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
*
* @apiUse InternalServerError
*/
func VerifyUser(w rest.ResponseWriter, r *rest.Request) {
params := &VerifyUserParams{}
err := r.DecodeJsonPayload(params)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = model.Instance.VerifyUser(params.Code)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
/**
* @api {post} /user/reset-password Send reset password email
* @apiVersion 1.0.0
* @apiName ResetPassword
* @apiGroup User
*
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
*
* @apiParam {String} email Email address for user
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
*
* @apiUse InternalServerError
*/
func ResetPassword(w rest.ResponseWriter, r *rest.Request) {
params := &ResetPasswordParams{}
err := r.DecodeJsonPayload(params)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = model.Instance.ResetPassword(params.Email)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
return
}

48
core/api/version.go Normal file
View File

@@ -0,0 +1,48 @@
package api
import (
"github.com/Masterminds/semver"
"github.com/ant0ine/go-json-rest/rest"
"net/http"
)
type VersionMiddleware struct {
}
// MiddlewareFunc makes AuthMiddleware implement the Middleware interface.
func (mw *VersionMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc {
return func(writer rest.ResponseWriter, request *rest.Request) {
version := request.Header.Get("Accept-Version")
// Don't require version header for websockets
if request.URL.String() == "/ws" {
handler(writer, request)
return
}
if version == "" {
rest.Error(writer, "Accept-Version header required", http.StatusBadRequest)
return
}
constraint, err := semver.NewConstraint(version)
if err != nil {
rest.Error(writer, "Invalid version", http.StatusBadRequest)
}
serverVersion, _ := semver.NewVersion("1.0.0")
// Pre-release versions
compatVersion, _ := semver.NewVersion("0.1.8")
versionMatch := constraint.Check(serverVersion)
compatMatch := constraint.Check(compatVersion)
if versionMatch == false && compatMatch == false {
rest.Error(writer, "Invalid version", http.StatusBadRequest)
return
}
handler(writer, request)
}
}