feat: add admin/auth api (#22)

* feat: add admin/auth api

- jwt cookie based auth
- /auth
- admin/*

* add: meta proxy

* fix: remove ussd account status from syncer

* add: cookie defaults and nuxt init check route

* add: pin and address handlers
This commit is contained in:
2022-06-02 11:24:58 +03:00
committed by GitHub
parent 861af1761d
commit 77f127e14a
14 changed files with 369 additions and 5 deletions

82
internal/admin/api.go Normal file
View File

@@ -0,0 +1,82 @@
package admin
import (
"net/http"
"github.com/golang-jwt/jwt"
"github.com/grassrootseconomics/cic-go/meta"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/labstack/echo/v4"
"github.com/nleof/goyesql"
)
type api struct {
db *pgxpool.Pool
q goyesql.Queries
m *meta.CicMeta
jwtKey []byte
}
func InitAdminApi(e *echo.Echo, db *pgxpool.Pool, queries goyesql.Queries, metaClient *meta.CicMeta, jwtKey string) {
api := newApi(db, queries, metaClient, jwtKey)
auth := e.Group(("/auth"))
g := e.Group("/admin")
auth.Use(api.dwCoreMiddleware)
auth.POST("/login", sendLoginJwtCookie)
auth.POST("/logout", sendLogoutCookie)
g.Use(api.dwCoreMiddleware)
g.Use(api.verifyAuthMiddleware)
g.GET("/check", isLoggedIn)
g.GET("/meta-proxy/:address", handleMetaProxy)
g.GET("/pin-status", handlePinStatus)
g.GET("/phone-2-address/:phone", handlePhone2Address)
g.GET("/address-2-phone/:address", handleAddress2Phone)
}
func newApi(db *pgxpool.Pool, queries goyesql.Queries, metaClient *meta.CicMeta, jwtKey string) *api {
return &api{
db: db,
q: queries,
m: metaClient,
jwtKey: []byte(jwtKey),
}
}
func (a *api) dwCoreMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Set("api", &api{
db: a.db,
q: a.q,
m: a.m,
jwtKey: a.jwtKey,
})
return next(c)
}
}
func (a *api) verifyAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cookie, err := c.Cookie("_ge_auth")
if err != nil {
return c.String(http.StatusForbidden, "auth cookie missing")
}
claims := &jwtClaims{}
token, err := jwt.ParseWithClaims(cookie.Value, claims, func(token *jwt.Token) (interface{}, error) {
return a.jwtKey, nil
})
if err != nil {
return c.String(http.StatusUnauthorized, "jwt validation failed")
}
if !token.Valid {
return c.String(http.StatusUnauthorized, "jwt invalid")
}
return next(c)
}
}

88
internal/admin/auth.go Normal file
View File

@@ -0,0 +1,88 @@
package admin
import (
"context"
"net/http"
"time"
"github.com/golang-jwt/jwt"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
)
type staff struct {
Username string `json:"username"`
Password string `json:"password"`
}
type jwtClaims struct {
Username string `json:"username"`
jwt.StandardClaims
}
func isLoggedIn(c echo.Context) error {
return c.String(http.StatusOK, "ok")
}
func sendLoginJwtCookie(c echo.Context) error {
var (
api = c.Get("api").(*api)
passwordHash string
)
u := new(staff)
if err := c.Bind(u); err != nil {
return err
}
if err := api.db.QueryRow(context.Background(), api.q["get-password-hash"], u.Username).Scan(&passwordHash); err != nil {
return c.String(http.StatusNotFound, "username not found")
}
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(u.Password)); err != nil {
return c.String(http.StatusForbidden, "login failed")
}
expiration := time.Now().Add(24 * 7 * time.Hour)
claims := &jwtClaims{
Username: u.Username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expiration.Unix(),
},
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := jwtToken.SignedString(api.jwtKey)
if err != nil {
return err
}
cookie := cookieDefaults()
cookie.Value = tokenString
cookie.Expires = expiration
c.SetCookie(cookie)
return c.String(http.StatusOK, "login successful")
}
func sendLogoutCookie(c echo.Context) error {
cookie := cookieDefaults()
cookie.MaxAge = -1
c.SetCookie(cookie)
return c.String(http.StatusOK, "logout successful")
}
func cookieDefaults() *http.Cookie {
cookie := new(http.Cookie)
cookie.Name = "_ge_auth"
cookie.Path = "/"
cookie.SameSite = 3
cookie.HttpOnly = true
cookie.Secure = false
return cookie
}

33
internal/admin/pins.go Normal file
View File

@@ -0,0 +1,33 @@
package admin
import (
"context"
"net/http"
"github.com/georgysavva/scany/pgxscan"
"github.com/labstack/echo/v4"
)
type pinStatusResponse struct {
PhoneNumber string `db:"phone_number" json:"phone_number"`
FailedPinAttempts int `db:"failed_pin_attempts" json:"failed_pin_attempts"`
AccountStatus string `db:"account_status" json:"account_status"`
}
func handlePinStatus(c echo.Context) error {
var (
api = c.Get("api").(*api)
res []pinStatusResponse
)
rows, err := api.db.Query(context.Background(), api.q["pin-status"])
if err != nil {
return err
}
if err := pgxscan.ScanAll(&res, rows); err != nil {
return err
}
return c.JSON(http.StatusOK, res)
}

95
internal/admin/user.go Normal file
View File

@@ -0,0 +1,95 @@
package admin
import (
"bytes"
"cic-dw/pkg/address"
"context"
"encoding/base64"
"net/http"
"strings"
"github.com/grassrootseconomics/cic-go/meta"
"github.com/labstack/echo/v4"
"github.com/mapaiva/vcard-go"
)
type metaRes struct {
Person meta.PersonResponse `json:"person"`
Name string `json:"name"`
}
func handlePhone2Address(c echo.Context) error {
var (
api = c.Get("api").(*api)
phone = c.Param("phone")
address string
)
if err := api.db.QueryRow(context.Background(), api.q["phone-2-address"], phone).Scan(&address); err != nil {
return c.String(http.StatusNotFound, "phone not found")
}
return c.String(http.StatusOK, address)
}
func handleAddress2Phone(c echo.Context) error {
var (
api = c.Get("api").(*api)
phone string
)
sarafuAddress, err := address.SarafuAddress(c.Param("address"))
if err != nil {
return err
}
if err := api.db.QueryRow(context.Background(), api.q["address-2-phone"], sarafuAddress).Scan(&phone); err != nil {
return c.String(http.StatusNotFound, "address not found")
}
return c.String(http.StatusOK, phone)
}
func handleMetaProxy(c echo.Context) error {
var (
api = c.Get("api").(*api)
address = c.Param("address")
)
person, err := api.m.GetPersonMetadata(address)
if err != nil {
if strings.Contains(err.Error(), "404") {
return c.String(http.StatusNotFound, "meta resource not found")
} else {
return err
}
}
vCard, err := parseVCard(person.VCard)
if err != nil {
return err
}
return c.JSON(http.StatusOK, &metaRes{
Person: person,
Name: vCard.FormattedName,
})
}
func parseVCard(encodedVCard string) (vcard.VCard, error) {
data, err := base64.StdEncoding.DecodeString(encodedVCard)
if err != nil {
return vcard.VCard{}, err
}
reader := bytes.NewReader(data)
vCards, err := vcard.GetVCardsByReader(reader)
if err != nil {
return vcard.VCard{}, nil
}
return vCards[0], nil
}