소스 검색

Add API routing and login functions

master
Immanuel Onyeka 2 년 전
부모
커밋
ee2358d259
4개의 변경된 파일241개의 추가작업 그리고 69개의 파일을 삭제
  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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 Type string
Label string Label string
Lender string Lender string
Rate float32 Rate float32
Premium float32 Premium float32
Upfront float32 Upfront float32
FiveYearTotal float32 FiveYearTotal float32
InitialAllInPremium float32 InitialAllInPremium float32
InitialAllInRate float32 InitialAllInRate float32
InitialAmount 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) { func getClaims(r *http.Request) (UserClaims, error) {
var page Page claims := new(UserClaims)
var args []string _, tokenStr, found := strings.Cut(r.Header.Get("Authorization"), " ")
p := r.URL.Path


switch { if !found {
case r.Method == "GET" && match(p, "/", &args): return *claims, errors.New("Token not found")
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
}


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): case match(p, "/api/login", &args) &&
resp, err := getLoanType(db, 0, 0, true) r.Method == http.MethodPost:

login(w, db, r)
if resp != nil { case match(p, "/api/token", &args) &&
json.NewEncoder(w).Encode(resp) r.Method == http.MethodGet && guard(r, 1):
} else { getToken(w, db, r)
json.NewEncoder(w).Encode(err) 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/fees", &args): case match(p, "/api/user", &args) &&
resp, err := getFeesTemp(db, 0) r.Method == http.MethodGet, && guard(r, 1):

getUser(w, db, r)
if resp != nil { case match(p, "/api/user", &args) &&
json.NewEncoder(w).Encode(resp) r.Method == http.MethodPost &&
} else { guard(r, 3):
json.NewEncoder(w).Encode(err) 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): db.Close()
var err error }
est, err := getEstimate(db, 1)
if err != nil {
json.NewEncoder(w).Encode(err)
log.Println("error occured:", err)
break
}


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 { switch {
// json.NewEncoder(w).Encode(err) case r.Method == "GET" && match(p, "/", &args):
// break page = pages[ "home" ]
// } else { case match(p, "/terms", &args):
// json.NewEncoder(w).Encode(resp) 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)


||||||
x
 
000:0
불러오는 중...
취소
저장