Compare commits

...

17 Commits

Author SHA1 Message Date
  Tovi Jaeschke-Rogers cfeb008724 Merge pull request 'feature/add-vue-frontend' (#3) from feature/add-vue-frontend into develop 2 years ago
  Tovi Jaeschke-Rogers 71d71e41cb Update fake routes 2 years ago
  Tovi Jaeschke-Rogers b8a76f8a9f Add AdminUsersCreate 2 years ago
  Tovi Jaeschke-Rogers 39cbc5f476 Update vue project with new styles, add users views 2 years ago
  Tovi Jaeschke-Rogers 6425603d68 Add router authetication and redirection 2 years ago
  Tovi Jaeschke-Rogers e9e9ec1fa2 Merge branch 'develop' into feature/add-vue-frontend 2 years ago
  Tovi Jaeschke-Rogers 766cf317f8 Merge pull request 'feature/add-users' (#2) from feature/add-users into develop 2 years ago
  Tovi Jaeschke-Rogers d2eb1c218c Add tests for logout and UpdatePassword 2 years ago
  Tovi Jaeschke-Rogers d584d40a52 Add authentication to all required endpoints 2 years ago
  Tovi Jaeschke-Rogers 946c4913fa Add Login and Logout routes 2 years ago
  Tovi Jaeschke-Rogers 4cb80bbb3a Add user_id to post model 2 years ago
  Tovi Jaeschke-Rogers fe4b31be06 Add createUser and getUsers API endpoints 2 years ago
  Tovi Jaeschke-Rogers 83690f3036 Embed vue.js build directory in golang 2 years ago
  Tovi Jaeschke-Rogers 68b64be7af Merge pull request 'Add post image API cruds' (#1) from feature/post-fk-cruds into develop 2 years ago
  Tovi Jaeschke-Rogers e6be47aaa5 Change PostImages upload limit to bitwise operation 2 years ago
  Tovi Jaeschke-Rogers e83a3b0f19 Add routes for creating and deleting post_images 2 years ago
  Tovi Jaeschke-Rogers 1f997971a4 Simplify and clean up Posts_test 2 years ago
65 changed files with 24645 additions and 191 deletions
Split View
  1. +2
    -0
      .gitignore
  2. +79
    -0
      Api/Auth/Login.go
  3. +111
    -0
      Api/Auth/Login_test.go
  4. +34
    -0
      Api/Auth/Logout.go
  5. +90
    -0
      Api/Auth/Logout_test.go
  6. +36
    -0
      Api/Auth/Me.go
  7. +22
    -0
      Api/Auth/Passwords.go
  8. +79
    -0
      Api/Auth/Session.go
  9. +53
    -0
      Api/Auth/UpdatePassword.go
  10. +100
    -0
      Api/Auth/UpdatePassword_test.go
  11. +76
    -0
      Api/JsonSerialization/DeserializeUserJson.go
  12. +37
    -16
      Api/JsonSerialization/VerifyJson.go
  13. +127
    -0
      Api/PostImages.go
  14. +258
    -0
      Api/PostImages_test.go
  15. +40
    -56
      Api/Posts.go
  16. +118
    -80
      Api/Posts_test.go
  17. +28
    -11
      Api/Routes.go
  18. +247
    -0
      Api/Users.go
  19. +372
    -0
      Api/Users_test.go
  20. +37
    -13
      Database/Init.go
  21. +33
    -0
      Database/PostImages.go
  22. +6
    -0
      Database/Posts.go
  23. +8
    -0
      Database/Seeder/Seed.go
  24. +97
    -0
      Database/Seeder/UserSeeder.go
  25. +139
    -0
      Database/Users.go
  26. +13
    -0
      Frontend/GetFrontendAssets_dev.go
  27. +26
    -0
      Frontend/GetFrontendAssets_prod.go
  28. +51
    -0
      Frontend/Routes.go
  29. +0
    -0
      Frontend/public/images/.gitkeep
  30. +23
    -0
      Frontend/vue/.gitignore
  31. +24
    -0
      Frontend/vue/README.md
  32. +5
    -0
      Frontend/vue/babel.config.js
  33. +19
    -0
      Frontend/vue/jsconfig.json
  34. +20536
    -0
      Frontend/vue/package-lock.json
  35. +56
    -0
      Frontend/vue/package.json
  36. BIN
      Frontend/vue/public/favicon.ico
  37. +17
    -0
      Frontend/vue/public/index.html
  38. +12
    -0
      Frontend/vue/src/App.vue
  39. +116
    -0
      Frontend/vue/src/assets/css/admin.css
  40. BIN
      Frontend/vue/src/assets/logo.png
  41. +58
    -0
      Frontend/vue/src/components/HelloWorld.vue
  42. +105
    -0
      Frontend/vue/src/components/admin/AdminLogin.vue
  43. +89
    -0
      Frontend/vue/src/components/admin/AdminNavbar.vue
  44. +166
    -0
      Frontend/vue/src/components/admin/AdminSignup.vue
  45. +165
    -0
      Frontend/vue/src/components/admin/users/AdminUsersCreate.vue
  46. +252
    -0
      Frontend/vue/src/components/admin/users/AdminUsersForm.vue
  47. +168
    -0
      Frontend/vue/src/components/admin/users/AdminUsersList.vue
  48. +37
    -0
      Frontend/vue/src/main.js
  49. +79
    -0
      Frontend/vue/src/router/index.js
  50. +28
    -0
      Frontend/vue/src/store/admin/index.js
  51. +29
    -0
      Frontend/vue/src/utils/http/index.js
  52. +4
    -0
      Frontend/vue/vue.config.js
  53. +12
    -0
      Makefile
  54. +19
    -10
      Models/Posts.go
  55. +26
    -0
      Models/Users.go
  56. +10
    -0
      Util/EmailValidation.go
  57. +67
    -0
      Util/Files.go
  58. +51
    -0
      Util/PostHelper.go
  59. +50
    -0
      Util/PostImageHelper.go
  60. +1
    -1
      Util/ReturnJson.go
  61. +21
    -0
      Util/Strings.go
  62. +51
    -0
      Util/UserHelper.go
  63. +2
    -0
      go.mod
  64. +5
    -0
      go.sum
  65. +23
    -4
      main.go

+ 2
- 0
.gitignore View File

@ -0,0 +1,2 @@
/Frontend/public/images/*
/Frontend/vue/node_modules

+ 79
- 0
Api/Auth/Login.go View File

@ -0,0 +1,79 @@
package Auth
import (
"encoding/json"
"log"
"net/http"
"time"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util"
"github.com/gofrs/uuid"
)
type Credentials struct {
Email string `json:"email"`
Password string `json:"password"`
}
func Login(w http.ResponseWriter, r *http.Request) {
var (
creds Credentials
userData Models.User
sessionToken uuid.UUID
expiresAt time.Time
returnJson []byte
err error
)
err = json.NewDecoder(r.Body).Decode(&creds)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
userData, err = Database.GetUserByEmail(creds.Email)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
if !CheckPasswordHash(creds.Password, userData.Password) {
w.WriteHeader(http.StatusUnauthorized)
return
}
sessionToken, err = uuid.NewV4()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
expiresAt = time.Now().Add(1 * time.Hour)
Sessions[sessionToken.String()] = Session{
UserID: userData.ID.String(),
Email: userData.Email,
Expiry: expiresAt,
}
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: sessionToken.String(),
Expires: expiresAt,
})
userData.Password = ""
returnJson, err = json.MarshalIndent(userData, "", " ")
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}

+ 111
- 0
Api/Auth/Login_test.go View File

@ -0,0 +1,111 @@
package Auth
import (
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"os"
"path"
"runtime"
"strings"
"testing"
"time"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"github.com/gorilla/mux"
)
var (
r *mux.Router
letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
)
func init() {
// Fix working directory for tests
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
Database.InitTest()
r = mux.NewRouter()
}
func randString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
func createTestUser(random bool) (Models.User, error) {
now := time.Now()
email := "email@email.com"
if random {
email = fmt.Sprintf("%s@email.com", randString(16))
}
password, err := HashPassword("password")
if err != nil {
return Models.User{}, err
}
userData := Models.User{
Email: email,
Password: password,
LastLogin: &now,
FirstName: "Hugh",
LastName: "Mann",
}
err = Database.CreateUser(&userData)
return userData, err
}
func Test_Login(t *testing.T) {
t.Log("Testing Login...")
r.HandleFunc("/admin/login", Login).Methods("POST")
ts := httptest.NewServer(r)
defer ts.Close()
userData, err := createTestUser(true)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
postJson := `
{
"email": "%s",
"password": "password"
}
`
postJson = fmt.Sprintf(postJson, userData.Email)
res, err := http.Post(ts.URL+"/admin/login", "application/json", strings.NewReader(postJson))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
return
}
if len(res.Cookies()) != 1 {
t.Errorf("Expected cookies len 1, recieved %d", len(res.Cookies()))
return
}
}

+ 34
- 0
Api/Auth/Logout.go View File

@ -0,0 +1,34 @@
package Auth
import (
"net/http"
"time"
)
func Logout(w http.ResponseWriter, r *http.Request) {
var (
c *http.Cookie
sessionToken string
err error
)
c, err = r.Cookie("session_token")
if err != nil {
if err == http.ErrNoCookie {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
sessionToken = c.Value
delete(Sessions, sessionToken)
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: "",
Expires: time.Now(),
})
}

+ 90
- 0
Api/Auth/Logout_test.go View File

@ -0,0 +1,90 @@
package Auth
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"runtime"
"strings"
"testing"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"github.com/gorilla/mux"
)
func init() {
// Fix working directory for tests
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
Database.InitTest()
r = mux.NewRouter()
}
func Test_Logout(t *testing.T) {
t.Log("Testing Logout...")
r.HandleFunc("/admin/login", Logout).Methods("POST")
r.HandleFunc("/admin/logout", Logout).Methods("GET")
ts := httptest.NewServer(r)
defer ts.Close()
userData, err := createTestUser(true)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
postJson := `
{
"email": "%s",
"password": "password"
}
`
postJson = fmt.Sprintf(postJson, userData.Email)
res, err := http.Post(ts.URL+"/admin/login", "application/json", strings.NewReader(postJson))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
return
}
if len(res.Cookies()) != 1 {
t.Errorf("Expected cookies len 1, recieved %d", len(res.Cookies()))
return
}
req, err := http.NewRequest("GET", ts.URL+"/admin/logout", nil)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
req.AddCookie(res.Cookies()[0])
res, err = http.DefaultClient.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
return
}
}

+ 36
- 0
Api/Auth/Me.go View File

@ -0,0 +1,36 @@
package Auth
import (
"encoding/json"
"log"
"net/http"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util"
)
func Me(w http.ResponseWriter, r *http.Request) {
var (
userData Models.User
returnJson []byte
err error
)
userData, err = CheckCookieCurrentUser(w, r)
if err != nil {
Util.JsonReturn(w, 401, "NO ERROR")
return
}
returnJson, err = json.MarshalIndent(userData, "", " ")
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}

+ 22
- 0
Api/Auth/Passwords.go View File

@ -0,0 +1,22 @@
package Auth
import (
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
var (
bytes []byte
err error
)
bytes, err = bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
var (
err error
)
err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

+ 79
- 0
Api/Auth/Session.go View File

@ -0,0 +1,79 @@
package Auth
import (
"errors"
"net/http"
"time"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
)
var (
Sessions = map[string]Session{}
)
type Session struct {
UserID string
Email string
Expiry time.Time
}
func (s Session) IsExpired() bool {
return s.Expiry.Before(time.Now())
}
func CheckCookie(r *http.Request) (Session, error) {
var (
c *http.Cookie
sessionToken string
userSession Session
exists bool
err error
)
c, err = r.Cookie("session_token")
if err != nil {
return userSession, err
}
sessionToken = c.Value
// We then get the session from our session map
userSession, exists = Sessions[sessionToken]
if !exists {
return userSession, errors.New("Cookie not found")
}
// If the session is present, but has expired, we can delete the session, and return
// an unauthorized status
if userSession.IsExpired() {
delete(Sessions, sessionToken)
return userSession, errors.New("Cookie expired")
}
return userSession, nil
}
func CheckCookieCurrentUser(w http.ResponseWriter, r *http.Request) (Models.User, error) {
var (
userSession Session
userData Models.User
err error
)
userSession, err = CheckCookie(r)
if err != nil {
return userData, err
}
userData, err = Database.GetUserById(userSession.UserID)
if err != nil {
return userData, err
}
if userData.ID.String() != userSession.UserID {
return userData, errors.New("Is not current user")
}
return userData, nil
}

+ 53
- 0
Api/Auth/UpdatePassword.go View File

@ -0,0 +1,53 @@
package Auth
import (
"encoding/json"
"net/http"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
)
type ChangePassword struct {
Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"`
}
func UpdatePassword(w http.ResponseWriter, r *http.Request) {
var (
changePasswd ChangePassword
userData Models.User
err error
)
userData, err = CheckCookieCurrentUser(w, r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
err = json.NewDecoder(r.Body).Decode(&changePasswd)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if changePasswd.Password != changePasswd.ConfirmPassword {
w.WriteHeader(http.StatusBadRequest)
return
}
userData.Password, err = HashPassword(changePasswd.Password)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = Database.UpdateUser(userData.ID.String(), &userData)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

+ 100
- 0
Api/Auth/UpdatePassword_test.go View File

@ -0,0 +1,100 @@
package Auth
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"runtime"
"strings"
"testing"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"github.com/gorilla/mux"
)
func init() {
// Fix working directory for tests
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
Database.InitTest()
r = mux.NewRouter()
}
func Test_UpdatePassword(t *testing.T) {
t.Log("Testing UpdatePassword...")
r.HandleFunc("/admin/login", Logout).Methods("POST")
r.HandleFunc("/admin/user/{userID}/update-password", UpdatePassword).Methods("PUT")
ts := httptest.NewServer(r)
defer ts.Close()
userData, err := createTestUser(true)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
postJson := `
{
"email": "%s",
"password": "password"
}
`
postJson = fmt.Sprintf(postJson, userData.Email)
res, err := http.Post(ts.URL+"/admin/login", "application/json", strings.NewReader(postJson))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
return
}
if len(res.Cookies()) != 1 {
t.Errorf("Expected cookies len 1, recieved %d", len(res.Cookies()))
return
}
postJson = `
{
"password": "new_password",
"confirm_password": "new_password"
}
`
req, err := http.NewRequest("PUT", fmt.Sprintf(
"%s/admin/user/%s/update-password",
ts.URL,
userData.ID,
), strings.NewReader(postJson))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
req.AddCookie(res.Cookies()[0])
res, err = http.DefaultClient.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
return
}
}

+ 76
- 0
Api/JsonSerialization/DeserializeUserJson.go View File

@ -0,0 +1,76 @@
package JsonSerialization
import (
"encoding/json"
"errors"
"fmt"
"strings"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
schema "github.com/Kangaroux/go-map-schema"
)
func DeserializeUser(data []byte, allowMissing []string, allowAllMissing bool) (Models.User, error) {
var (
userData Models.User = Models.User{}
jsonStructureTest map[string]interface{} = make(map[string]interface{})
jsonStructureTestResults *schema.CompareResults
field schema.FieldMissing
allowed string
missingFields []string
i int
err error
)
// Verify the JSON has the correct structure
json.Unmarshal(data, &jsonStructureTest)
jsonStructureTestResults, err = schema.CompareMapToStruct(
&userData,
jsonStructureTest,
&schema.CompareOpts{
ConvertibleFunc: CanConvert,
TypeNameFunc: schema.DetailedTypeName,
})
if err != nil {
return userData, err
}
if len(jsonStructureTestResults.MismatchedFields) > 0 {
return userData, errors.New(fmt.Sprintf(
"MismatchedFields found when deserializing data: %s",
jsonStructureTestResults.Errors().Error(),
))
}
// Remove allowed missing fields from MissingFields
for _, allowed = range allowMissing {
for i, field = range jsonStructureTestResults.MissingFields {
if allowed == field.String() {
jsonStructureTestResults.MissingFields = append(
jsonStructureTestResults.MissingFields[:i],
jsonStructureTestResults.MissingFields[i+1:]...,
)
}
}
}
if !allowAllMissing && len(jsonStructureTestResults.MissingFields) > 0 {
for _, field = range jsonStructureTestResults.MissingFields {
missingFields = append(missingFields, field.String())
}
return userData, errors.New(fmt.Sprintf(
"MissingFields found when deserializing data: %s",
strings.Join(missingFields, ", "),
))
}
// Deserialize the JSON into the struct
err = json.Unmarshal(data, &userData)
if err != nil {
return userData, err
}
return userData, err
}

+ 37
- 16
Api/JsonSerialization/VerifyJson.go View File

@ -7,7 +7,11 @@ import (
// isIntegerType returns whether the type is an integer and if it's unsigned.
// See: https://github.com/Kangaroux/go-map-schema/blob/master/schema.go#L328
func isIntegerType(t reflect.Type) (yes bool, unsigned bool) {
func isIntegerType(t reflect.Type) (bool, bool) {
var (
yes bool
unsigned bool
)
switch t.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
yes = true
@ -16,19 +20,22 @@ func isIntegerType(t reflect.Type) (yes bool, unsigned bool) {
unsigned = true
}
return
return yes, unsigned
}
// isFloatType returns true if the type is a floating point. Note that this doesn't
// care about the value -- unmarshaling the number "0" gives a float, not an int.
// See: https://github.com/Kangaroux/go-map-schema/blob/master/schema.go#L319
func isFloatType(t reflect.Type) (yes bool) {
func isFloatType(t reflect.Type) bool {
var (
yes bool
)
switch t.Kind() {
case reflect.Float32, reflect.Float64:
yes = true
}
return
return yes
}
// CanConvert returns whether value v is convertible to type t.
@ -38,9 +45,20 @@ func isFloatType(t reflect.Type) (yes bool) {
// Modified due to not handling slices (DefaultCanConvert fails on PhotoUrls and Tags)
// See: https://github.com/Kangaroux/go-map-schema/blob/master/schema.go#L191
func CanConvert(t reflect.Type, v reflect.Value) bool {
isPtr := t.Kind() == reflect.Ptr
isStruct := t.Kind() == reflect.Struct
dstType := t
var (
isPtr bool
isStruct bool
isArray bool
dstType reflect.Type
dstInt bool
unsigned bool
f float64
srcInt bool
)
isPtr = t.Kind() == reflect.Ptr
isStruct = t.Kind() == reflect.Struct
isArray = t.Kind() == reflect.Array
dstType = t
// Check if v is a nil value.
if !v.IsValid() || (v.CanAddr() && v.IsNil()) {
@ -58,6 +76,10 @@ func CanConvert(t reflect.Type, v reflect.Value) bool {
return v.Kind() == reflect.Map
}
if isArray {
return v.Kind() == reflect.String
}
if t.Kind() == reflect.Slice {
return v.Kind() == reflect.Slice
}
@ -67,20 +89,19 @@ func CanConvert(t reflect.Type, v reflect.Value) bool {
}
// Handle converting to an integer type.
if dstInt, unsigned := isIntegerType(dstType); dstInt {
dstInt, unsigned = isIntegerType(dstType)
if dstInt {
if isFloatType(v.Type()) {
f := v.Float()
f = v.Float()
if math.Trunc(f) != f {
return false
} else if unsigned && f < 0 {
return false
}
} else if srcInt, _ := isIntegerType(v.Type()); srcInt {
if unsigned && v.Int() < 0 {
if math.Trunc(f) != f || unsigned && f < 0 {
return false
}
}
srcInt, _ = isIntegerType(v.Type())
if srcInt && unsigned && v.Int() < 0 {
return false
}
}
return true


+ 127
- 0
Api/PostImages.go View File

@ -0,0 +1,127 @@
package Api
import (
"encoding/json"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util"
"github.com/gofrs/uuid"
)
func createPostImage(w http.ResponseWriter, r *http.Request) {
var (
postData Models.Post
postID string
postUUID uuid.UUID
postImage Models.PostImage
formData *multipart.Form
fileHeaders []*multipart.FileHeader
fileHeader *multipart.FileHeader
file multipart.File
fileBytes []byte
fileObject Util.FileObject
returnJson []byte
err error
)
postID, err = Util.GetPostId(r)
if err != nil {
log.Printf("Error encountered getting id\n")
Util.JsonReturn(w, 500, "An error occured")
return
}
postUUID = uuid.FromStringOrNil(postID)
err = r.ParseMultipartForm(20 << 20)
if err != nil {
log.Printf("Error encountered parsing multipart form: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
formData = r.MultipartForm
fileHeaders = formData.File["files"]
for _, fileHeader = range fileHeaders {
file, err = fileHeader.Open()
if err != nil {
log.Printf("Error encountered while post image upload: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
defer file.Close()
fileBytes, err = ioutil.ReadAll(file)
if err != nil {
log.Printf("Error encountered while post image upload: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
fileObject, err = Util.WriteFile(fileBytes, "image")
if err != nil {
log.Printf("Error encountered while post image upload: %s\n", err.Error())
Util.JsonReturn(w, 415, "Invalid filetype")
return
}
postImage = Models.PostImage{
PostID: postUUID,
Filepath: fileObject.Filepath,
Mimetype: fileObject.Mimetype,
Size: fileObject.Size,
}
err = Database.CreatePostImage(&postImage)
if err != nil {
log.Printf("Error encountered while creating post_image record: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
}
postData, err = Util.GetPostById(w, r)
if err != nil {
return
}
returnJson, err = json.MarshalIndent(postData, "", " ")
if err != nil {
Util.JsonReturn(w, 500, "An error occured")
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}
func deletePostImage(w http.ResponseWriter, r *http.Request) {
var (
postImageData Models.PostImage
err error
)
postImageData, err = Util.GetPostImageById(w, r)
if err != nil {
return
}
err = Database.DeletePostImage(&postImageData)
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
}

+ 258
- 0
Api/PostImages_test.go View File

@ -0,0 +1,258 @@
package Api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"testing"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"github.com/gorilla/mux"
)
func init() {
// Fix working directory for tests
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
Database.InitTest()
r = mux.NewRouter()
}
func get(url string) (resp *http.Response, err error) {
for i := 0; i < 5; i++ {
resp, err = http.Get(url)
if err == nil {
return resp, err
}
}
return resp, err
}
// Image generates a *os.File with a random image using the loremflickr.com service
func fakeImage(width, height int, categories []string, prefix string, categoriesStrict bool) *os.File {
url := "https://loremflickr.com"
switch prefix {
case "g":
url += "/g"
case "p":
url += "/p"
case "red":
url += "/red"
case "green":
url += "/green"
case "blue":
url += "/blue"
}
url += string('/') + strconv.Itoa(width) + string('/') + strconv.Itoa(height)
if len(categories) > 0 {
url += string('/')
for _, category := range categories {
url += category + string(',')
}
if categoriesStrict {
url += "/all"
}
}
resp, err := get(url)
defer resp.Body.Close()
if err != nil {
panic(err)
}
f, err := ioutil.TempFile(os.TempDir(), "loremflickr-img-*.jpg")
if err != nil {
panic(err)
}
io.Copy(f, resp.Body)
err = f.Close()
if err != nil {
panic(err)
}
return f
}
func Test_createPostImages(t *testing.T) {
t.Log("Testing createPostImages...")
r.HandleFunc("/post/{postID}/image", createPostImage).Methods("POST")
ts := httptest.NewServer(r)
defer ts.Close()
postData := Models.Post{
Title: "Test post",
Content: "Test content",
FrontPage: true,
Order: 1,
PostLinks: []Models.PostLink{
{
Type: "Facebook",
Link: "http://facebook.com/",
},
},
}
err := Database.CreatePost(&postData)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
image := fakeImage(100, 100, []string{}, "", false)
image, err = os.Open(image.Name())
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("files", filepath.Base(image.Name()))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
io.Copy(part, image)
writer.Close()
request, err := http.NewRequest(
"POST",
fmt.Sprintf(
"%s/post/%s/image",
ts.URL,
postData.ID,
),
body,
)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
request.Header.Add("Content-Type", writer.FormDataContentType())
client := &http.Client{}
res, err := client.Do(request)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
}
defer res.Body.Close()
updatePostData := new(Models.Post)
err = json.NewDecoder(res.Body).Decode(updatePostData)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
if len(updatePostData.PostImages) != 1 {
t.Errorf("Expected len(updatePostData.PostImages) == 1, recieved %d", len(updatePostData.PostImages))
}
for _, f := range updatePostData.PostImages {
if _, err := os.Stat("./" + f.Filepath); errors.Is(err, os.ErrNotExist) {
t.Errorf(
"File ./%s does not exist",
f.Filepath,
)
} else {
os.Remove("./" + f.Filepath)
}
}
}
func Test_deletePostImages(t *testing.T) {
t.Log("Testing createPostImages...")
r.HandleFunc("/post/{postID}/image/{imageID}", deletePostImage).Methods("DELETE")
ts := httptest.NewServer(r)
defer ts.Close()
image := fakeImage(100, 100, []string{}, "", false)
image, err := os.Open(image.Name())
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
defer image.Close()
postData := Models.Post{
Title: "Test post",
Content: "Test content",
FrontPage: true,
Order: 1,
PostLinks: []Models.PostLink{
{
Type: "Facebook",
Link: "http://facebook.com/",
},
},
PostImages: []Models.PostImage{
{
Filepath: image.Name(),
},
},
}
err = Database.CreatePost(&postData)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
req, err := http.NewRequest("DELETE", fmt.Sprintf(
"%s/post/%s/image/%s",
ts.URL,
postData.ID,
postData.PostImages[0].ID,
), nil)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
}
}

+ 40
- 56
Api/Posts.go View File

@ -2,46 +2,19 @@ package Api
import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/Auth"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/JsonSerialization"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"github.com/gorilla/mux"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util"
)
func getPostById(w http.ResponseWriter, r *http.Request) (Models.Post, error) {
var (
postData Models.Post
urlVars map[string]string
id string
ok bool
err error
)
urlVars = mux.Vars(r)
id, ok = urlVars["postID"]
if !ok {
log.Printf("Error encountered getting id\n")
JsonReturn(w, 500, "An error occured")
return postData, errors.New("Could not get id")
}
postData, err = Database.GetPostById(id)
if err != nil {
log.Printf("Could not find pet with id %s\n", id)
JsonReturn(w, 404, "Not found")
return postData, err
}
return postData, nil
}
func getPosts(w http.ResponseWriter, r *http.Request) {
var (
posts []Models.Post
@ -56,27 +29,27 @@ func getPosts(w http.ResponseWriter, r *http.Request) {
page, err = strconv.Atoi(values.Get("page"))
if err != nil {
log.Println("Could not parse page url argument")
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
page, err = strconv.Atoi(values.Get("pageSize"))
if err != nil {
log.Println("Could not parse pageSize url argument")
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
posts, err = Database.GetPosts(page, pageSize)
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
returnJson, err = json.MarshalIndent(posts, "", " ")
if err != nil {
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
@ -95,13 +68,13 @@ func getFrontPagePosts(w http.ResponseWriter, r *http.Request) {
posts, err = Database.GetFrontPagePosts()
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
returnJson, err = json.MarshalIndent(posts, "", " ")
if err != nil {
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
@ -117,14 +90,14 @@ func getPost(w http.ResponseWriter, r *http.Request) {
err error
)
postData, err = getPostById(w, r)
postData, err = Util.GetPostById(w, r)
if err != nil {
return
}
returnJson, err = json.MarshalIndent(postData, "", " ")
if err != nil {
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
@ -141,14 +114,16 @@ func createPost(w http.ResponseWriter, r *http.Request) {
err error
)
// TODO: Add auth
log.Printf("Posts handler recieved %s request", r.Method)
_, err = Auth.CheckCookie(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error encountered reading POST body: %s\n", err.Error())
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
@ -161,19 +136,19 @@ func createPost(w http.ResponseWriter, r *http.Request) {
}, false)
if err != nil {
log.Printf("Invalid data provided to posts API: %s\n", err.Error())
JsonReturn(w, 405, "Invalid data")
Util.JsonReturn(w, 405, "Invalid data")
return
}
err = Database.CreatePost(&postData)
if err != nil {
JsonReturn(w, 405, "Invalid data")
Util.JsonReturn(w, 405, "Invalid data")
}
returnJson, err = json.MarshalIndent(postData, "", " ")
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
@ -187,45 +162,48 @@ func updatePost(w http.ResponseWriter, r *http.Request) {
postData Models.Post
requestBody []byte
returnJson []byte
urlVars map[string]string
id string
ok bool
err error
)
urlVars = mux.Vars(r)
id, ok = urlVars["postID"]
if !ok {
_, err = Auth.CheckCookie(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
id, err = Util.GetPostId(r)
if err != nil {
log.Printf("Error encountered getting id\n")
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error encountered reading POST body: %s\n", err.Error())
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
postData, err = JsonSerialization.DeserializePost(requestBody, []string{}, true)
if err != nil {
log.Printf("Invalid data provided to posts API: %s\n", err.Error())
JsonReturn(w, 405, "Invalid data")
Util.JsonReturn(w, 405, "Invalid data")
return
}
postData, err = Database.UpdatePost(id, &postData)
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
returnJson, err = json.MarshalIndent(postData, "", " ")
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}
@ -240,7 +218,13 @@ func deletePost(w http.ResponseWriter, r *http.Request) {
err error
)
postData, err = getPostById(w, r)
_, err = Auth.CheckCookie(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
postData, err = Util.GetPostById(w, r)
if err != nil {
return
}
@ -248,7 +232,7 @@ func deletePost(w http.ResponseWriter, r *http.Request) {
err = Database.DeletePost(&postData)
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
JsonReturn(w, 500, "An error occured")
Util.JsonReturn(w, 500, "An error occured")
return
}


+ 118
- 80
Api/Posts_test.go View File

@ -3,46 +3,72 @@ package Api
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"path"
"runtime"
"strings"
"testing"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"github.com/gorilla/mux"
)
var (
r *mux.Router
)
func init() {
// Fix working directory for tests
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
Database.InitTest()
r = mux.NewRouter()
}
func createTestPost() (Models.Post, error) {
userData, err := createTestUser(true)
postData := Models.Post{
UserID: userData.ID,
Title: "Test post",
Content: "Test content",
FrontPage: true,
Order: 1,
PostLinks: []Models.PostLink{
{
Type: "Facebook",
Link: "http://facebook.com/",
},
},
}
err = Database.CreatePost(&postData)
return postData, err
}
func Test_getPosts(t *testing.T) {
log.SetOutput(ioutil.Discard)
Database.Init()
t.Log("Testing getPosts...")
r := mux.NewRouter()
r.HandleFunc("/post", getPosts).Methods("GET")
ts := httptest.NewServer(r)
defer ts.Close()
var err error
for i := 0; i < 20; i++ {
postData := Models.Post{
Title: "Test post",
Content: "Test content",
FrontPage: true,
Order: i,
PostLinks: []Models.PostLink{
{
Type: "Facebook",
Link: "http://google.com/",
},
},
}
Database.CreatePost(&postData)
defer Database.DB.Unscoped().Delete(&postData)
createTestPost()
}
res, err := http.Get(ts.URL + "/post?page=1&pageSize=10")
@ -66,31 +92,20 @@ func Test_getPosts(t *testing.T) {
}
func Test_getPost(t *testing.T) {
log.SetOutput(ioutil.Discard)
Database.Init()
t.Log("Testing getPost...")
r := mux.NewRouter()
r.HandleFunc("/post/{postID}", getPost).Methods("GET")
ts := httptest.NewServer(r)
defer ts.Close()
postData := Models.Post{
Title: "Test post",
Content: "Test content",
FrontPage: true,
Order: 1,
PostLinks: []Models.PostLink{
{
Type: "Facebook",
Link: "http://google.com/",
},
},
postData, err := createTestPost()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
Database.CreatePost(&postData)
res, err := http.Get(fmt.Sprintf(
"%s/post/%s",
ts.URL,
@ -99,50 +114,86 @@ func Test_getPost(t *testing.T) {
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
t.FailNow()
}
getPostData := new(Models.Post)
err = json.NewDecoder(res.Body).Decode(getPostData)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
if getPostData.Title != "Test post" {
t.Errorf("Expected title \"Test post\", recieved %s", getPostData.Title)
t.FailNow()
}
if getPostData.Content != "Test content" {
t.Errorf("Expected content \"Test content\", recieved %s", getPostData.Content)
t.FailNow()
}
if len(getPostData.PostLinks) != 1 {
t.Errorf("Expected len(PostLinks) == 1, recieved %d", len(getPostData.PostLinks))
t.FailNow()
}
Database.DB.Unscoped().Delete(&postData)
}
func Test_createPost(t *testing.T) {
log.SetOutput(ioutil.Discard)
Database.Init()
t.Log("Testing createPost...")
r := mux.NewRouter()
r.HandleFunc("/post", createPost).Methods("POST")
ts := httptest.NewServer(r)
defer ts.Close()
c, u, err := login()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
postJson := `
{
"user_id": "%s",
"title": "Test post",
"content": "Test content",
"front_page": true,
"order": 1,
"links": [{
"type": "Facebook",
"link": "http://google.com/"
"link": "http://facebook.com/"
}]
}
`
res, err := http.Post(ts.URL+"/post", "application/json", strings.NewReader(postJson))
postJson = fmt.Sprintf(postJson, u.ID.String())
req, err := http.NewRequest("POST", ts.URL+"/post", strings.NewReader(postJson))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
req.AddCookie(c)
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
return
}
postData := new(Models.Post)
@ -157,35 +208,28 @@ func Test_createPost(t *testing.T) {
if postData.Content != "Test content" {
t.Errorf("Expected content \"Test content\", recieved \"%s\"", postData.Content)
}
Database.DB.Unscoped().Delete(&postData)
}
func Test_deletePost(t *testing.T) {
log.SetOutput(ioutil.Discard)
Database.Init()
t.Log("Testing deletePost...")
r := mux.NewRouter()
r.HandleFunc("/post/{postID}", deletePost).Methods("DELETE")
ts := httptest.NewServer(r)
defer ts.Close()
postData := Models.Post{
Title: "Test post",
Content: "Test content",
FrontPage: true,
Order: 1,
PostLinks: []Models.PostLink{
{
Type: "Facebook",
Link: "http://google.com/",
},
},
c, _, err := login()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
Database.CreatePost(&postData)
postData, err := createTestPost()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
req, err := http.NewRequest("DELETE", fmt.Sprintf(
"%s/post/%s",
@ -197,6 +241,8 @@ func Test_deletePost(t *testing.T) {
t.Errorf("Expected nil, recieved %s", err.Error())
}
req.AddCookie(c)
// Fetch Request
res, err := http.DefaultClient.Do(req)
if err != nil {
@ -208,35 +254,27 @@ func Test_deletePost(t *testing.T) {
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
}
Database.DB.Unscoped().Delete(&postData)
}
func Test_updatePost(t *testing.T) {
log.SetOutput(ioutil.Discard)
Database.Init()
t.Log("Testing updatePost...")
r := mux.NewRouter()
r.HandleFunc("/post/{postID}", updatePost).Methods("PUT")
ts := httptest.NewServer(r)
defer ts.Close()
postData := Models.Post{
Title: "Test post",
Content: "Test content",
FrontPage: true,
Order: 1,
PostLinks: []Models.PostLink{
{
Type: "Facebook",
Link: "http://google.com/",
},
},
c, _, err := login()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
Database.CreatePost(&postData)
postData, err := createTestPost()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
postJson := `
{
@ -256,6 +294,8 @@ func Test_updatePost(t *testing.T) {
t.Errorf("Expected nil, recieved %s", err.Error())
}
req.AddCookie(c)
// Fetch Request
res, err := http.DefaultClient.Do(req)
if err != nil {
@ -284,6 +324,4 @@ func Test_updatePost(t *testing.T) {
if updatePostData.Order != 2 {
t.Errorf("Expected 2, recieved %d", updatePostData.Order)
}
Database.DB.Unscoped().Delete(&postData)
}

+ 28
- 11
Api/Routes.go View File

@ -3,27 +3,44 @@ package Api
import (
"log"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/Auth"
"github.com/gorilla/mux"
)
func InitApiEndpoints() *mux.Router {
func InitApiEndpoints(router *mux.Router) {
var (
router *mux.Router
api *mux.Router
)
log.Println("Initializing API routes...")
router = mux.NewRouter()
api = router.PathPrefix("/api/v1/").Subrouter()
// Define routes for posts api
router.HandleFunc("/post", getPosts).Methods("GET")
router.HandleFunc("/frontPagePosts", getFrontPagePosts).Methods("GET")
router.HandleFunc("/post", createPost).Methods("POST")
router.HandleFunc("/post/{postID}", createPost).Methods("GET")
router.HandleFunc("/post/{postID}", updatePost).Methods("PUT")
router.HandleFunc("/post/{postID}", deletePost).Methods("DELETE")
api.HandleFunc("/post", getPosts).Methods("GET")
api.HandleFunc("/post", createPost).Methods("POST")
api.HandleFunc("/post/{postID}", getPost).Methods("GET")
api.HandleFunc("/post/{postID}", updatePost).Methods("PUT")
api.HandleFunc("/post/{postID}", deletePost).Methods("DELETE")
api.HandleFunc("/frontPagePosts", getFrontPagePosts).Methods("GET")
api.HandleFunc("/post/{postID}/image", createPostImage).Methods("POST")
api.HandleFunc("/post/{postID}/image/{imageID}", deletePostImage).Methods("DELETE")
// Define routes for users api
api.HandleFunc("/admin/user", getUsers).Methods("GET")
api.HandleFunc("/admin/user", createUser).Methods("POST")
api.HandleFunc("/admin/user/{userID}", getUser).Methods("GET")
api.HandleFunc("/admin/user/{userID}", updateUser).Methods("PUT")
api.HandleFunc("/admin/user/{userID}", deletePost).Methods("DELETE")
api.HandleFunc("/admin/user/{userID}/update-password", Auth.UpdatePassword).Methods("PUT")
// Define routes for authentication
api.HandleFunc("/admin/login", Auth.Login).Methods("POST")
api.HandleFunc("/admin/logout", Auth.Logout).Methods("GET")
api.HandleFunc("/admin/me", Auth.Me).Methods("GET")
//router.PathPrefix("/").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir("./uploads"))))
return router
}

+ 247
- 0
Api/Users.go View File

@ -0,0 +1,247 @@
package Api
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/Auth"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/JsonSerialization"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util"
)
func getUsers(w http.ResponseWriter, r *http.Request) {
var (
users []Models.User
returnJson []byte
values url.Values
page, pageSize int
search string
err error
)
_, err = Auth.CheckCookie(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
values = r.URL.Query()
page, err = strconv.Atoi(values.Get("page"))
if err != nil {
log.Println("Could not parse page url argument")
Util.JsonReturn(w, 500, "An error occured")
return
}
pageSize, err = strconv.Atoi(values.Get("pageSize"))
if err != nil {
log.Println("Could not parse pageSize url argument")
Util.JsonReturn(w, 500, "An error occured")
return
}
search = values.Get("search")
users, err = Database.GetUsers(page, pageSize, search)
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
if len(users) == 0 {
Util.JsonReturn(w, 404, "No more data")
return
}
returnJson, err = json.MarshalIndent(users, "", " ")
if err != nil {
Util.JsonReturn(w, 500, "An error occured")
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}
func getUser(w http.ResponseWriter, r *http.Request) {
var (
userData Models.User
returnJson []byte
err error
)
_, err = Auth.CheckCookie(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
userData, err = Util.GetUserById(w, r)
if err != nil {
return
}
returnJson, err = json.MarshalIndent(userData, "", " ")
if err != nil {
Util.JsonReturn(w, 500, "An error occured")
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var (
userData Models.User
requestBody []byte
returnJson []byte
err error
)
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error encountered reading POST body: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
userData, err = JsonSerialization.DeserializeUser(requestBody, []string{
"id",
"last_login",
}, false)
if err != nil {
log.Printf("Invalid data provided to user API: %s\n", err.Error())
Util.JsonReturn(w, 405, "Invalid data")
return
}
if userData.FirstName == "" ||
userData.LastName == "" ||
userData.Email == "" ||
userData.Password == "" ||
userData.ConfirmPassword == "" {
Util.JsonReturn(w, http.StatusUnprocessableEntity, "Invalid data")
return
}
err = Database.CheckUniqueEmail(userData.Email)
if err != nil || !Util.IsEmailValid(userData.Email) {
Util.JsonReturn(w, 405, "invalid_email")
return
}
if userData.Password != userData.ConfirmPassword {
Util.JsonReturn(w, 405, "invalid_password")
return
}
userData.Password, err = Auth.HashPassword(userData.Password)
if err != nil {
Util.JsonReturn(w, 500, "An error occured")
return
}
err = Database.CreateUser(&userData)
if err != nil {
Util.JsonReturn(w, 500, "An error occured")
return
}
returnJson, err = json.MarshalIndent(userData, "", " ")
if err != nil {
Util.JsonReturn(w, 500, "An error occured")
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}
func updateUser(w http.ResponseWriter, r *http.Request) {
var (
currentUserData Models.User
userData Models.User
requestBody []byte
returnJson []byte
err error
)
currentUserData, err = Auth.CheckCookieCurrentUser(w, r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error encountered reading POST body: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
userData, err = JsonSerialization.DeserializeUser(requestBody, []string{}, true)
if err != nil {
log.Printf("Invalid data provided to users API: %s\n", err.Error())
Util.JsonReturn(w, 405, "Invalid data")
return
}
err = Database.UpdateUser(currentUserData.ID.String(), &userData)
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
returnJson, err = json.MarshalIndent(userData, "", " ")
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}
func deleteUser(w http.ResponseWriter, r *http.Request) {
var (
userData Models.User
err error
)
_, err = Auth.CheckCookie(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
userData, err = Util.GetUserById(w, r)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
err = Database.DeleteUser(&userData)
if err != nil {
log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
}

+ 372
- 0
Api/Users_test.go View File

@ -0,0 +1,372 @@
package Api
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"os"
"path"
"runtime"
"strings"
"testing"
"time"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/Auth"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"github.com/gorilla/mux"
)
func init() {
// Fix working directory for tests
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
Database.InitTest()
r = mux.NewRouter()
}
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
func createTestUser(random bool) (Models.User, error) {
now := time.Now()
email := "email@email.com"
if random {
email = fmt.Sprintf("%s@email.com", randString(16))
}
password, err := Auth.HashPassword("password")
if err != nil {
return Models.User{}, err
}
userData := Models.User{
Email: email,
Password: password,
LastLogin: &now,
FirstName: "Hugh",
LastName: "Mann",
}
err = Database.CreateUser(&userData)
return userData, err
}
func login() (*http.Cookie, Models.User, error) {
var (
c *http.Cookie
u Models.User
)
r.HandleFunc("/admin/login", Auth.Login).Methods("POST")
ts := httptest.NewServer(r)
defer ts.Close()
u, err := createTestUser(true)
if err != nil {
return c, u, err
}
postJson := `
{
"email": "%s",
"password": "password"
}
`
postJson = fmt.Sprintf(postJson, u.Email)
res, err := http.Post(ts.URL+"/admin/login", "application/json", strings.NewReader(postJson))
if err != nil {
return c, u, err
}
if res.StatusCode != http.StatusOK {
return c, u, errors.New("Invalid res.StatusCode")
}
if len(res.Cookies()) != 1 {
return c, u, errors.New("Invalid cookies length")
}
return res.Cookies()[0], u, nil
}
func Test_getUser(t *testing.T) {
t.Log("Testing getUser...")
r.HandleFunc("/user/{userID}", getUser).Methods("GET")
ts := httptest.NewServer(r)
defer ts.Close()
c, u, err := login()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
req, err := http.NewRequest("GET", fmt.Sprintf(
"%s/user/%s",
ts.URL,
u.ID,
), nil)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
req.AddCookie(c)
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
t.FailNow()
}
getUserData := new(Models.User)
err = json.NewDecoder(res.Body).Decode(getUserData)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
if getUserData.Email != u.Email {
t.Errorf("Expected email \"%s\", recieved %s", u.Email, getUserData.Email)
t.FailNow()
}
if getUserData.FirstName != u.FirstName {
t.Errorf("Expected email \"%s\", recieved %s", u.FirstName, getUserData.FirstName)
t.FailNow()
}
if getUserData.LastName != u.LastName {
t.Errorf("Expected email \"%s\", recieved %s", u.LastName, getUserData.LastName)
t.FailNow()
}
}
func Test_getUsers(t *testing.T) {
t.Log("Testing getUsers...")
r.HandleFunc("/user", getUsers).Methods("GET")
ts := httptest.NewServer(r)
defer ts.Close()
c, _, err := login()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
for i := 0; i < 20; i++ {
createTestUser(true)
}
req, err := http.NewRequest("GET", ts.URL+"/user?page=0&pageSize=10", nil)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
req.AddCookie(c)
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
t.FailNow()
}
getUsersData := new([]Models.User)
err = json.NewDecoder(res.Body).Decode(getUsersData)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
if len(*getUsersData) != 10 {
t.Errorf("Expected 10, recieved %d", len(*getUsersData))
t.FailNow()
}
}
func Test_createUser(t *testing.T) {
t.Log("Testing createUser...")
r.HandleFunc("/user", createUser).Methods("POST")
ts := httptest.NewServer(r)
defer ts.Close()
email := fmt.Sprintf("%s@email.com", randString(16))
postJson := `
{
"email": "%s",
"password": "password",
"confirm_password": "password",
"first_name": "Hugh",
"last_name": "Mann"
}
`
postJson = fmt.Sprintf(postJson, email)
res, err := http.Post(ts.URL+"/user", "application/json", strings.NewReader(postJson))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
return
}
}
func Test_updateUser(t *testing.T) {
t.Log("Testing updateUser...")
r.HandleFunc("/user/{userID}", updateUser).Methods("PUT")
ts := httptest.NewServer(r)
defer ts.Close()
c, u, err := login()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
email := fmt.Sprintf("%s@email.com", randString(16))
postJson := `
{
"email": "%s",
"first_name": "first",
"last_name": "last"
}
`
postJson = fmt.Sprintf(postJson, email)
req, err := http.NewRequest("PUT", fmt.Sprintf(
"%s/user/%s",
ts.URL,
u.ID,
), strings.NewReader(postJson))
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
req.AddCookie(c)
// Fetch Request
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
}
updateUserData := new(Models.User)
err = json.NewDecoder(res.Body).Decode(updateUserData)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
if updateUserData.Email != email {
t.Errorf("Expected email \"%s\", recieved %s", email, updateUserData.Email)
}
if updateUserData.FirstName != "first" {
t.Errorf("Expected FirstName \"first\", recieved %s", updateUserData.FirstName)
}
if updateUserData.LastName != "last" {
t.Errorf("Expected LastName \"last\", recieved %s", updateUserData.LastName)
}
}
func Test_deleteUser(t *testing.T) {
t.Log("Testing deleteUser...")
r.HandleFunc("/user/{userID}", deleteUser).Methods("DELETE")
ts := httptest.NewServer(r)
defer ts.Close()
c, _, err := login()
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
userData, err := createTestUser(true)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
t.FailNow()
}
req, err := http.NewRequest("DELETE", fmt.Sprintf(
"%s/user/%s",
ts.URL,
userData.ID,
), nil)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
}
req.AddCookie(c)
// Fetch Request
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Errorf("Expected nil, recieved %s", err.Error())
return
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode)
}
}

+ 37
- 13
Database/Init.go View File

@ -10,14 +10,30 @@ import (
)
const dbUrl = "postgres://postgres:@localhost:5432/sudden_impact_records"
const dbTestUrl = "postgres://postgres:@localhost:5432/sudden_impact_records_test"
var (
DB *gorm.DB
)
func GetModels() []interface{} {
return []interface{}{
&Models.User{},
&Models.PostImage{},
&Models.PostVideo{},
&Models.PostAudio{},
&Models.PostLink{},
&Models.Post{},
&Models.SubscriptionEmailAttachment{},
&Models.SubscriptionEmail{},
&Models.Subscription{},
}
}
func Init() {
var (
err error
model interface{}
err error
)
log.Println("Initializing database...")
@ -28,19 +44,27 @@ func Init() {
log.Fatalln(err)
}
log.Println("Running AutoMigrate on Post tables...")
log.Println("Running AutoMigrate...")
// Post tables
DB.AutoMigrate(&Models.PostImage{})
DB.AutoMigrate(&Models.PostVideo{})
DB.AutoMigrate(&Models.PostAudio{})
DB.AutoMigrate(&Models.PostLink{})
DB.AutoMigrate(&Models.Post{})
for _, model = range GetModels() {
DB.AutoMigrate(model)
}
}
log.Println("Running AutoMigrate on Subscription tables...")
func InitTest() {
var (
model interface{}
err error
)
// Email subscription tables
DB.AutoMigrate(&Models.SubscriptionEmailAttachment{})
DB.AutoMigrate(&Models.SubscriptionEmail{})
DB.AutoMigrate(&Models.Subscription{})
DB, err = gorm.Open(postgres.Open(dbTestUrl), &gorm.Config{})
if err != nil {
log.Fatalln(err)
}
for _, model = range GetModels() {
DB.Migrator().DropTable(model)
DB.AutoMigrate(model)
}
}

+ 33
- 0
Database/PostImages.go View File

@ -0,0 +1,33 @@
package Database
import (
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func CreatePostImage(postImageData *Models.PostImage) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(postImageData).
Error
}
func GetPostImageById(id string) (Models.PostImage, error) {
var (
postImageData Models.PostImage
err error
)
err = DB.Preload(clause.Associations).
First(&postImageData, "id = ?", id).
Error
return postImageData, err
}
func DeletePostImage(postImageData *Models.PostImage) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(postImageData).
Error
}

+ 6
- 0
Database/Posts.go View File

@ -70,12 +70,18 @@ func UpdatePost(id string, postData *Models.Post) (Models.Post, error) {
err error
)
DB.Model(postData).
Where("id = ?", id).
Association("PostLinks").
Replace(postData.PostLinks)
err = DB.Model(&Models.Post{}).
Select("*").
Omit("id", "created_at", "updated_at", "deleted_at").
Where("id = ?", id).
Updates(postData).
Error
if err != nil {
return Models.Post{}, err
}


+ 8
- 0
Database/Seeder/Seed.go View File

@ -0,0 +1,8 @@
package Seeder
import "log"
func Seed() {
log.Println("Seeding users...")
SeedUsers()
}

+ 97
- 0
Database/Seeder/UserSeeder.go View File

@ -0,0 +1,97 @@
package Seeder
import (
"fmt"
"math/rand"
"time"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/Auth"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util"
)
var (
firstNames = []string{
"John",
"Mark",
"Annie",
"Hannah",
"Shane",
"Joe",
"Katara",
"Zuko",
"Aang",
"Sokka",
}
lastNames = []string{
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Garcia",
"Miller",
"Davis",
"Lopez",
}
)
func randName(last bool) string {
var (
choices []string
)
choices = firstNames
if last {
choices = lastNames
}
return choices[rand.Intn(len(choices))]
}
func createUser() (Models.User, error) {
var (
userData Models.User
now time.Time
firstName, lastName string
email, password string
err error
)
now = time.Now()
firstName = randName(false)
lastName = randName(true)
email = fmt.Sprintf("%s%s+%s@email.com", firstName, lastName, Util.RandomString(4))
password, err = Auth.HashPassword("password")
if err != nil {
return Models.User{}, err
}
userData = Models.User{
Email: email,
Password: password,
LastLogin: &now,
FirstName: firstName,
LastName: lastName,
}
err = Database.CreateUser(&userData)
return userData, err
}
func SeedUsers() {
var (
i int
err error
)
for i = 0; i <= 20; i++ {
_, err = createUser()
if err != nil {
panic(err)
}
}
}

+ 139
- 0
Database/Users.go View File

@ -0,0 +1,139 @@
package Database
import (
"errors"
"fmt"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func GetUserById(id string) (Models.User, error) {
var (
userData Models.User
err error
)
err = DB.Preload(clause.Associations).
First(&userData, "id = ?", id).
Error
userData.Password = ""
return userData, err
}
func GetUserByEmail(email string) (Models.User, error) {
var (
userData Models.User
err error
)
err = DB.Preload(clause.Associations).
First(&userData, "email = ?", email).
Error
return userData, err
}
func GetUsers(page, pageSize int, search string) ([]Models.User, error) {
var (
users []Models.User
query *gorm.DB
offset int
i int
err error
)
switch {
case pageSize > 100:
pageSize = 100
case pageSize <= 0:
pageSize = 10
}
offset = page * pageSize
search = fmt.Sprintf("%%%s%%", search)
query = DB.Model(Models.User{}).
Offset(offset).
Limit(pageSize).
Order("created_at desc")
if search != "" {
query = query.
Where("CONCAT_WS(' ', first_name, last_name) LIKE ?", search).
Or("email LIKE ?", search)
}
err = query.
Find(&users).
Error
for i, _ = range users {
users[i].Password = ""
}
return users, err
}
func CheckUniqueEmail(email string) error {
var (
exists bool
err error
)
err = DB.Model(Models.User{}).
Select("count(*) > 0").
Where("email = ?", email).
Find(&exists).
Error
if err != nil {
return err
}
if exists {
return errors.New("Invalid email")
}
return nil
}
func CreateUser(userData *Models.User) error {
var (
err error
)
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(userData).
Error
userData.Password = ""
return err
}
func UpdateUser(id string, userData *Models.User) error {
var (
err error
)
err = DB.Model(&userData).
Omit("id", "created_at", "updated_at", "deleted_at").
Where("id = ?", id).
Updates(userData).
Error
userData.Password = ""
return err
}
func DeleteUser(userData *Models.User) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(userData).
Error
}

+ 13
- 0
Frontend/GetFrontendAssets_dev.go View File

@ -0,0 +1,13 @@
//go:build !prod
// +build !prod
package Frontend
import (
"io/fs"
"os"
)
func GetFrontendAssets() fs.FS {
return os.DirFS("Frontend/vue/dist")
}

+ 26
- 0
Frontend/GetFrontendAssets_prod.go View File

@ -0,0 +1,26 @@
//go:build prod
// +build prod
package Frontend
import (
"embed"
"io/fs"
"log"
)
//go:embed Frontend/vue/dist
var frontend embed.FS
func GetFrontendAssets() fs.FS {
var (
stripped fs.FS
err error
)
stripped, err = fs.Sub(frontend, "Frontend/vue/dist")
if err != nil {
log.Fatalln(err)
}
return stripped
}

+ 51
- 0
Frontend/Routes.go View File

@ -0,0 +1,51 @@
package Frontend
import (
"io/fs"
"net/http"
"github.com/gorilla/mux"
)
const (
indexPath = "Frontend/vue/dist/index.html"
)
var (
routes []string = []string{
"/admin/login",
"/admin/signup",
"/admin/users",
"/admin/users/new",
"/admin/users/{id}",
}
)
func indexHandler(entrypoint string) func(w http.ResponseWriter, r *http.Request) {
fn := func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, entrypoint)
}
return http.HandlerFunc(fn)
}
func InitFrontendRoutes(router *mux.Router) {
var (
frontendFS http.Handler
stripped fs.FS
route string
)
stripped = GetFrontendAssets()
frontendFS = http.FileServer(http.FS(stripped))
for _, route = range routes {
router.
PathPrefix(route).
HandlerFunc(indexHandler(indexPath))
}
router.PathPrefix("/").Handler(frontendFS)
}

+ 0
- 0
Frontend/public/images/.gitkeep View File


+ 23
- 0
Frontend/vue/.gitignore View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

+ 24
- 0
Frontend/vue/README.md View File

@ -0,0 +1,24 @@
# vue
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

+ 5
- 0
Frontend/vue/babel.config.js View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

+ 19
- 0
Frontend/vue/jsconfig.json View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

+ 20536
- 0
Frontend/vue/package-lock.json
File diff suppressed because it is too large
View File


+ 56
- 0
Frontend/vue/package.json View File

@ -0,0 +1,56 @@
{
"name": "vue",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"watch": "vue-cli-service build --watch"
},
"dependencies": {
"@meforma/vue-toaster": "^1.3.0",
"@vee-validate/rules": "^4.5.10",
"@vuepic/vue-datepicker": "^3.0.0",
"axios": "^0.26.1",
"bootstrap": "^5.1.3",
"core-js": "^3.8.3",
"vee-validate": "^4.5.10",
"vue": "^3.2.13",
"vue-axios": "^3.4.1",
"vue-router": "^4.0.13",
"vue-toastification": "^2.0.0-rc.5",
"vue3-cookies": "^1.0.6",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.5.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

BIN
Frontend/vue/public/favicon.ico View File

Before After

+ 17
- 0
Frontend/vue/public/index.html View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

+ 12
- 0
Frontend/vue/src/App.vue View File

@ -0,0 +1,12 @@
<template>
<router-view />
</template>
<script>
export default {
name: 'App',
}
</script>
<style>
</style>

+ 116
- 0
Frontend/vue/src/assets/css/admin.css View File

@ -0,0 +1,116 @@
body {
min-height: 100vh;
}
#app, #admin-page-container {
min-height: 100vh;
height: 100%;
}
#admin-page-container {
background-color: #ccc;
}
.background-color {
background-color: #ccc;
}
.card-registration {
border-radius: 1.5rem !important;
}
.card-registration .select-input.form-control[readonly]:not([disabled]) {
font-size: 1rem;
line-height: 2.15;
padding-left: .75em;
padding-right: .75em;
}
.card-registration .select-arrow {
top: 13px;
}
.center-align {
text-align: center;
}
.center-align * {
display: inline-block;
}
.right-align {
text-align: right;
}
.right-align * {
margin-left: 1rem;
}
.page-nav-container {
background-color: #FFF;
border-radius: 1.5rem;
height: 3.4rem;
padding: 0.5rem;
}
.page-nav-container .btn-rounded {
border-radius: 1rem;
}
.page-nav-container input {
border-radius: 1rem;
}
.page-nav-container .input-group-append button {
border-radius: 0 1rem 1rem 0;
}
.float-right {
float: right;
}
table th,
table td {
text-align: center;
}
table td {
text-align: center;
}
input.invalid {
border-color: var(--bs-danger);
}
label[role=alert] {
color: var(--bs-danger);
}
.dp__input.dp__input_icon_pad {
min-height: calc(1.5em + 1rem + 2px);
padding: .5rem 1rem;
padding-left: 35px !important;
font-size: 1.25rem;
border-radius: .3rem;
display: block;
width: 100%;
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.dp__input.dp__input_icon_pad:disabled {
background-color: #e9ecef;
opacity: 1;
}

BIN
Frontend/vue/src/assets/logo.png View File

Before After
Width: 200  |  Height: 200  |  Size: 6.7 KiB

+ 58
- 0
Frontend/vue/src/components/HelloWorld.vue View File

@ -0,0 +1,58 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

+ 105
- 0
Frontend/vue/src/components/admin/AdminLogin.vue View File

@ -0,0 +1,105 @@
<template>
<section class="vh-100 background-color">
<div class="container py-5 h-100">
<div class="row justify-content-center align-items-center h-100">
<div class="col-12 col-lg-9 col-xl-7">
<div class="card shadow-2-strong card-registration border-2" style="border-radius: 15px;">
<div class="card-body p-4 p-md-5">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Login</h3>
<Form @submit="login" v-slot="{ meta, errors }">
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="email"
type="email"
id="emailAddress"
name="Email"
class="form-control form-control-lg"
:class="errors['Email'] ? 'invalid' : ''"
rules="required|email"/>
<label v-if="!errors['Email']" class="form-label" for="email">Email</label>
<ErrorMessage name="Email" as="label" class="form-label" for="email"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="password"
type="password"
id="emailAddress"
name="Password"
class="form-control form-control-lg"
:class="errors['Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Password']" class="form-label" for="password">Password</label>
<ErrorMessage name="Password" as="label" class="form-label" for="password"/>
</div>
</div>
</div>
<div class="mt-2 pt-2 center-align">
<button
:disabled="!meta.touched || !meta.valid"
class="btn btn-primary btn-lg"
type="submit"
>
Login
</button>
</div>
<div class="mt-2 pt-2 center-align">
<p style="padding-right: 10px;">Don't have an account? </p><router-link :to='{"name": "AdminSignup"}'>Sign up</router-link>
</div>
</Form>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
import { Form, Field, ErrorMessage } from 'vee-validate'
export default {
name: 'AdminLogin',
data() {
return {
email: '',
password: '',
}
},
components: {
Form,
Field,
ErrorMessage,
},
methods: {
async login () {
try {
const response = await this.axios.post(
'/admin/login',
{
email: this.email,
password: this.password,
}
)
if (response.status === 200) {
this.$store.dispatch('setUser', response.data)
this.$router.push({ name: 'AdminUsersList' })
}
} catch (error) {
this.$toast.error('An error occured')
}
}
}
}
</script>

+ 89
- 0
Frontend/vue/src/components/admin/AdminNavbar.vue View File

@ -0,0 +1,89 @@
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid px-5">
<!-- TODO: Replace with logo -->
<router-link
:to="{ name: 'AdminUsersList' }"
class="nav-item"
>
Sudden Impact
</router-link>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-md-auto gap-2">
<li class="nav-item rounded">
<router-link
:to="{ name: 'AdminUsersList' }"
class="nav-link"
aria-current="page"
>
<i class="bi bi-house-fill me-2"></i>
Users
</router-link>
</li>
<li class="nav-item dropdown rounded">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"><i class="bi bi-person-fill me-2"></i>Profile</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li>
<router-link
:to="{ name: 'AdminUsersForm', params: { id: $store.getters.getUser.id } }"
class="dropdown-item"
>
Account
</router-link>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<div v-on:click="logout" class="dropdown-item">Logout</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default {
methods: {
async logout () {
try {
const response = await this.axios.get('/admin/logout');
if (response.status === 200) {
this.$store.dispatch('setUser', null);
this.$router.push({ name: 'AdminLogin' })
}
} catch (error) {
console.log(error)
}
}
}
}
</script>
<style>
body {
font-family: Montserrat, sans-serif;
}
.navbar-nav .nav-item:hover {
background-color: rgba(180, 190, 203, 0.4);
}
.navbar-dark .navbar-nav .nav-link.router-link-active {
color: #fff;
}
</style>

+ 166
- 0
Frontend/vue/src/components/admin/AdminSignup.vue View File

@ -0,0 +1,166 @@
<template>
<section class="vh-100 background-color">
<div class="container py-5 h-100">
<div class="row justify-content-center align-items-center h-100">
<div class="col-12 col-lg-9 col-xl-7">
<div class="card shadow-2-strong card-registration border-2" style="border-radius: 15px;">
<div class="card-body p-4 p-md-5">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Sign Up</h3>
<Form @submit="signup" v-slot="{ errors, meta }">
<div class="row">
<div class="col-md-6 mb-4">
<div class="form-outline">
<Field
v-model="first_name"
type="text"
id="firstName"
name="First Name"
class="form-control form-control-lg"
:class="errors['First Name'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['First Name']" class="form-label" for="firstName">First Name</label>
<ErrorMessage name="First Name" as="label" class="form-label" for="firstName"/>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="form-outline">
<Field
v-model="last_name"
type="text"
id="lastName"
name="Last Name"
class="form-control form-control-lg"
:class="errors['Last Name'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['Last Name']" class="form-label" for="lastName">Last Name</label>
<ErrorMessage name="Last Name" as="label" class="form-label" for="lastName"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="email"
type="text"
id="email"
name="Email"
class="form-control form-control-lg"
:class="errors['Email'] ? 'invalid' : ''"
rules="required|email"/>
<label v-if="!errors['Email']" class="form-label" for="email">Email</label>
<ErrorMessage name="Email" as="label" class="form-label" for="email"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="password"
type="password"
id="password"
name="Password"
class="form-control form-control-lg"
:class="errors['Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Password']" class="form-label" for="password">Password</label>
<ErrorMessage name="Password" as="label" class="form-label" for="password"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="confirm_password"
type="password"
id="confirm_password"
name="Confirm Password"
class="form-control form-control-lg"
:class="errors['Confirm Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Confirm Password']" class="form-label" for="password">Confirm Password</label>
<ErrorMessage name="Confirm Password" as="label" class="form-label" for="password"/>
</div>
</div>
</div>
<div class="mt-2 pt-2 center-align">
<button
:disabled="!meta.touched || !meta.valid"
class="btn btn-primary btn-lg"
type="submit"
>
Sign Up
</button>
</div>
<div class="mt-2 pt-2 center-align">
<p style="padding-right: 10px;">Already have an account? </p><router-link :to='{"name": "AdminLogin"}'>Login</router-link>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
import { Form, Field, ErrorMessage } from 'vee-validate'
export default {
name: 'AdminSignup',
data() {
return {
first_name: '',
last_name: '',
email: '',
password: '',
confirm_password: ''
}
},
components: {
Form,
Field,
ErrorMessage,
},
methods: {
async signup () {
try {
const response = await this.axios.post(
'/admin/user',
{
first_name: this.first_name,
last_name: this.last_name,
email: this.email,
password: this.password,
confirm_password: this.confirm_password,
}
)
if (response.status === 200) {
this.$router.push({ name: 'AdminLogin' })
}
} catch (error) {
if (error.response.data.message === 'invalid_email') {
this.$toast.error('Email already exists.')
return
}
this.$toast.error('An error occured.')
}
}
}
}
</script>

+ 165
- 0
Frontend/vue/src/components/admin/users/AdminUsersCreate.vue View File

@ -0,0 +1,165 @@
<template>
<div id="admin-page-container">
<admin-navbar/>
<section class="container mt-5">
<div class="row mb-3">
<div class="col-12">
<div class="page-nav-container">
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-rounded"
:class="tab === 'details' ? 'btn-dark' : 'btn-outline-dark'"
>
User Details
</button>
</div>
</div>
</div>
</div>
<div class="card shadow-2-strong card-registration">
<div class="card-body p-4 p-md-5" v-if="tab === 'details'">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Create User</h3>
<Form @submit="createUser" v-slot="{ meta, errors }">
<div class="row">
<div class="col-md-6 mb-4">
<div class="form-outline">
<Field
v-model="user.first_name"
type="text"
id="firstName"
name="First Name"
class="form-control form-control-lg"
:class="errors['First Name'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['First Name']" class="form-label" for="firstName">First Name</label>
<ErrorMessage name="First Name" as="label" class="form-label" for="firstName"/>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="form-outline">
<Field
v-model="user.last_name"
type="text"
id="lastName"
name="Last Name"
class="form-control form-control-lg"
:class="errors['Last Name'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['Last Name']" class="form-label" for="lastName">Last Name</label>
<ErrorMessage name="Last Name" as="label" class="form-label" for="lastName"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="user.email"
type="email"
id="email"
name="Email"
class="form-control form-control-lg"
:class="errors['Email'] ? 'invalid' : ''"
rules="required|email"/>
<label v-if="!errors['Email']" class="form-label" for="email">Email</label>
<ErrorMessage name="Email" as="label" class="form-label" for="email"/>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="user.password"
type="password"
id="password"
name="Password"
class="form-control form-control-lg"
:class="errors['Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Password']" class="form-label" for="password">Password</label>
<ErrorMessage name="Password" as="label" class="form-label" for="email"/>
</div>
</div>
<div class="col-12 col-md-6 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="user.confirm_password"
type="password"
id="confirm_password"
name="Confirm Password"
class="form-control form-control-lg"
:class="errors['Confirm Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Confirm Password']" class="form-label" for="confirm_password">Confirm Password</label>
<ErrorMessage name="Confirm Password" as="label" class="form-label" for="confirm_password"/>
</div>
</div>
</div>
<div class="mt-2 pt-2 right-align">
<button :disabled="!meta.touched || !meta.valid" class="btn btn-primary btn-md" type="submit">
Create
</button>
</div>
</Form>
</div>
</div>
</section>
</div>
</template>
<script>
import AdminNavbar from '@/components/admin/AdminNavbar'
import { Form, Field, ErrorMessage } from 'vee-validate'
export default {
data() {
return {
tab: 'details',
user: {
first_name: null,
last_name: null,
email: null,
password: null,
confirm_password: null,
}
}
},
components: {
AdminNavbar,
Form,
Field,
ErrorMessage,
},
methods: {
async createUser () {
try {
let response = await this.axios.post(
'/admin/user',
this.user,
)
if (response.status === 200) {
this.$router.push({ name: 'AdminUsersForm', params: { id: response.data.id } })
this.$toast.success('Successfully created user details.');
}
} catch (error) {
this.$toast.error('An error occured');
}
},
}
}
</script>

+ 252
- 0
Frontend/vue/src/components/admin/users/AdminUsersForm.vue View File

@ -0,0 +1,252 @@
<template>
<div id="admin-page-container">
<admin-navbar/>
<section class="container mt-5">
<div class="row mb-3">
<div class="col-12">
<div class="page-nav-container">
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-rounded"
:class="tab === 'details' ? 'btn-dark' : 'btn-outline-dark'"
@click="tab = 'details'"
>
User Details
</button>
<button
type="button"
class="btn btn-rounded"
:class="tab === 'change_password' ? 'btn-dark' : 'btn-outline-dark'"
@click="tab = 'change_password'"
>
Change Password
</button>
</div>
</div>
</div>
</div>
<div class="card shadow-2-strong card-registration">
<div class="card-body p-4 p-md-5" v-if="tab === 'details'">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Update User</h3>
<Form @submit="updateUser" v-slot="{ meta, errors }">
<div class="row">
<div class="col-md-6 mb-4">
<div class="form-outline">
<Field
v-model="user.first_name"
type="text"
id="firstName"
name="First Name"
class="form-control form-control-lg"
:class="errors['First Name'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['First Name']" class="form-label" for="firstName">First Name</label>
<ErrorMessage name="First Name" as="label" class="form-label" for="firstName"/>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="form-outline">
<Field
v-model="user.last_name"
type="text"
id="lastName"
name="Last Name"
class="form-control form-control-lg"
:class="errors['Last Name'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['Last Name']" class="form-label" for="lastName">Last Name</label>
<ErrorMessage name="Last Name" as="label" class="form-label" for="lastName"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="user.email"
type="email"
id="email"
name="Email"
class="form-control form-control-lg"
:class="errors['Email'] ? 'invalid' : ''"
rules="required|email"/>
<label v-if="!errors['Email']" class="form-label" for="email">Email</label>
<ErrorMessage name="Email" as="label" class="form-label" for="email"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-4 pb-2">
<div class="form-outline">
<date-picker
v-model="user.last_login"
format="dd/MM/yyyy, HH:mm"
disabled="disabled"
id="last_login"/>
<label class="form-label" for="last_login">Last Login</label>
</div>
</div>
</div>
<div class="mt-2 pt-2 right-align">
<button class="btn btn-danger btn-md" type="button">
Delete
</button>
<button :disabled="!meta.touched || !meta.valid" class="btn btn-primary btn-md" type="submit">
Update
</button>
</div>
</Form>
</div>
<div class="card-body p-4 p-md-5" v-if="tab === 'change_password'">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Change Password</h3>
<Form @submit="updatePassword" v-slot="{ meta, errors }">
<div class="row">
<div class="col-12 col-md-6 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="password.password"
type="password"
id="password"
name="Password"
class="form-control form-control-lg"
:class="errors['Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Password']" class="form-label" for="password">Password</label>
<ErrorMessage name="Password" as="label" class="form-label" for="email"/>
</div>
</div>
<div class="col-12 col-md-6 mb-4 pb-2">
<div class="form-outline">
<Field
v-model="password.confirm_password"
type="password"
id="confirm_password"
name="Confirm Password"
class="form-control form-control-lg"
:class="errors['Confirm Password'] ? 'invalid' : ''"
rules="required|min:8"/>
<label v-if="!errors['Confirm Password']" class="form-label" for="confirm_password">Confirm Password</label>
<ErrorMessage name="Confirm Password" as="label" class="form-label" for="confirm_password"/>
</div>
</div>
</div>
<div class="mt-2 pt-2 right-align">
<button
type="submit"
:disabled="!meta.touched || !meta.valid"
class="btn btn-primary btn-md"
>
Update Password
</button>
</div>
</Form>
</div>
</div>
</section>
</div>
</template>
<script>
import AdminNavbar from '@/components/admin/AdminNavbar'
import { Form, Field, ErrorMessage } from 'vee-validate'
export default {
data() {
return {
tab: 'details',
user: {
first_name: null,
last_name: null,
email: null,
last_login: null,
},
password: {
password: null,
confirm_password: null,
}
}
},
components: {
AdminNavbar,
Form,
Field,
ErrorMessage,
},
mounted () {
this.getUser()
},
methods: {
setUserFromResponse (response) {
this.user = {
first_name: response.data.first_name,
last_name: response.data.last_name,
email: response.data.email,
last_login: response.data.last_login,
}
},
async getUser () {
try {
const response = await this.axios.get(`/admin/user/${this.$route.params.id}`)
if (response.status === 200) {
this.setUserFromResponse(response)
}
} catch (error) {
console.log(error)
}
},
async updateUser () {
try {
let response = await this.axios.put(
`/admin/user/${this.$route.params.id}`,
this.user,
)
if (response.status === 200) {
this.$toast.success('Successfully updated user details.');
this.setUserFromResponse(response)
}
} catch (error) {
this.$toast.error('An error occured');
}
},
async updatePassword () {
try {
let response = await this.axios.put(
`/admin/user/${this.$route.params.id}/update-password`,
this.password,
)
if (response.status === 200) {
this.$toast.success('Successfully updated user password.');
}
} catch (error) {
this.$toast.error('An error occured');
}
}
}
}
</script>

+ 168
- 0
Frontend/vue/src/components/admin/users/AdminUsersList.vue View File

@ -0,0 +1,168 @@
<template>
<div id="admin-page-container">
<admin-navbar/>
<div class="container table-responsive mt-5 pb-5">
<div class="row mb-3">
<div class="col-12">
<div class="page-nav-container">
<div class="row">
<div class="col-sm-6 col-9">
<div class="input-group">
<input
type="text"
class="form-control"
placeholder="Search..."
ref="search"
>
<div class="input-group-append">
<button
class="btn btn-dark"
type="button"
@click="searchUsers"
>
Search
</button>
</div>
</div>
</div>
<div class="col-sm-6 float-right col-3">
<div class="btn-group float-right" role="group">
<router-link :to="{ name: 'AdminUsersCreate' }">
<button
type="button"
class="btn btn-rounded btn-dark"
>
<!-- TODO: Change this to + sign on small screens -->
Add User
</button>
</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-2-strong card-registration">
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th scope="col">Name</th>
<th scope="col" class="d-none d-sm-table-cell">Email</th>
<th scope="col" class="d-none d-sm-table-cell">Last Login</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td class="align-middle">{{ user.first_name }} {{ user.last_name }}</td>
<td class="align-middle d-none d-sm-table-cell">{{ user.email }}</td>
<td class="align-middle d-none d-sm-table-cell">{{ formatDate(user.last_login) }}</td>
<td class="align-middle">
<router-link
:to="{ name: 'AdminUsersForm', params: { id: user.id } }"
>
<button
class="btn btn-outline-dark"
>
Open
</button>
</router-link>
</td>
</tr>
</tbody>
</table>
<p v-if="dataEnd" class="py-2 center-align text-muted">No more data</p>
</div>
</div>
</div>
</template>
<script>
import AdminNavbar from '@/components/admin/AdminNavbar'
export default {
data() {
return {
users: {},
pageSize: 15,
page: 0,
search: '',
dataEnd: false,
}
},
components: {
AdminNavbar,
},
beforeMount () {
this.getInitialUsers()
},
mounted () {
this.getNextUsers()
},
methods: {
formatDate (dateString) {
const d = new Date(dateString)
let hours = d.getHours();
let minutes = d.getMinutes();
const ampm = hours >= 12 ? 'pm' : 'am';
hours = hours % 12;
hours = hours ? hours : 12; // the hour '0' should be '12'
minutes = minutes < 10 ? '0'+minutes : minutes;
const strTime = hours + ':' + minutes + ' ' + ampm;
return d.getDate() + "/" + (d.getMonth()+1) + "/" + d.getFullYear() + " " + strTime;
},
async getInitialUsers () {
try {
const response = await this.axios.get(
`/admin/user?page=${this.page}&pageSize=${this.pageSize}&search=${this.search}`
)
if (response.status === 200) {
this.users = response.data
}
} catch (error) {
if (error.response.status === 404) {
this.users = {}
this.dataEnd = true
}
}
},
async getNextUsers () {
window.onscroll = async () => {
let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight;
if (bottomOfWindow) {
try {
this.page += 1
const response = await this.axios.get(
`/admin/user?page=${this.page}&pageSize=${this.pageSize}&search=${this.search}`
)
if (response.status === 200) {
this.users.push(...response.data)
}
} catch (error) {
if (error.response.status === 404) {
this.dataEnd = true
}
}
}
}
},
searchUsers () {
this.search = this.$refs.search.value
this.getInitialUsers()
}
}
}
</script>

+ 37
- 0
Frontend/vue/src/main.js View File

@ -0,0 +1,37 @@
import { createApp } from 'vue'
import axios from './utils/http'
import VueAxios from 'vue-axios'
import VueCookies from "vue3-cookies";
import { defineRule } from 'vee-validate';
import AllRules from '@vee-validate/rules';
import Toaster from "@meforma/vue-toaster";
import Datepicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css'
import App from './App.vue'
import router from './router'
import admin from './store/admin/index.js'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.js'
// Import the CSS or use your own!
import "vue-toastification/dist/index.css";
import './assets/css/admin.css'
const app = createApp(App)
router.app = app
app.use(router)
app.use(VueAxios, axios)
app.use(VueCookies)
app.use(admin)
app.use(Toaster, { position: 'top-right' })
Object.keys(AllRules).forEach(rule => {
defineRule(rule, AllRules[rule]);
});
app.component('date-picker', Datepicker);
app.mount('#app')

+ 79
- 0
Frontend/vue/src/router/index.js View File

@ -0,0 +1,79 @@
import { createWebHistory, createRouter } from "vue-router";
import HelloWorld from "@/components/HelloWorld.vue";
import AdminLogin from "@/components/admin/AdminLogin.vue";
import AdminSignup from "@/components/admin/AdminSignup.vue";
import AdminUsersList from "@/components/admin/users/AdminUsersList.vue";
import AdminUsersCreate from "@/components/admin/users/AdminUsersCreate.vue";
import AdminUsersForm from "@/components/admin/users/AdminUsersForm.vue";
import admin from '@/store/admin/index.js'
const routes = [
{
path: "/",
name: "Home",
component: HelloWorld,
},
{
path: "/admin/login",
name: "AdminLogin",
component: AdminLogin,
},
{
path: "/admin/signup",
name: "AdminSignup",
component: AdminSignup,
},
{
path: "/admin/users",
name: "AdminUsersList",
component: AdminUsersList,
meta: {
requiresAuth: true,
},
},
{
path: '/admin/users/new',
name: 'AdminUsersCreate',
component: AdminUsersCreate,
meta: {
requiresAuth: true,
},
},
{
path: '/admin/users/:id',
name: 'AdminUsersForm',
component: AdminUsersForm,
meta: {
requiresAuth: true,
},
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
router.beforeEach((to, from, next) => {
const user = admin.getters.getUser;
if ((to.name == 'AdminLogin' || to.name == 'AdminSignup') && user !== null && !to.params.unauthorized) {
next({ name: 'AdminUsersList' });
return;
}
if (!to.meta.requiresAuth) {
next();
return;
}
if (user === null) {
next({ name: 'AdminLogin' });
return;
}
next();
});
export default router;

+ 28
- 0
Frontend/vue/src/store/admin/index.js View File

@ -0,0 +1,28 @@
import { createStore } from 'vuex';
import createPersistedState from "vuex-persistedstate";
export default createStore({
plugins: [createPersistedState()],
state: {
user: {},
},
mutations: {
UPDATE_USER(state, user ){
state.user = user
}
},
actions: {
setUser(context, user) {
context.commit('UPDATE_USER', user)
}
},
getters: {
getUser (state) {
return state.user;
}
}
})

+ 29
- 0
Frontend/vue/src/utils/http/index.js View File

@ -0,0 +1,29 @@
import axios from 'axios'
import router from '@/router'
import admin from '@/store/admin/index.js'
const instance = axios.create({
baseURL: "http://localhost:8080/api/v1/",
headers: {
"Content-Type": "application/json",
},
});
instance.interceptors.response.use(
function (response) {
return response;
},
function (error) {
if (error.response.status === 401) {
admin.dispatch('setUser', null)
router.push({ name: 'AdminLogin', params: { unauthorized: true } })
return
}
return Promise.reject(error);
}
);
export default instance

+ 4
- 0
Frontend/vue/vue.config.js View File

@ -0,0 +1,4 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})

+ 12
- 0
Makefile View File

@ -0,0 +1,12 @@
FLAC_FILES = $(shell find . -type d -not -path '*/\.git/*' -not -path '*\/.git' -not -path '.')
default: test build
build:
go build -o SuddenImpactRecords main
test:
for dir in ${FLAC_FILES}; do \
go test $$dir; \
done

+ 19
- 10
Models/Posts.go View File

@ -6,10 +6,11 @@ import (
type Post struct {
Base
Title string `gorm:"not null" json:"title"`
Content string `gorm:"not null" json:"content"`
FrontPage bool `gorm:"not null;type:boolean" json:"front_page"`
Order int `gorm:"not null" json:"order"`
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"`
Title string `gorm:"not null" json:"title"`
Content string `gorm:"not null" json:"content"`
FrontPage bool `gorm:"not null;type:boolean" json:"front_page"`
Order int `gorm:"not null" json:"order"`
PostLinks []PostLink `json:"links"`
PostImages []PostImage `json:"images"`
@ -29,25 +30,33 @@ const (
type PostLink struct {
Base
PostID uuid.UUID `gorm:"type:uuid;column:post_foreign_key;not null;" json:"post_id"`
PostID uuid.UUID `gorm:"type:uuid;column:post_id;not null;" json:"post_id"`
Link string `gorm:"not null" json:"link"`
Type PostLinkType `gorm:"not null" json:"type"`
}
type PostImage struct {
Base
PostID uuid.UUID `gorm:"type:uuid;column:post_foreign_key;not null;" json:"post_id"`
PostID uuid.UUID `gorm:"type:uuid;column:post_id;not null;" json:"post_id"`
Filepath string `gorm:"not null" json:"filepath"`
Mimetype string `gorm:"not null" json:"mimetype"`
Size int64 `gorm:"not null"`
}
type PostVideo struct {
Base
PostID uuid.UUID `gorm:"type:uuid;column:post_foreign_key;not null;" json:"post_id"`
Filepath string `gorm:"not null" json:"filepath"`
PostID uuid.UUID `gorm:"type:uuid;column:post_id;not null;" json:"post_id"`
Filepath string `json:"filepath"`
Mimetype string `json:"mimetype"`
Size int64 `json:"size"`
Url string `json:"url"`
}
type PostAudio struct {
Base
PostID uuid.UUID `gorm:"type:uuid;column:post_foreign_key;not null;" json:"post_id"`
Filepath string `gorm:"not null" json:"filepath"`
PostID uuid.UUID `gorm:"type:uuid;column:post_id;not null;" json:"post_id"`
Filepath string `json:"filepath"`
Mimetype string `json:"mimetype"`
Size int64 `json:"size"`
Url string `json:"url"`
}

+ 26
- 0
Models/Users.go View File

@ -0,0 +1,26 @@
package Models
import (
"time"
"gorm.io/gorm"
)
// Prevent updating the email if it has not changed
// This stops a unique constraint error
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
if !tx.Statement.Changed("Email") {
tx.Statement.Omit("Email")
}
return nil
}
type User struct {
Base
Email string `gorm:"not null;unique" json:"email"`
Password string `gorm:"not null" json:"password,omitempty"`
ConfirmPassword string `gorm:"-" json:"confirm_password"`
LastLogin *time.Time `json:"last_login"`
FirstName string `gorm:"not null" json:"first_name"`
LastName string `gorm:"not null" json:"last_name"`
}

+ 10
- 0
Util/EmailValidation.go View File

@ -0,0 +1,10 @@
package Util
import (
"net/mail"
)
func IsEmailValid(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
}

+ 67
- 0
Util/Files.go View File

@ -0,0 +1,67 @@
package Util
import (
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/gabriel-vasile/mimetype"
)
type FileObject struct {
Filepath string
Mimetype string
Size int64
}
func WriteFile(fileBytes []byte, acceptedMime string) (FileObject, error) {
var (
mime *mimetype.MIME
mimeSplit []string
file *os.File
fi os.FileInfo
fileObject FileObject
err error
)
mime = mimetype.Detect(fileBytes)
mimeSplit = strings.Split(mime.String(), "/")
if mimeSplit[0] != acceptedMime {
return fileObject, errors.New("Invalid filetype provided")
}
file, err = ioutil.TempFile(
fmt.Sprintf(
"./Frontend/public/%ss/",
mimeSplit[0],
),
fmt.Sprintf(
"%ss-*%s",
mimeSplit[0],
mime.Extension(),
),
)
if err != nil {
return fileObject, err
}
defer file.Close()
_, err = file.Write(fileBytes)
fi, err = file.Stat()
if err != nil {
return fileObject, err
}
fileObject = FileObject{
Filepath: file.Name(),
Mimetype: mime.String(),
Size: fi.Size(),
}
return fileObject, err
}

+ 51
- 0
Util/PostHelper.go View File

@ -0,0 +1,51 @@
package Util
import (
"errors"
"log"
"net/http"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"github.com/gorilla/mux"
)
func GetPostId(r *http.Request) (string, error) {
var (
urlVars map[string]string
id string
ok bool
)
urlVars = mux.Vars(r)
id, ok = urlVars["postID"]
if !ok {
return id, errors.New("Could not get id")
}
return id, nil
}
func GetPostById(w http.ResponseWriter, r *http.Request) (Models.Post, error) {
var (
postData Models.Post
id string
err error
)
id, err = GetPostId(r)
if err != nil {
log.Printf("Error encountered getting id\n")
JsonReturn(w, 500, "An error occured")
return postData, err
}
postData, err = Database.GetPostById(id)
if err != nil {
log.Printf("Could not find pet with id %s\n", id)
JsonReturn(w, 404, "Not found")
return postData, err
}
return postData, nil
}

+ 50
- 0
Util/PostImageHelper.go View File

@ -0,0 +1,50 @@
package Util
import (
"errors"
"log"
"net/http"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"github.com/gorilla/mux"
)
func GetPostImageId(r *http.Request) (string, error) {
var (
urlVars map[string]string
id string
ok bool
)
urlVars = mux.Vars(r)
id, ok = urlVars["imageID"]
if !ok {
return id, errors.New("Could not get id")
}
return id, nil
}
func GetPostImageById(w http.ResponseWriter, r *http.Request) (Models.PostImage, error) {
var (
postImageData Models.PostImage
id string
err error
)
id, err = GetPostImageId(r)
if err != nil {
log.Printf("Error encountered getting id\n")
JsonReturn(w, 500, "An error occured")
return postImageData, err
}
postImageData, err = Database.GetPostImageById(id)
if err != nil {
log.Printf("Could not find pet with id %s\n", id)
JsonReturn(w, 404, "Not found")
return postImageData, err
}
return postImageData, nil
}

Api/ReturnJson.go → Util/ReturnJson.go View File


+ 21
- 0
Util/Strings.go View File

@ -0,0 +1,21 @@
package Util
import (
"math/rand"
)
var (
letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
)
func RandomString(n int) string {
var (
b []rune
i int
)
b = make([]rune, n)
for i = range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}

+ 51
- 0
Util/UserHelper.go View File

@ -0,0 +1,51 @@
package Util
import (
"errors"
"log"
"net/http"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"github.com/gorilla/mux"
)
func GetUserId(r *http.Request) (string, error) {
var (
urlVars map[string]string
id string
ok bool
)
urlVars = mux.Vars(r)
id, ok = urlVars["userID"]
if !ok {
return id, errors.New("Could not get id")
}
return id, nil
}
func GetUserById(w http.ResponseWriter, r *http.Request) (Models.User, error) {
var (
postData Models.User
id string
err error
)
id, err = GetUserId(r)
if err != nil {
log.Printf("Error encountered getting id\n")
JsonReturn(w, 500, "An error occured")
return postData, err
}
postData, err = Database.GetUserById(id)
if err != nil {
log.Printf("Could not find pet with id %s\n", id)
JsonReturn(w, 404, "Not found")
return postData, err
}
return postData, nil
}

+ 2
- 0
go.mod View File

@ -4,6 +4,7 @@ go 1.17
require (
github.com/Kangaroux/go-map-schema v0.6.1
github.com/gabriel-vasile/mimetype v1.4.0
github.com/gofrs/uuid v4.2.0+incompatible
github.com/gorilla/mux v1.8.0
gorm.io/driver/postgres v1.3.1
@ -22,5 +23,6 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 // indirect
golang.org/x/text v0.3.7 // indirect
)

+ 5
- 0
go.sum View File

@ -11,6 +11,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -144,6 +146,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -156,6 +160,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=


+ 23
- 4
main.go View File

@ -1,24 +1,43 @@
package main
import (
"flag"
"net/http"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database/Seeder"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Frontend"
"github.com/gorilla/mux"
)
var (
seed bool
)
func init() {
Database.Init()
flag.BoolVar(&seed, "seed", false, "Seed database for development")
flag.Parse()
}
func main() {
var (
router *mux.Router
)
Database.Init()
if seed {
Seeder.Seed()
return
}
router = mux.NewRouter()
router = Api.InitApiEndpoints()
Api.InitApiEndpoints(router)
Frontend.InitFrontendRoutes(router)
// TODO: Run this within goroutine when running vue application
// Start and listen to requests
http.ListenAndServe(":8080", router)
}

Loading…
Cancel
Save