mirror of https://github.com/gabehf/massflip.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
9.6 KiB
347 lines
9.6 KiB
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
"go.mongodb.org/mongo-driver/mongo"
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
|
"go.mongodb.org/mongo-driver/mongo/readpref"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// Note: I would love to embed a lot of these structs to avoid
|
|
// duplicate fields, but it breaks json.(Un)marshal and I dont want to deal with that
|
|
|
|
type Credentials struct {
|
|
Username string `json:"username" bson:"username"`
|
|
Password string `json:"password" bson:"password"`
|
|
}
|
|
|
|
type Login struct {
|
|
Username string `json:"username" bson:"username"`
|
|
Password string `json:"password" bson:"password"`
|
|
RememberMe bool `json:"remember"`
|
|
}
|
|
|
|
type CreateAccount struct {
|
|
Username string `json:"username" bson:"username"`
|
|
Password string `json:"password" bson:"password"`
|
|
RememberMe bool `json:"remember"`
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
type Session struct {
|
|
Session string `json:"session" bson:"session"`
|
|
}
|
|
|
|
// ExistingAccount is a struct that mirrors how Users are stored in the Database
|
|
type ExistingAccount struct {
|
|
Username string `json:"username" bson:"username"`
|
|
Password string `json:"password" bson:"password"`
|
|
Case string `json:"case" bson:"case"`
|
|
Color string `json:"color" bson:"color"`
|
|
Points int `json:"points" bson:"points"`
|
|
Resets int `json:"resets" bson:"resets"`
|
|
Session string `json:"session" bson:"session"`
|
|
}
|
|
|
|
type ReturnedAccount struct {
|
|
Username string `json:"username" bson:"case"`
|
|
Color string `json:"color" bson:"color"`
|
|
Points int `json:"points" bson:"points"`
|
|
Resets int `json:"resets" bson:"resets"`
|
|
}
|
|
|
|
var (
|
|
UsernameRegex = regexp.MustCompile(`^[a-zA-Z0-9-_]{3,24}$`)
|
|
)
|
|
|
|
func createAccount(w http.ResponseWriter, r *http.Request) {
|
|
// prepare DB
|
|
err := DB.Ping(context.Background(), readpref.Primary())
|
|
if err != nil {
|
|
DB = openDB()
|
|
}
|
|
var userCollection *mongo.Collection
|
|
if os.Getenv("ENVIRONMENT") == "production" {
|
|
userCollection = DB.Database("Users").Collection("Users")
|
|
} else {
|
|
userCollection = DB.Database("Development").Collection("Users")
|
|
}
|
|
|
|
// var v contains POST credentials
|
|
var v CreateAccount
|
|
err = json.NewDecoder(r.Body).Decode(&v)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
log.Println("* Create Account Refused: Bad form data")
|
|
return
|
|
}
|
|
|
|
var verify struct {
|
|
Secret string
|
|
Response string
|
|
}
|
|
verify.Secret = os.Getenv("CAPTCHA_SECRET")
|
|
verify.Response = v.Token
|
|
|
|
//url := "https://www.google.com/recaptcha/api/siteverify?secret=" + url.QueryEscape(verify.Secret) + "?response=" + url.QueryEscape(verify.Response)
|
|
u := "https://www.google.com/recaptcha/api/siteverify"
|
|
d := url.Values{"secret": []string{verify.Secret}, "response": []string{verify.Response}}
|
|
|
|
resp, err := http.PostForm(u, d)
|
|
|
|
var captchaResponse struct {
|
|
Success bool `json:"success"`
|
|
Time primitive.Timestamp `json:"challenge_ts"`
|
|
Hostname string `json:"hostname"`
|
|
Errors []string `json:"error-codes"`
|
|
}
|
|
|
|
json.NewDecoder(resp.Body).Decode(&captchaResponse)
|
|
|
|
if !captchaResponse.Success {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
log.Print("* Create Account Refused: Unsuccessful reCaptcha challenge\nErrors: ")
|
|
for _, e := range captchaResponse.Errors {
|
|
fmt.Print(e + " ")
|
|
}
|
|
fmt.Print("\n")
|
|
return
|
|
}
|
|
|
|
if len(v.Password) < 8 || len(v.Password) > 255 || !UsernameRegex.MatchString(v.Username) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprint(w, "{\"error\":\"there was a problem with your request. Please try again with different values\"}")
|
|
return
|
|
}
|
|
|
|
// search if user with that username already exists
|
|
find := userCollection.FindOne(r.Context(), bson.D{primitive.E{Key: "username", Value: strings.ToLower(v.Username)}})
|
|
if find.Err() == nil {
|
|
w.WriteHeader(http.StatusConflict)
|
|
fmt.Fprint(w, "{\"error\":\"user already exists with that username\"}")
|
|
return
|
|
}
|
|
|
|
// create a new session for the new user
|
|
sessionID := uuid.NewString()
|
|
var session *http.Cookie
|
|
if v.RememberMe {
|
|
expire := time.Now().Add(30 * 24 * time.Hour)
|
|
session = &http.Cookie{
|
|
Name: "session",
|
|
Value: sessionID,
|
|
Path: "/",
|
|
Secure: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: 0,
|
|
Expires: expire,
|
|
}
|
|
} else {
|
|
session = &http.Cookie{
|
|
Name: "session",
|
|
Value: sessionID,
|
|
Path: "/",
|
|
Secure: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: 0,
|
|
}
|
|
}
|
|
http.SetCookie(w, session)
|
|
|
|
// hash and store the user's hashed password
|
|
hasedPass, err := bcrypt.GenerateFromPassword([]byte(v.Password), 8)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
fmt.Fprintf(w, "{\"error\":\"internal server error, please try again later\"}")
|
|
}
|
|
|
|
// add the new user to the database
|
|
var acc ExistingAccount
|
|
acc.Username = strings.ToLower(v.Username)
|
|
acc.Password = string(hasedPass)
|
|
acc.Case = v.Username
|
|
acc.Color = "white"
|
|
acc.Points = 100
|
|
acc.Resets = 0
|
|
acc.Session = sessionID
|
|
_, err = userCollection.InsertOne(r.Context(), acc)
|
|
if err != nil {
|
|
log.Println("* Error inserting new user")
|
|
}
|
|
|
|
// return the account information to the user
|
|
var ret ReturnedAccount
|
|
ret.Username = v.Username
|
|
ret.Color = "white"
|
|
ret.Points = 100
|
|
ret.Resets = 0
|
|
account, err := json.Marshal(ret)
|
|
if err != nil {
|
|
fmt.Println("* Error marshalling bson.D response")
|
|
}
|
|
fmt.Fprint(w, string(account))
|
|
}
|
|
|
|
func login(w http.ResponseWriter, r *http.Request) {
|
|
// prepare DB collection
|
|
err := DB.Ping(context.Background(), readpref.Primary())
|
|
if err != nil {
|
|
DB = openDB()
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
var userCollection *mongo.Collection
|
|
if os.Getenv("ENVIRONMENT") == "production" {
|
|
userCollection = DB.Database("Users").Collection("Users")
|
|
} else {
|
|
userCollection = DB.Database("Development").Collection("Users")
|
|
}
|
|
|
|
// decode POST into v struct
|
|
var v Login
|
|
json.NewDecoder(r.Body).Decode(&v)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprintf(w, "{\"error\":\"bad request. Try again later\"}")
|
|
return
|
|
}
|
|
|
|
// cmp struct will be compared with v to verify credentials
|
|
var cmp Login
|
|
|
|
found := userCollection.FindOne(r.Context(), bson.D{primitive.E{Key: "username", Value: strings.ToLower(v.Username)}})
|
|
if found.Err() != nil {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
fmt.Fprintf(w, "{\"error\":\"account with that username does not exist\"}")
|
|
return
|
|
}
|
|
err = found.Decode(&cmp)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
err = bcrypt.CompareHashAndPassword([]byte(cmp.Password), []byte(v.Password))
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
fmt.Fprintf(w, "{\"error\":\"invalid password\"}")
|
|
return
|
|
}
|
|
|
|
// prepare ReturnedAccount struct to be sent back to the client
|
|
var account ReturnedAccount
|
|
err = found.Decode(&account)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// set new session cookie for user, either persistent (remember me) or temporary
|
|
sessionID := uuid.NewString()
|
|
var session *http.Cookie
|
|
if v.RememberMe {
|
|
expire := time.Now().Add(30 * 24 * time.Hour)
|
|
session = &http.Cookie{
|
|
Name: "session",
|
|
Value: sessionID,
|
|
Path: "/",
|
|
Secure: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: 0,
|
|
Expires: expire,
|
|
}
|
|
} else {
|
|
session = &http.Cookie{
|
|
Name: "session",
|
|
Value: sessionID,
|
|
Path: "/",
|
|
Secure: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: 0,
|
|
}
|
|
}
|
|
http.SetCookie(w, session)
|
|
|
|
// update the new user session in the DB
|
|
filter := bson.D{primitive.E{Key: "username", Value: strings.ToLower(account.Username)}}
|
|
opts := options.Update().SetUpsert(true)
|
|
update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "session", Value: sessionID}}}}
|
|
userCollection.UpdateOne(context.TODO(), filter, update, opts)
|
|
|
|
acc, err := json.Marshal(account)
|
|
if err != nil {
|
|
fmt.Println("Error marshalling bson.D response")
|
|
}
|
|
fmt.Fprint(w, string(acc))
|
|
//fmt.Println("logged in user successfully")
|
|
}
|
|
|
|
func loginBySession(w http.ResponseWriter, r *http.Request) {
|
|
err := DB.Ping(context.Background(), readpref.Primary())
|
|
if err != nil {
|
|
DB = openDB()
|
|
}
|
|
var userCollection *mongo.Collection
|
|
if os.Getenv("ENVIRONMENT") == "production" {
|
|
userCollection = DB.Database("Users").Collection("Users")
|
|
} else {
|
|
userCollection = DB.Database("Development").Collection("Users")
|
|
}
|
|
|
|
var id Session
|
|
var account ReturnedAccount
|
|
|
|
json.NewDecoder(r.Body).Decode(&id)
|
|
|
|
filter := bson.D{primitive.E{Key: "session", Value: id.Session}}
|
|
find := userCollection.FindOne(r.Context(), filter)
|
|
if find.Err() != nil {
|
|
log.Println(find.Err())
|
|
fmt.Fprintf(w, "{\"error\":\"no user with given session id\"}")
|
|
return
|
|
}
|
|
err = find.Decode(&account)
|
|
if err != nil {
|
|
log.Println(err)
|
|
fmt.Fprintf(w, "{\"error\":\"cannot decode user bson from session\"}")
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(account)
|
|
}
|
|
|
|
func logout(w http.ResponseWriter, r *http.Request) {
|
|
err := DB.Ping(context.Background(), readpref.Primary())
|
|
if err != nil {
|
|
DB = openDB()
|
|
}
|
|
var userCollection *mongo.Collection
|
|
if os.Getenv("ENVIRONMENT") == "production" {
|
|
userCollection = DB.Database("Users").Collection("Users")
|
|
} else {
|
|
userCollection = DB.Database("Development").Collection("Users")
|
|
}
|
|
|
|
var v Credentials
|
|
|
|
json.NewDecoder(r.Body).Decode(&v)
|
|
|
|
filter := bson.D{primitive.E{Key: "username", Value: strings.ToLower(v.Username)}}
|
|
opts := options.Update().SetUpsert(true)
|
|
update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "session", Value: ""}}}}
|
|
userCollection.UpdateOne(context.TODO(), filter, update, opts)
|
|
}
|