639 lines
14 KiB
Go
639 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"myapp/internal/cards"
|
|
"myapp/internal/cards/encryption"
|
|
"myapp/internal/models"
|
|
"myapp/internal/urlsigner"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/stripe/stripe-go/v79"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type stripePayload struct {
|
|
Currency string `json:"currency"`
|
|
Amount string `json:"amount"`
|
|
PaymentMethod string `json:"payment_method"`
|
|
Email string `json:"email"`
|
|
CardBrand string `json:"card_brand"`
|
|
ExpiryMonth int `json:"expiry_month"`
|
|
ExpiryYear int `json:"expiry_year"`
|
|
LastFour string `json:"last_four"`
|
|
Plan string `json:"plan"`
|
|
ProductID string `json:"product_id"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
}
|
|
|
|
type jsonResponse struct {
|
|
OK bool `json:"ok"`
|
|
Message string `json:"message,omitempty"`
|
|
Content string `json:"content,omitempty"`
|
|
ID int `json:"id,omitempty"`
|
|
}
|
|
|
|
func (app *application) GetPaymentIntent(w http.ResponseWriter, r *http.Request) {
|
|
var payload stripePayload
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&payload)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
return // TODO: return a valid json
|
|
}
|
|
|
|
amount, err := strconv.Atoi(payload.Amount)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
return // TODO: return a valid json
|
|
}
|
|
|
|
card := cards.Card{
|
|
Secret: app.config.stripe.secret,
|
|
Key: app.config.stripe.key,
|
|
Currency: payload.Currency,
|
|
}
|
|
|
|
pi, msg, err := card.Charge(payload.Currency, amount)
|
|
if err != nil {
|
|
j := jsonResponse{
|
|
OK: false,
|
|
Message: msg,
|
|
Content: "",
|
|
}
|
|
|
|
out, err := json.MarshalIndent(j, "", " ")
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(out)
|
|
return
|
|
}
|
|
|
|
out, err := json.MarshalIndent(pi, "", " ")
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
return // TODO: return a valid json
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(out)
|
|
}
|
|
|
|
func (app *application) GetWidgetByID(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
widgetID, _ := strconv.Atoi(id)
|
|
|
|
widget, err := app.DB.GetWidget(widgetID)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
}
|
|
out, err := json.MarshalIndent(widget, "", " ")
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(out)
|
|
}
|
|
|
|
func (app *application) CreateCustomerAndSubscribeToPlan(w http.ResponseWriter, r *http.Request) {
|
|
var data stripePayload
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
return
|
|
}
|
|
|
|
app.infoLog.Println(data.Email, data.LastFour, data.PaymentMethod, data.Plan)
|
|
|
|
card := cards.Card{
|
|
Secret: app.config.stripe.secret,
|
|
Key: app.config.stripe.key,
|
|
Currency: data.Currency,
|
|
}
|
|
|
|
okay := true
|
|
var subscription *stripe.Subscription
|
|
txnMsg := "Transaction successful"
|
|
|
|
stripeCustomer, msg, err := card.CreateCustomer(data.PaymentMethod, data.Email)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
okay = false
|
|
txnMsg = msg
|
|
}
|
|
|
|
if okay {
|
|
subscription, err = card.SubscribeToPlan(
|
|
stripeCustomer,
|
|
data.Plan,
|
|
data.Email,
|
|
data.LastFour,
|
|
"",
|
|
)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
okay = false
|
|
txnMsg = "Error subscribing customer"
|
|
}
|
|
app.infoLog.Println("subscription id is", subscription.ID)
|
|
}
|
|
|
|
if okay {
|
|
productID, _ := strconv.Atoi(data.ProductID)
|
|
customerID, err := app.SaveCustomer(data.FirstName, data.LastName, data.Email)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
return
|
|
}
|
|
|
|
// create a new txn
|
|
amount, _ := strconv.Atoi(data.Amount)
|
|
txn := models.Transaction{
|
|
Amount: amount,
|
|
Currency: "eur",
|
|
LastFour: data.LastFour,
|
|
ExpiryMonth: data.ExpiryMonth,
|
|
ExpiryYear: data.ExpiryYear,
|
|
TransactionStatusID: 2,
|
|
PaymentIntent: subscription.ID,
|
|
PaymentMethod: data.PaymentMethod,
|
|
}
|
|
txnID, err := app.SaveTransaction(txn)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
return
|
|
}
|
|
|
|
// create order
|
|
order := models.Order{
|
|
WidgetID: productID,
|
|
TransactionID: txnID,
|
|
CustomerID: customerID,
|
|
StatusID: 1,
|
|
Quantity: 1,
|
|
Amount: amount,
|
|
}
|
|
|
|
_, err = app.SaveOrder(order)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
resp := jsonResponse{
|
|
OK: okay,
|
|
Message: txnMsg,
|
|
}
|
|
|
|
out, err := json.MarshalIndent(resp, "", " ")
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(out)
|
|
}
|
|
|
|
func (app *application) SaveCustomer(firstName, lastName, email string) (int, error) {
|
|
customer := models.Customer{
|
|
FirstName: firstName,
|
|
LastName: lastName,
|
|
Email: email,
|
|
}
|
|
|
|
id, err := app.DB.InsertCustomer(customer)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (app *application) SaveTransaction(txn models.Transaction) (int, error) {
|
|
txnID, err := app.DB.InsertTransaction(txn)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
return 0, err
|
|
}
|
|
|
|
return txnID, nil
|
|
}
|
|
|
|
func (app *application) SaveOrder(order models.Order) (int, error) {
|
|
id, err := app.DB.InsertOrder(order)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
return 0, err
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (app *application) CreateAuthToken(w http.ResponseWriter, r *http.Request) {
|
|
var userInput struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
err := app.readJSON(w, r, &userInput)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
// get the user from the db by email, send error if invalid email
|
|
user, err := app.DB.GetUserByEmail(userInput.Email)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.invalidCredentials(w)
|
|
return
|
|
}
|
|
|
|
// validate the password, send error if invalid password
|
|
validPassword, err := app.passwordMatches(user.Password, userInput.Password)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.invalidCredentials(w)
|
|
return
|
|
}
|
|
|
|
if !validPassword {
|
|
app.invalidCredentials(w)
|
|
return
|
|
}
|
|
|
|
// generate the token
|
|
token, err := models.GenerateToken(user.ID, 24*time.Hour, models.ScopeAuthentication)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
// save to DB
|
|
err = app.DB.InsertToken(token, user)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
// send response
|
|
|
|
var payload struct {
|
|
OK bool `json:"ok"`
|
|
Message string `json:"message"`
|
|
Token *models.Token `json:"authentication_token"`
|
|
}
|
|
|
|
payload.OK = true
|
|
payload.Message = fmt.Sprintf("token for %s created", userInput.Email)
|
|
payload.Token = token
|
|
|
|
_ = app.writeJSON(w, http.StatusOK, payload)
|
|
}
|
|
|
|
func (app *application) authenticateToken(r *http.Request) (*models.User, error) {
|
|
authorizationHeader := r.Header.Get("Authorization")
|
|
if authorizationHeader == "" {
|
|
return nil, errors.New("no authorization header received")
|
|
}
|
|
|
|
headerParts := strings.Split(authorizationHeader, " ")
|
|
if len(headerParts) != 2 || headerParts[0] != "Bearer" {
|
|
return nil, errors.New("no authorization header received")
|
|
}
|
|
|
|
token := headerParts[1]
|
|
if len(token) != 26 {
|
|
return nil, errors.New("authentication token wrong size")
|
|
}
|
|
|
|
// get the user from the tokens table
|
|
user, err := app.DB.GetUserForToken(token)
|
|
if err != nil {
|
|
return nil, errors.New("no matching user found")
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (app *application) CheckAuthentication(w http.ResponseWriter, r *http.Request) {
|
|
// validate the token, and get associated user
|
|
user, err := app.authenticateToken(r)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.invalidCredentials(w)
|
|
return
|
|
}
|
|
|
|
// valid user
|
|
var payload jsonResponse
|
|
payload.OK = true
|
|
payload.Message = fmt.Sprintf("authenticated user %s", user.Email)
|
|
app.writeJSON(w, http.StatusOK, payload)
|
|
}
|
|
|
|
func (app *application) VirtualTerminalPaymentSucceeded(w http.ResponseWriter, r *http.Request) {
|
|
var txnData struct {
|
|
PaymentAmount int `json:"amount"`
|
|
PaymentCurrency string `json:"currency"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Email string `json:"email"`
|
|
PaymentIntent string `json:"payment_intent"`
|
|
PaymentMethod string `json:"payment_method"`
|
|
BankReturnCode string `json:"bank_return_code"`
|
|
ExpiryMonth int `json:"expiry_month"`
|
|
ExpiryYear int `json:"expiry_year"`
|
|
LastFour string `json:"last_four"`
|
|
}
|
|
|
|
err := app.readJSON(w, r, &txnData)
|
|
if err != nil {
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
card := cards.Card{
|
|
Secret: app.config.stripe.secret,
|
|
Key: app.config.stripe.key,
|
|
}
|
|
|
|
pi, err := card.RetrievePaymentIntent(txnData.PaymentIntent)
|
|
if err != nil {
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
pm, err := card.GetPaymentMethod(txnData.PaymentMethod)
|
|
if err != nil {
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
txnData.LastFour = pm.Card.Last4
|
|
txnData.ExpiryMonth = int(pm.Card.ExpMonth)
|
|
txnData.ExpiryYear = int(pm.Card.ExpYear)
|
|
|
|
txn := models.Transaction{
|
|
Amount: txnData.PaymentAmount,
|
|
Currency: txnData.PaymentCurrency,
|
|
LastFour: txnData.LastFour,
|
|
ExpiryMonth: txnData.ExpiryMonth,
|
|
ExpiryYear: txnData.ExpiryYear,
|
|
BankReturnCode: pi.LatestCharge.ID,
|
|
PaymentIntent: txnData.PaymentIntent,
|
|
PaymentMethod: txnData.PaymentMethod,
|
|
TransactionStatusID: 2,
|
|
}
|
|
|
|
_, err = app.SaveTransaction(txn)
|
|
if err != nil {
|
|
app.badRequest(w, r, err)
|
|
}
|
|
|
|
app.writeJSON(w, http.StatusOK, txn)
|
|
}
|
|
|
|
func (app *application) SendPasswordResetEmail(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
err := app.readJSON(w, r, &payload)
|
|
if err != nil {
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
// verify that email exists
|
|
_, err = app.DB.GetUserByEmail(payload.Email)
|
|
if err != nil {
|
|
resp := jsonResponse{
|
|
OK: false,
|
|
Message: "No matching email found on our system",
|
|
}
|
|
app.writeJSON(w, http.StatusAccepted, resp)
|
|
return
|
|
}
|
|
|
|
link := fmt.Sprintf("%s/reset-password?email=%s", app.config.frontend, payload.Email)
|
|
sign := urlsigner.Signer{
|
|
Secret: []byte(app.config.secretkey),
|
|
}
|
|
|
|
signedLink := sign.GenerateTokenFromString(link)
|
|
|
|
var data struct {
|
|
Link string
|
|
}
|
|
|
|
data.Link = signedLink
|
|
|
|
// send mail
|
|
err = app.SendMail(
|
|
"info@widgets.com",
|
|
payload.Email,
|
|
"Password Reset Request",
|
|
"password-reset",
|
|
data,
|
|
)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
resp := jsonResponse{
|
|
OK: true,
|
|
}
|
|
|
|
app.writeJSON(w, http.StatusCreated, resp)
|
|
}
|
|
|
|
func (app *application) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
err := app.readJSON(w, r, &payload)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
encryptor := encryption.Encryption{
|
|
Key: []byte(app.config.secretkey),
|
|
}
|
|
|
|
realEmail, err := encryptor.Decrypt(payload.Email)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
user, err := app.DB.GetUserByEmail(realEmail)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
newHash, err := bcrypt.GenerateFromPassword([]byte(payload.Password), 12)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
err = app.DB.UpdatePasswordForUser(user, string(newHash))
|
|
if err != nil {
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
resp := jsonResponse{
|
|
OK: true,
|
|
Message: "Password reset.",
|
|
}
|
|
app.writeJSON(w, http.StatusCreated, resp)
|
|
}
|
|
|
|
func (app *application) AllSales(w http.ResponseWriter, r *http.Request) {
|
|
allSales, err := app.DB.GetAllOrders(false)
|
|
if err != nil {
|
|
app.badRequest(w, r, err)
|
|
}
|
|
app.writeJSON(w, http.StatusOK, allSales)
|
|
}
|
|
|
|
func (app *application) AllSubscriptions(w http.ResponseWriter, r *http.Request) {
|
|
allSubscriptions, err := app.DB.GetAllOrders(true)
|
|
if err != nil {
|
|
app.badRequest(w, r, err)
|
|
}
|
|
app.writeJSON(w, http.StatusOK, allSubscriptions)
|
|
}
|
|
|
|
func (app *application) GetSale(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
orderID, _ := strconv.Atoi(id)
|
|
|
|
order, err := app.DB.GetOrderByID(orderID)
|
|
if err != nil {
|
|
app.badRequest(w, r, err)
|
|
}
|
|
app.writeJSON(w, http.StatusOK, order)
|
|
}
|
|
|
|
func (app *application) RefundCharge(w http.ResponseWriter, r *http.Request) {
|
|
var chargeToRefund struct {
|
|
ID int `json:"id"`
|
|
PaymentIntent string `json:"pi"`
|
|
Amount int `json:"amount"`
|
|
Currency string `json:"currency"`
|
|
}
|
|
|
|
err := app.readJSON(w, r, &chargeToRefund)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
// validate
|
|
|
|
card := cards.Card{
|
|
Secret: app.config.stripe.secret,
|
|
Key: app.config.stripe.key,
|
|
Currency: chargeToRefund.Currency,
|
|
}
|
|
|
|
err = card.Refund(chargeToRefund.PaymentIntent, chargeToRefund.Amount)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
// update status in DB
|
|
err = app.DB.UpdateOrderStatus(chargeToRefund.ID, 2)
|
|
if err != nil {
|
|
app.badRequest(
|
|
w,
|
|
r,
|
|
errors.New("the charge was refunded, but the database could not be updated"),
|
|
)
|
|
return
|
|
}
|
|
|
|
var resp jsonResponse
|
|
resp.OK = true
|
|
resp.Message = "Charge refunded"
|
|
|
|
app.writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (app *application) CancelSubscription(w http.ResponseWriter, r *http.Request) {
|
|
var subToCancel struct {
|
|
ID int `json:"id"`
|
|
PaymentIntent string `json:"pi"`
|
|
Currency string `json:"currency"`
|
|
}
|
|
|
|
err := app.readJSON(w, r, &subToCancel)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
// validate
|
|
|
|
card := cards.Card{
|
|
Secret: app.config.stripe.secret,
|
|
Key: app.config.stripe.key,
|
|
Currency: subToCancel.Currency,
|
|
}
|
|
|
|
err = card.CancelSubscription(subToCancel.PaymentIntent)
|
|
if err != nil {
|
|
app.errorLog.Println(err)
|
|
app.badRequest(w, r, err)
|
|
return
|
|
}
|
|
|
|
// update status in DB
|
|
err = app.DB.UpdateOrderStatus(subToCancel.ID, 3)
|
|
if err != nil {
|
|
app.badRequest(
|
|
w,
|
|
r,
|
|
errors.New("the subscription was refunded, but the database could not be updated"),
|
|
)
|
|
return
|
|
}
|
|
|
|
var resp jsonResponse
|
|
resp.OK = true
|
|
resp.Message = "Subscription canceled"
|
|
|
|
app.writeJSON(w, http.StatusOK, resp)
|
|
}
|