0.0.4 rate limiting, captcha, more better auth, bug fixes

main
gabe farrell 3 years ago
parent fdbf7217e9
commit a3e56fa753

@ -6,6 +6,8 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/url"
"os"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -13,11 +15,15 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive" "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/options"
"go.mongodb.org/mongo-driver/mongo/readpref" "go.mongodb.org/mongo-driver/mongo/readpref"
"golang.org/x/crypto/bcrypt" "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 { type Credentials struct {
Username string `json:"username" bson:"username"` Username string `json:"username" bson:"username"`
Password string `json:"password" bson:"password"` Password string `json:"password" bson:"password"`
@ -29,6 +35,13 @@ type Login struct {
RememberMe bool `json:"remember"` 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 { type Session struct {
Session string `json:"session" bson:"session"` Session string `json:"session" bson:"session"`
} }
@ -61,13 +74,55 @@ func createAccount(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
DB = openDB() DB = openDB()
} }
userCollection := DB.Database("Users").Collection("Users") 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 contains POST credentials
var v Login var v CreateAccount
err = json.NewDecoder(r.Body).Decode(&v) 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}}
if err != nil || len(v.Password) < 8 || len(v.Password) > 255 || !UsernameRegex.MatchString(v.Username) { 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) w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "{\"error\":\"there was a problem with your request. Please try again with different values\"}") fmt.Fprint(w, "{\"error\":\"there was a problem with your request. Please try again with different values\"}")
return return
@ -148,7 +203,12 @@ func login(w http.ResponseWriter, r *http.Request) {
DB = openDB() DB = openDB()
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
userCollection := DB.Database("Users").Collection("Users") 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 // decode POST into v struct
var v Login var v Login
@ -234,7 +294,12 @@ func loginBySession(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
DB = openDB() DB = openDB()
} }
userCollection := DB.Database("Users").Collection("Users") 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 id Session
var account ReturnedAccount var account ReturnedAccount
@ -263,7 +328,12 @@ func logout(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
DB = openDB() DB = openDB()
} }
userCollection := DB.Database("Users").Collection("Users") 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 var v Credentials

@ -4,10 +4,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"net/http" "net/http"
"os"
"strings" "strings"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive" "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/options"
"go.mongodb.org/mongo-driver/mongo/readpref" "go.mongodb.org/mongo-driver/mongo/readpref"
) )
@ -23,7 +25,12 @@ func chatColor(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
DB = openDB() DB = openDB()
} }
userCollection := DB.Database("Users").Collection("Users") var userCollection *mongo.Collection
if os.Getenv("ENVIRONMENT") == "production" {
userCollection = DB.Database("Users").Collection("Users")
} else {
userCollection = DB.Database("Development").Collection("Users")
}
// decode PUT into v struct // decode PUT into v struct
var v ColorRequest var v ColorRequest

@ -47,6 +47,14 @@ type Client struct {
send chan []byte send chan []byte
username string username string
auth bool
}
type Auth struct {
Type string `json:"type"`
Username string `json:"username"`
Key string `json:"key"`
} }
// readPump pumps messages from the websocket connection to the hub. // readPump pumps messages from the websocket connection to the hub.
@ -70,19 +78,29 @@ func (c *Client) readPump() {
} }
break break
} }
var v WsBetMessage var v Auth
err = json.Unmarshal(message, &v) err = json.Unmarshal(message, &v)
if err == nil { if err == nil {
if v.Type == "bind" { // check for authorization messages
if v.Type == "auth" {
// bind the WS client to the username (useful)
c.username = strings.ToLower(v.Username) c.username = strings.ToLower(v.Username)
// if auth key matches session, they are authorized to bet/chat etc
if DBGetUserByUsername(c.username).Session == v.Key {
c.auth = true
}
// if the user has bet (and then reloaded the page) send them their state
if c.hub.allUsers[c.username] != "" { if c.hub.allUsers[c.username] != "" {
c.send <- []byte("{\"type\":\"hasbet\",\"value\":true,\"bet\":\"" + c.hub.allUsers[c.username] + "\"}") c.send <- []byte("{\"type\":\"state\",\"value\":true,\"bet\":\"" + c.hub.allUsers[c.username] + "\"}")
} }
continue continue
} }
} }
// only authorized clients are allowed to push to the Hub
if c.auth {
c.hub.broadcast <- message c.hub.broadcast <- message
} }
}
} }
// writePump pumps messages from the hub to the websocket connection. // writePump pumps messages from the hub to the websocket connection.

@ -24,8 +24,7 @@ func (h *Hub) runGameClock() {
msg = "{\"type\":\"flip\",\"value\":\"tails\"}" msg = "{\"type\":\"flip\",\"value\":\"tails\"}"
} }
h.broadcast <- []byte(msg) h.broadcast <- []byte(msg)
time.Sleep(4 * time.Second) time.Sleep(5 * time.Second)
h.broadcastPoolUpdate() h.broadcastPoolUpdate()
time.Sleep(1 * time.Second)
} }
} }

34
db.go

@ -7,7 +7,6 @@ import (
"os" "os"
"time" "time"
"github.com/joho/godotenv"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
@ -15,26 +14,21 @@ import (
"go.mongodb.org/mongo-driver/mongo/readpref" "go.mongodb.org/mongo-driver/mongo/readpref"
) )
var DB = openDB() var DB *mongo.Client
func openDB() *mongo.Client { func openDB() *mongo.Client {
err := godotenv.Load()
if err != nil {
log.Fatal("Unable to load database. Shutting down...")
}
dbUsername := os.Getenv("DB_USERNAME") dbUsername := os.Getenv("DB_USERNAME")
dbPassword := os.Getenv("DB_PASSWORD") dbPassword := os.Getenv("DB_PASSWORD")
serverAPIOptions := options.ServerAPI(options.ServerAPIVersion1) serverAPIOptions := options.ServerAPI(options.ServerAPIVersion1)
clientOptions := options.Client(). clientOptions := options.Client().
ApplyURI("mongodb+srv://" + dbUsername + ":" + dbPassword + "@cluster0.tqrat.mongodb.net/myFirstDatabase?retryWrites=true&w=majority"). ApplyURI("mongodb+srv://" + dbUsername + ":" + dbPassword + "@cluster0.tqrat.mongodb.net/?retryWrites=true&w=majority").
SetServerAPIOptions(serverAPIOptions) SetServerAPIOptions(serverAPIOptions)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
client, err := mongo.Connect(ctx, clientOptions) client, err := mongo.Connect(ctx, clientOptions)
err = client.Ping(ctx, readpref.Primary())
if err != nil { if err != nil {
log.Fatal("DB Error: " + err.Error()) log.Fatal(err)
} }
return client return client
} }
@ -44,7 +38,13 @@ func DBSubtractPoints(user string, p int) error {
if err != nil { if err != nil {
DB = openDB() DB = openDB()
} }
userCollection := DB.Database("Users").Collection("Users") var userCollection *mongo.Collection
if os.Getenv("ENVIRONMENT") == "production" {
userCollection = DB.Database("Users").Collection("Users")
} else {
userCollection = DB.Database("Development").Collection("Users")
}
var v ExistingAccount var v ExistingAccount
err = userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "username", Value: user}}).Decode(&v) err = userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "username", Value: user}}).Decode(&v)
@ -67,7 +67,12 @@ func DBAddPoints(user string, p int) error {
if err != nil { if err != nil {
DB = openDB() DB = openDB()
} }
userCollection := DB.Database("Users").Collection("Users") var userCollection *mongo.Collection
if os.Getenv("ENVIRONMENT") == "production" {
userCollection = DB.Database("Users").Collection("Users")
} else {
userCollection = DB.Database("Development").Collection("Users")
}
var v ExistingAccount var v ExistingAccount
err = userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "username", Value: user}}).Decode(&v) err = userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "username", Value: user}}).Decode(&v)
@ -92,7 +97,12 @@ func DBGetUserByUsername(user string) ExistingAccount {
if err != nil { if err != nil {
DB = openDB() DB = openDB()
} }
userCollection := DB.Database("Users").Collection("Users") var userCollection *mongo.Collection
if os.Getenv("ENVIRONMENT") == "production" {
userCollection = DB.Database("Users").Collection("Users")
} else {
userCollection = DB.Database("Development").Collection("Users")
}
var v ExistingAccount var v ExistingAccount
err = userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "username", Value: user}}).Decode(&v) err = userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "username", Value: user}}).Decode(&v)

@ -1,18 +1,19 @@
{ {
"name": "massflip", "name": "massflip",
"version": "0.0.3", "version": "0.0.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "massflip", "name": "massflip",
"version": "0.0.3", "version": "0.0.4",
"dependencies": { "dependencies": {
"core-js": "^3.8.3", "core-js": "^3.8.3",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"pinia": "^2.0.0-rc.10", "pinia": "^2.0.0-rc.10",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-gtag": "^2.0.1" "vue-gtag": "^2.0.1",
"vue-recaptcha-v3": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.16", "@babel/core": "^7.12.16",
@ -9264,6 +9265,11 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/recaptcha-v3": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/recaptcha-v3/-/recaptcha-v3-1.10.0.tgz",
"integrity": "sha512-aGTxYSk3FFNKnXeKDbLpgRDRyIHRZNBF5HyaXXAN1Aj4TSyyZvmoAn9CylvpqLV3pYpIQavwc+2rzhNFn5SsLQ=="
},
"node_modules/regenerate": { "node_modules/regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -10788,6 +10794,17 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/vue-recaptcha-v3": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/vue-recaptcha-v3/-/vue-recaptcha-v3-2.0.1.tgz",
"integrity": "sha512-isEDtOfHU4wWRrZZuxciAELtQmPOeEEdicPNa0f1AOyLPy3sCcBEcpFt+FOcO3RQv5unJ3Yn5NlsWtXv9rXqjg==",
"dependencies": {
"recaptcha-v3": "^1.8.0"
},
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/vue-style-loader": { "node_modules/vue-style-loader": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
@ -18387,6 +18404,11 @@
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
} }
}, },
"recaptcha-v3": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/recaptcha-v3/-/recaptcha-v3-1.10.0.tgz",
"integrity": "sha512-aGTxYSk3FFNKnXeKDbLpgRDRyIHRZNBF5HyaXXAN1Aj4TSyyZvmoAn9CylvpqLV3pYpIQavwc+2rzhNFn5SsLQ=="
},
"regenerate": { "regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -19558,6 +19580,14 @@
} }
} }
}, },
"vue-recaptcha-v3": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/vue-recaptcha-v3/-/vue-recaptcha-v3-2.0.1.tgz",
"integrity": "sha512-isEDtOfHU4wWRrZZuxciAELtQmPOeEEdicPNa0f1AOyLPy3sCcBEcpFt+FOcO3RQv5unJ3Yn5NlsWtXv9rXqjg==",
"requires": {
"recaptcha-v3": "^1.8.0"
}
},
"vue-style-loader": { "vue-style-loader": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",

@ -1,6 +1,6 @@
{ {
"name": "massflip", "name": "massflip",
"version": "0.0.2", "version": "0.0.4",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@ -12,7 +12,8 @@
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"pinia": "^2.0.0-rc.10", "pinia": "^2.0.0-rc.10",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-gtag": "^2.0.1" "vue-gtag": "^2.0.1",
"vue-recaptcha-v3": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.16", "@babel/core": "^7.12.16",

@ -28,7 +28,7 @@ onMounted(() => {
let id = jar["session"] let id = jar["session"]
// handle logged in user // handle logged in user
let req = new XMLHttpRequest let req = new XMLHttpRequest
req.open("POST", "/api/login/bysession") req.open('POST', '/api/login/bysession')
req.send(JSON.stringify({ req.send(JSON.stringify({
session: id session: id
})) }))
@ -36,14 +36,16 @@ onMounted(() => {
if (req.readyState == XMLHttpRequest.DONE) { if (req.readyState == XMLHttpRequest.DONE) {
let usr = JSON.parse(req.responseText) let usr = JSON.parse(req.responseText)
if ("error" in usr) { if ("error" in usr) {
document.cookie = "session=; Max-Age=-99999999" document.cookie = 'session=; Max-Age=-99999999'
console.log(usr["error"]) console.log(usr['error'])
return return
} }
userStore().updateUser(usr) userStore().updateUser(usr)
let jar = cookiesToObj(cookieStr)
let msg = JSON.stringify({ let msg = JSON.stringify({
type: "bind", type: "auth",
username: usr.username username: usr.username,
key: jar['session']
}) })
WSSend(msg) WSSend(msg)
} }

@ -69,6 +69,7 @@ onMounted(() => {
if (isNaN(hP)) { if (isNaN(hP)) {
headsPercent.value = 0 headsPercent.value = 0
headsStyle['--p'] = 50 headsStyle['--p'] = 50
} else { } else {
headsPercent.value = hP headsPercent.value = hP
headsStyle['--p'] = headsPercent.value headsStyle['--p'] = headsPercent.value
@ -83,7 +84,9 @@ onMounted(() => {
tailsPool.value = wsMsg.tailspool tailsPool.value = wsMsg.tailspool
} }
WS.addEventListener("message", function (evt) { WS.addEventListener("message", function (evt) {
let wsMsg = JSON.parse(evt.data) let MSGs = evt.data.split('\n')
MSGs.forEach((i) => {
let wsMsg = JSON.parse(i)
if (wsMsg.type == "pool") { if (wsMsg.type == "pool") {
updatePool(wsMsg) updatePool(wsMsg)
} else if (wsMsg.type == "win") { } else if (wsMsg.type == "win") {
@ -98,12 +101,14 @@ onMounted(() => {
clock.value = wsMsg.value clock.value = wsMsg.value
until.value = '' until.value = ''
userStore().setBet("") userStore().setBet("")
} else if (wsMsg.type == "hasbet") { } else if (wsMsg.type == "state") {
if (wsMsg.value == true) { if (wsMsg.value == true) {
userStore().bet = wsMsg.bet userStore().bet = wsMsg.bet
} }
} }
}) })
})
}) })
</script> </script>
@ -192,6 +197,9 @@ onMounted(() => {
background:var(--c); background:var(--c);
transform:rotate(calc(var(--p)*3.6deg - 90deg)) translate(calc(var(--w)/2 - 50%)); transform:rotate(calc(var(--p)*3.6deg - 90deg)) translate(calc(var(--w)/2 - 50%));
} }
@-moz-keyframes p{
from{--p:0}
}
@keyframes p{ @keyframes p{
from{--p:0} from{--p:0}
} }

@ -22,10 +22,14 @@ function submitBet(HorT){
if (userStore().bet != '') { if (userStore().bet != '') {
return return
} }
if (bet.value <= 0) { if (bet.value <= 0 || isNaN(bet.value)) {
hasError.value = true hasError.value = true
error.value = "Error: bet must be greater than 0" error.value = "Error: bet must be greater than 0"
return return
} else if (bet.value % 1 != 0) {
hasError.value = true
error.value = "Error: bet must be a whole number"
return
} }
if (userStore().points - bet.value < 0) { if (userStore().points - bet.value < 0) {
hasError.value = true hasError.value = true

@ -31,10 +31,11 @@ const ChatColors = {
green: "limegreen", green: "limegreen",
yellow: "gold", yellow: "gold",
cyan: "cyan", cyan: "cyan",
red: "firebrick", red: "crimson",
pink: "fuchsia", pink: "fuchsia",
violet: "violet", violet: "violet",
orange: "orange", orange: "orange",
blue: "cornflowerblue"
} }
const _CHAT_MAX_HISTORY = 75; const _CHAT_MAX_HISTORY = 75;
@ -49,7 +50,9 @@ onMounted(() => {
} }
} }
WS.addEventListener("message", function (evt) { WS.addEventListener("message", function (evt) {
let wsMsg = JSON.parse(evt.data) let MSGs = evt.data.split('\n')
MSGs.forEach((i) => {
let wsMsg = JSON.parse(i)
if (wsMsg.type == "chat") { if (wsMsg.type == "chat") {
chatQueue.enqueue(wsMsg) chatQueue.enqueue(wsMsg)
if (chatQueue.length >= _CHAT_MAX_HISTORY) { if (chatQueue.length >= _CHAT_MAX_HISTORY) {
@ -72,6 +75,7 @@ onMounted(() => {
} }
} }
}) })
})
}) })
function sendChat() { function sendChat() {

@ -16,8 +16,9 @@
</div> </div>
<div id="noAcc" @click="toggleCreate">Don't have an account?</div> <div id="noAcc" @click="toggleCreate">Don't have an account?</div>
</div> </div>
<button class="submit" v-show="!tCreate" @click="login">Login</button> <button class="submit" v-if="!tCreate" @click="login">Login</button>
<button class="submit" v-show="tCreate" @click="createAccount">Create</button>
<button class="submit" v-if="tCreate" @click="createAccount">Create</button>
</form> </form>
<p id="serverResponse"> {{ serverResponse }}</p> <p id="serverResponse"> {{ serverResponse }}</p>
</div> </div>
@ -26,6 +27,8 @@
<script setup> <script setup>
import { defineEmits, ref, reactive } from 'vue' import { defineEmits, ref, reactive } from 'vue'
import { useReCaptcha } from 'vue-recaptcha-v3'
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha()
defineEmits(['display']) defineEmits(['display'])
const form = reactive({ const form = reactive({
username: '', username: '',
@ -39,6 +42,11 @@ const pHasError = ref(false)
const serverResponse = ref('') const serverResponse = ref('')
function toggleCreate() { function toggleCreate() {
tCreate.value = !tCreate.value tCreate.value = !tCreate.value
if (tCreate.value) {
document.getElementById("noAcc").innerText = "Already have an account?"
} else {
document.getElementById("noAcc").innerText = "Don't have an account?"
}
} }
function loginFieldsReady() { function loginFieldsReady() {
let ret = true let ret = true
@ -73,13 +81,17 @@ async function createAccount(e) {
serverResponse.value = "passwords do not match" serverResponse.value = "passwords do not match"
return return
} }
serverResponse.value = ""
await recaptchaLoaded()
const token = await executeRecaptcha('login')
let req = new XMLHttpRequest() let req = new XMLHttpRequest()
req.open("POST", "/api/createaccount") req.open("POST", "/api/createaccount")
req.withCredentials = true req.withCredentials = true
req.send(JSON.stringify({ req.send(JSON.stringify({
username: form.username, username: form.username,
password: form.password, password: form.password,
remember: form.remember remember: form.remember,
token: token
})) }))
req.onreadystatechange = () => { req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE) { if (req.readyState == XMLHttpRequest.DONE) {
@ -104,7 +116,7 @@ function login(e) {
req.send(JSON.stringify({ req.send(JSON.stringify({
username: form.username, username: form.username,
password: form.password, password: form.password,
remember: form.remember remember: form.remember,
})) }))
req.onreadystatechange = () => { req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE) { if (req.readyState == XMLHttpRequest.DONE) {

@ -1,5 +1,5 @@
<template> <template>
<p>Massflip v0.0.3 - Copyright 2022 MNRVA</p> <p>Massflip v0.0.4 - Copyright 2022 MNRVA</p>
</template> </template>
<script setup> <script setup>

@ -2,10 +2,11 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import VueGtag from "vue-gtag"; import VueGtag from "vue-gtag";
import { VueReCaptcha } from 'vue-recaptcha-v3';
// switch these in production // switch these in production
export const WS = new WebSocket("wss://" + "massflip.mnrva.dev" + "/ws") export const WS = new WebSocket("wss://" + "massflip.mnrva.dev" + "/ws")
//export const WS = new WebSocket("ws://" + "127.0.0.1:8000" + "/ws") //export const WS = new WebSocket("ws://" + "localhost:8000" + "/ws")
WS.onclose = function() { WS.onclose = function() {
alert("WebSocket connection closed.") alert("WebSocket connection closed.")
@ -65,5 +66,8 @@ const pinia = createPinia()
var app = createApp(App) var app = createApp(App)
app.use(pinia) app.use(pinia)
app.use(VueGtag, {config: { id: "G-C3WQH98SZB" }}) app.use(VueGtag, {config: { id: "G-C3WQH98SZB" }})
app.use(VueReCaptcha, { siteKey: '6LeDtKUgAAAAAH0OVNYPyxE8-k9EtjeSDW5jamle' }) // prod
//app.use(VueReCaptcha, { siteKey: '6LfD6qUgAAAAAHCKSiEW1fuyuCJiZrAPya26Ro8Z' }) // dev
app.mount('#app') app.mount('#app')

@ -6,11 +6,11 @@ module.exports = defineConfig({
module.exports = { module.exports = {
devServer: { devServer: {
host: "localhost", host: "localhost",
port: 8000,
proxy: { proxy: {
"/": { "/": {
target: "http://localhost:8000", target: "http://localhost:8000",
secure: false, secure: false
ws: true
} }
} }
} }

@ -6,20 +6,24 @@ require github.com/go-chi/chi/v5 v5.0.7 // direct
require github.com/gorilla/websocket v1.5.0 // direct require github.com/gorilla/websocket v1.5.0 // direct
require (
github.com/google/uuid v1.3.0
github.com/joho/godotenv v1.4.0
go.mongodb.org/mongo-driver v1.8.4
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
)
require ( require (
github.com/go-chi/cors v1.2.0 // indirect github.com/go-chi/cors v1.2.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect github.com/go-stack/stack v1.8.0 // indirect
github.com/golang/snappy v0.0.1 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/klauspost/compress v1.13.6 // indirect github.com/klauspost/compress v1.13.6 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.0.2 // indirect github.com/xdg-go/scram v1.0.2 // indirect
github.com/xdg-go/stringprep v1.0.2 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.mongodb.org/mongo-driver v1.8.4 // indirect
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
golang.org/x/text v0.3.5 // indirect golang.org/x/text v0.3.5 // indirect
) )

@ -51,6 +51,8 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

@ -0,0 +1,80 @@
package main
import (
"net"
"net/http"
"sync"
"golang.org/x/time/rate"
)
// IP limiter code taken from
// https://medium.com/@pliutau/rate-limiting-http-requests-in-go-based-on-ip-address-4e66d1bea4cf
var limiter = NewIPRateLimiter(1, 10)
// IPRateLimiter
type IPRateLimiter struct {
ips map[string]*rate.Limiter
mu *sync.RWMutex
r rate.Limit
b int
}
// NewIPRateLimiter
func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
i := &IPRateLimiter{
ips: make(map[string]*rate.Limiter),
mu: &sync.RWMutex{},
r: r,
b: b,
}
return i
}
// AddIP creates a new rate limiter and adds it to the ips map,
// using the IP address as the key
func (i *IPRateLimiter) AddIP(ip string) *rate.Limiter {
i.mu.Lock()
defer i.mu.Unlock()
limiter := rate.NewLimiter(i.r, i.b)
i.ips[ip] = limiter
return limiter
}
// GetLimiter returns the rate limiter for the provided IP address if it exists.
// Otherwise calls AddIP to add IP address to the map
func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
i.mu.Lock()
limiter, exists := i.ips[ip]
if !exists {
i.mu.Unlock()
return i.AddIP(ip)
}
i.mu.Unlock()
return limiter
}
func limitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
limiter := limiter.GetLimiter(ip)
if !limiter.Allow() {
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}

@ -7,16 +7,27 @@ import (
"net/http" "net/http"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/joho/godotenv"
) )
/* /*
* TODO: 0.0.4
Later: - added rate limiting
- user pages - added captcha for account creation
- figure out an actual goal for the game - added WebSocket authentication
* - fixed BetInput bugs (NaN, decimals)
- seperated development and production environment
- frontend bug fixes
*/ */
func init() {
err := godotenv.Load()
if err != nil {
fmt.Println("* No .env file found")
}
DB = openDB()
}
func main() { func main() {
// prepare router // prepare router
@ -30,6 +41,8 @@ func main() {
// disconnect to DB on application exit // disconnect to DB on application exit
defer DB.Disconnect(context.Background()) defer DB.Disconnect(context.Background())
// rate limiting middleware
r.Use(limitMiddleware)
// handlers // handlers
r.Handle("/*", http.FileServer(http.Dir("./frontend/dist"))) r.Handle("/*", http.FileServer(http.Dir("./frontend/dist")))
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) { r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
@ -42,6 +55,6 @@ func main() {
r.Put("/api/chatcolor", chatColor) r.Put("/api/chatcolor", chatColor)
// run server // run server
fmt.Println("* Listening on localhost:8000") log.Println("* Listening on localhost:8000")
log.Fatal(http.ListenAndServe(":8000", r)) log.Fatal(http.ListenAndServe(":8000", r))
} }

Loading…
Cancel
Save