Browse Source

Add API routing and login functions

master
Immanuel Onyeka 1 year ago
parent
commit
ee2358d259
4 changed files with 241 additions and 69 deletions
  1. +5
    -1
      go.mod
  2. +4
    -0
      go.sum
  3. +2
    -1
      migrations/0_29092022_create_main_tables.sql
  4. +230
    -67
      skouter.go

+ 5
- 1
go.mod View File

@@ -2,4 +2,8 @@ module example.com/m


go 1.19 go 1.19


require github.com/go-sql-driver/mysql v1.6.0
require (
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.0
github.com/go-sql-driver/mysql v1.6.0
github.com/golang-jwt/jwt/v4 v4.5.0
)

+ 4
- 0
go.sum View File

@@ -1,2 +1,6 @@
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.0 h1:DNrExYwvyyI404SxdUCCANAj9TwnGjRfa3cYFMNY1AU=
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.0/go.mod h1:SQq4xfIdvf6WYKSDxAJc+xOJdolt+/bc1jnQKMtPMvQ=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=

+ 2
- 1
migrations/0_29092022_create_main_tables.sql View File

@@ -108,12 +108,13 @@ CREATE TABLE mi (
five_year_total INT DEFAULT 0, five_year_total INT DEFAULT 0,
initial_premium INT DEFAULT 0, initial_premium INT DEFAULT 0,
initial_rate INT DEFAULT 0, initial_rate INT DEFAULT 0,
initial_amount INT DEFAULT 0,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
FOREIGN KEY (loan_id) REFERENCES loan(id) FOREIGN KEY (loan_id) REFERENCES loan(id)
); );


/* template = true fees are saved for users or branches. If template or default /* template = true fees are saved for users or branches. If template or default
* are true, estimate_id should be null.*/
* are true, estimate_id should be null. */
CREATE TABLE fee ( CREATE TABLE fee (
id INT AUTO_INCREMENT NOT NULL, id INT AUTO_INCREMENT NOT NULL,
loan_id INT, loan_id INT,


+ 230
- 67
skouter.go View File

@@ -10,11 +10,21 @@ import (
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"fmt" "fmt"
"encoding/json" "encoding/json"
// "io"
"strconv" "strconv"
"bytes" "bytes"
"time"
"errors"
"strings"
pdf "github.com/SebastiaanKlippert/go-wkhtmltopdf"
"github.com/golang-jwt/jwt/v4"
) )


type UserClaims struct {
Id int `json:"id"`
Role string `json:"role"`
Exp string `json:"exp"`
}

type Page struct { type Page struct {
tpl *template.Template tpl *template.Template
Title string Title string
@@ -75,16 +85,16 @@ type Loan struct {
} }


type MI struct { type MI struct {
Type string
Label string
Lender string
Rate float32
Premium float32
Upfront float32
FiveYearTotal float32
InitialAllInPremium float32
InitialAllInRate float32
InitialAmount float32
Type string
Label string
Lender string
Rate float32
Premium float32
Upfront float32
FiveYearTotal float32
InitialAllInPremium float32
InitialAllInRate float32
InitialAmount float32
} }


type Estimate struct { type Estimate struct {
@@ -118,6 +128,22 @@ var pages = map[string]Page {
"app": cache("app", "App"), "app": cache("app", "App"),
} }


var roles = map[string]int{
"guest": 1,
"employee": 2,
"admin": 3,
}

// Used to validate claim in JWT token body. Checks if user id is greater than
// zero and time format is valid
func (c UserClaims) Valid() error {
if c.Id < 1 { return errors.New("Invalid id") }
t, err := time.Parse(time.UnixDate, c.Exp)
if err != nil { return err }
if t.Before(time.Now()) { return errors.New("Token expired.") }
return err
}

func cache(name string, title string) Page { func cache(name string, title string) Page {
var p = []string{"master.tpl", paths[name]} var p = []string{"master.tpl", paths[name]}
tpl := template.Must(template.ParseFiles(p...)) tpl := template.Must(template.ParseFiles(p...))
@@ -482,6 +508,7 @@ func fetchMi(db *sql.DB, estimate *Estimate, pos int) []MI {


resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
var res map[string]interface{} var res map[string]interface{}
var result []MI


if resp.StatusCode != 200 { if resp.StatusCode != 200 {
log.Printf("the status: %v\nthe resp: %v\n the req: %v\n the body: %v\n", log.Printf("the status: %v\nthe resp: %v\n the req: %v\n the body: %v\n",
@@ -489,40 +516,116 @@ func fetchMi(db *sql.DB, estimate *Estimate, pos int) []MI {
} else { } else {
json.NewDecoder(resp.Body).Decode(&res) json.NewDecoder(resp.Body).Decode(&res)
// estimate.Loans[pos].Mi = res // estimate.Loans[pos].Mi = res
// Parse res into result here
}

return result
}

func login(w http.ResponseWriter, db *sql.DB, r *http.Request) {
var id int
var role string
var err error
r.ParseForm()

row := db.QueryRow(
`SELECT id, role FROM user WHERE email = ? AND password = sha2(?, 256)`,
r.PostFormValue("email"), r.PostFormValue("password"))

err = row.Scan(&id, &role)
if err != nil {
http.Error(w, "Invalid Credentials.", http.StatusUnauthorized)
return
} }


return estimate
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
UserClaims{ Id: id, Role: role,
Exp: time.Now().Add(time.Minute * 30).Format(time.UnixDate)})

tokenStr, err := token.SignedString([]byte(config["JWT_SECRET"]))
if err != nil {
log.Println("Token could not be signed: ", err, tokenStr)
http.Error(w, "Token generation error.", http.StatusInternalServerError)
return
}

cookie := http.Cookie{Name: "hound",
Value: tokenStr,
Path: "/",
Expires: time.Now().Add(time.Hour * 24)}
http.SetCookie(w, &cookie)
_, err = w.Write([]byte(tokenStr))
if err != nil {
http.Error(w,
"Could not complete token write.",
http.StatusInternalServerError)}
}

func getToken(w http.ResponseWriter, db *sql.DB, r *http.Request) {
claims, err := getClaims(r)
// Will verify existing signature and expiry time
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
UserClaims{ Id: claims.Id, Role: claims.Role,
Exp: time.Now().Add(time.Minute * 30).Format(time.UnixDate)})

tokenStr, err := token.SignedString([]byte(config["JWT_SECRET"]))
if err != nil {
log.Println("Token could not be signed: ", err, tokenStr)
http.Error(w, "Token generation error.", http.StatusInternalServerError)
return
}

cookie := http.Cookie{Name: "hound",
Value: tokenStr,
Path: "/",
Expires: time.Now().Add(time.Hour * 24)}
http.SetCookie(w, &cookie)
_, err = w.Write([]byte(tokenStr))
if err != nil {
http.Error(w,
"Could not complete token write.",
http.StatusInternalServerError)}
} }


func validateEstimate() { func validateEstimate() {
return return
} }


func route(w http.ResponseWriter, r *http.Request) {
var page Page
var args []string
p := r.URL.Path
func getClaims(r *http.Request) (UserClaims, error) {
claims := new(UserClaims)
_, tokenStr, found := strings.Cut(r.Header.Get("Authorization"), " ")


switch {
case r.Method == "GET" && match(p, "/", &args):
page = pages[ "home" ]
case match(p, "/terms", &args):
page = pages[ "terms" ]
case match(p, "/app", &args):
page = pages[ "app" ]
case match(p, "/assets", &args):
page = pages[ "app" ]
default:
http.NotFound(w, r)
return
}
if !found {
return *claims, errors.New("Token not found")
}


page.Render(w)
// Pull token payload into UserClaims
_, err := jwt.ParseWithClaims(tokenStr, claims,
func(token *jwt.Token) (any, error) {
return []byte(config["JWT_SECRET"]), nil
})

if err != nil {
return *claims, err
}

if err = claims.Valid(); err != nil {
return *claims, err
}

return *claims, nil
}

func guard(r *http.Request, required int) bool {
claims, err := getClaims(r)
if err != nil { return false }
if roles[claims.Role] < required { return false }

return true
} }


func api(w http.ResponseWriter, r *http.Request) { func api(w http.ResponseWriter, r *http.Request) {
var args []string var args []string
// var response string


p := r.URL.Path p := r.URL.Path
db, err := sql.Open("mysql", db, err := sql.Open("mysql",
@@ -534,54 +637,114 @@ func api(w http.ResponseWriter, r *http.Request) {


err = db.Ping() err = db.Ping()
if err != nil { if err != nil {
print("Bad database configuration: %v", err)
fmt.Println("Bad database configuration: %v\n", err)
panic(err) panic(err)
// maybe os.Exit(1) instead // maybe os.Exit(1) instead
} }

switch { switch {
case match(p, "/api/loans", &args):
resp, err := getLoanType(db, 0, 0, true)

if resp != nil {
json.NewEncoder(w).Encode(resp)
} else {
json.NewEncoder(w).Encode(err)
}

case match(p, "/api/fees", &args):
resp, err := getFeesTemp(db, 0)

if resp != nil {
json.NewEncoder(w).Encode(resp)
} else {
json.NewEncoder(w).Encode(err)
}
case match(p, "/api/login", &args) &&
r.Method == http.MethodPost:
login(w, db, r)
case match(p, "/api/token", &args) &&
r.Method == http.MethodGet && guard(r, 1):
getToken(w, db, r)
case match(p, "/api/users", &args) && // Array of all users
r.Method == http.MethodGet && guard(r, 2):
getUsers(w, db, r)
case match(p, "/api/user", &args) &&
r.Method == http.MethodGet, && guard(r, 1):
getUser(w, db, r)
case match(p, "/api/user", &args) &&
r.Method == http.MethodPost &&
guard(r, 3):
createUser(w, db, r)
case match(p, "/api/user", &args) &&
r.Method == http.MethodPatch &&
guard(r, 3): // For admin to modify any user
patchUser(w, db, r)
case match(p, "/api/user", &args) &&
r.Method == http.MethodPatch &&
guard(r, 2): // For employees to modify own accounts
patchSelf(w, db, r)
case match(p, "/api/user", &args) &&
r.Method == http.MethodDelete &&
guard(r, 3):
deleteUser(w, db, r)
case match(p, "/api/batch", &args) &&
r.Method == http.MethodGet &&
guard(r, 1):
getBatch(w, db, r)
case match(p, "/api/batch", &args) &&
r.Method == http.MethodPost &&
guard(r, 2):
openBatch(w, db, r)
case match(p, "/api/batch", &args) &&
r.Method == http.MethodPatch &&
guard(r, 2):
closeBatch(w, db, r)
case match(p, "/api/client", &args) &&
r.Method == http.MethodGet &&
guard(r, 1):
getClient(w, db, r)
case match(p, "/api/client", &args) &&
r.Method == http.MethodPost &&
guard(r, 2):
createClient(w, db, r)
case match(p, "/api/client", &args) &&
r.Method == http.MethodPatch &&
guard(r, 2):
patchClient(w, db, r)
case match(p, "/api/client", &args) &&
r.Method == http.MethodDelete &&
guard(r, 2):
deleteClient(w, db, r)
case match(p, "/api/ticket", &args) &&
r.Method == http.MethodPost &&
guard(r, 2):
openTicket(w, db, r)
case match(p, "/api/ticket", &args) &&
r.Method == http.MethodPatch &&
guard(r, 2):
closeTicket(w, db, r)
case match(p, "/api/ticket", &args) &&
r.Method == http.MethodDelete &&
guard(r, 2):
voidTicket(w, db, r)
case match(p, "/api/report/batch", &args) &&
r.Method == http.MethodGet &&
guard(r, 2):
reportBatch(w, db, r)
case match(p, "/api/report/summary", &args) &&
r.Method == http.MethodPost &&
guard(r, 2):
reportSummary(w, db, r)
}


case match(p, "/api/mi", &args):
var err error
est, err := getEstimate(db, 1)
if err != nil {
json.NewEncoder(w).Encode(err)
log.Println("error occured:", err)
break
}
db.Close()
}


json.NewEncoder(w).Encode(fetchMi(db, &est, 0).Loans[0].Mi)
func route(w http.ResponseWriter, r *http.Request) {
var page Page
var args []string
p := r.URL.Path


// if err != nil {
// json.NewEncoder(w).Encode(err)
// break
// } else {
// json.NewEncoder(w).Encode(resp)
// }
}
switch {
case r.Method == "GET" && match(p, "/", &args):
page = pages[ "home" ]
case match(p, "/terms", &args):
page = pages[ "terms" ]
case match(p, "/app", &args):
page = pages[ "app" ]
default:
http.NotFound(w, r)
return
}


page.Render(w)
} }


func main() { func main() {
files := http.FileServer(http.Dir("")) files := http.FileServer(http.Dir(""))


http.Handle("/assets/", files) http.Handle("/assets/", files)
http.HandleFunc("/api/", api) http.HandleFunc("/api/", api)


Loading…
Cancel
Save