@ -0,0 +1,2 @@ | |||||
/Frontend/public/images/* | |||||
/Frontend/vue/node_modules |
@ -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) | |||||
} |
@ -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 | |||||
} | |||||
} |
@ -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(), | |||||
}) | |||||
} |
@ -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 | |||||
} | |||||
} |
@ -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) | |||||
} |
@ -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 | |||||
} |
@ -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 | |||||
} |
@ -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) | |||||
} |
@ -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 | |||||
} | |||||
} |
@ -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 | |||||
} |
@ -0,0 +1,128 @@ | |||||
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, | |||||
PublicFilepath: fileObject.PublicFilepath, | |||||
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) | |||||
} |
@ -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) | |||||
} | |||||
} |
@ -0,0 +1,246 @@ | |||||
package Api | |||||
import ( | |||||
"encoding/json" | |||||
"io/ioutil" | |||||
"log" | |||||
"net/http" | |||||
"net/url" | |||||
"strconv" | |||||
"time" | |||||
"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 getPosts(w http.ResponseWriter, r *http.Request) { | |||||
var ( | |||||
posts []Models.Post | |||||
returnJson []byte | |||||
values url.Values | |||||
page, pageSize int | |||||
search string | |||||
err error | |||||
) | |||||
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") | |||||
posts, err = Database.GetPosts(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(posts) == 0 { | |||||
Util.JsonReturn(w, 404, "No more data") | |||||
return | |||||
} | |||||
returnJson, err = json.MarshalIndent(posts, "", " ") | |||||
if err != nil { | |||||
Util.JsonReturn(w, 500, "An error occured") | |||||
return | |||||
} | |||||
// Return updated json | |||||
w.WriteHeader(http.StatusOK) | |||||
w.Write(returnJson) | |||||
} | |||||
func getPost(w http.ResponseWriter, r *http.Request) { | |||||
var ( | |||||
postData Models.Post | |||||
returnJson []byte | |||||
err error | |||||
) | |||||
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 createPost(w http.ResponseWriter, r *http.Request) { | |||||
var ( | |||||
requestBody []byte | |||||
postData Models.Post | |||||
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 | |||||
} | |||||
postData, err = JsonSerialization.DeserializePost(requestBody, []string{ | |||||
"id", | |||||
"links", | |||||
"images", | |||||
"videos", | |||||
"audios", | |||||
}, false) | |||||
if err != nil { | |||||
log.Printf("Invalid data provided to posts API: %s\n", err.Error()) | |||||
Util.JsonReturn(w, 405, "Invalid data") | |||||
return | |||||
} | |||||
err = Database.CreatePost(&postData) | |||||
if err != nil { | |||||
Util.JsonReturn(w, 405, "Invalid data") | |||||
} | |||||
returnJson, err = json.MarshalIndent(postData, "", " ") | |||||
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 updatePost(w http.ResponseWriter, r *http.Request) { | |||||
var ( | |||||
postData Models.Post | |||||
requestBody []byte | |||||
returnJson []byte | |||||
id string | |||||
err error | |||||
) | |||||
id, err = Util.GetPostId(r) | |||||
if err != nil { | |||||
log.Printf("Error encountered getting id\n") | |||||
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()) | |||||
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()) | |||||
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()) | |||||
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()) | |||||
Util.JsonReturn(w, 500, "An error occured") | |||||
return | |||||
} | |||||
// Return updated json | |||||
w.WriteHeader(http.StatusOK) | |||||
w.Write(returnJson) | |||||
} | |||||
func publishPost(w http.ResponseWriter, r *http.Request) { | |||||
var ( | |||||
now time.Time = time.Now() | |||||
postData Models.Post | |||||
returnJson []byte | |||||
err error | |||||
) | |||||
postData, err = Util.GetPostById(w, r) | |||||
if err != nil { | |||||
log.Printf("Error encountered getting id\n") | |||||
Util.JsonReturn(w, 500, "An error occured") | |||||
return | |||||
} | |||||
if postData.PublishedAt == nil { | |||||
postData.PublishedAt = &now | |||||
} else { | |||||
postData.PublishedAt = nil | |||||
} | |||||
postData, err = Database.UpdatePost(postData.ID.String(), &postData) | |||||
if err != nil { | |||||
log.Printf("An error occured: %s\n", err.Error()) | |||||
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()) | |||||
Util.JsonReturn(w, 500, "An error occured") | |||||
return | |||||
} | |||||
// Return updated json | |||||
w.WriteHeader(http.StatusOK) | |||||
w.Write(returnJson) | |||||
} | |||||
func deletePost(w http.ResponseWriter, r *http.Request) { | |||||
var ( | |||||
postData Models.Post | |||||
err error | |||||
) | |||||
postData, err = Util.GetPostById(w, r) | |||||
if err != nil { | |||||
return | |||||
} | |||||
err = Database.DeletePost(&postData) | |||||
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) | |||||
} |
@ -0,0 +1,230 @@ | |||||
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 | |||||
) | |||||
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 | |||||
) | |||||
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 ( | |||||
userData Models.User | |||||
requestBody []byte | |||||
returnJson []byte | |||||
id string | |||||
err error | |||||
) | |||||
id, err = Util.GetUserId(r) | |||||
if err != nil { | |||||
log.Printf("Error encountered reading POST body: %s\n", err.Error()) | |||||
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()) | |||||
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(id, &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 | |||||
) | |||||
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) | |||||
} |
@ -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) | |||||
} | |||||
} |
@ -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 | |||||
} |
@ -0,0 +1,97 @@ | |||||
package Database | |||||
import ( | |||||
"fmt" | |||||
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" | |||||
"gorm.io/gorm" | |||||
"gorm.io/gorm/clause" | |||||
) | |||||
func GetPosts(page, pageSize int, search string) ([]Models.Post, error) { | |||||
var ( | |||||
posts []Models.Post | |||||
query *gorm.DB | |||||
offset 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.Post{}). | |||||
Preload(clause.Associations). | |||||
Offset(offset). | |||||
Limit(pageSize). | |||||
Order("created_at desc") | |||||
if search != "%%" { | |||||
query = query. | |||||
Where("title LIKE ?", search). | |||||
Or("content LIKE ?", search) | |||||
} | |||||
err = query. | |||||
Find(&posts). | |||||
Error | |||||
return posts, err | |||||
} | |||||
func GetPostById(id string) (Models.Post, error) { | |||||
var ( | |||||
postData Models.Post | |||||
err error | |||||
) | |||||
err = DB.Preload(clause.Associations). | |||||
First(&postData, "id = ?", id). | |||||
Error | |||||
return postData, err | |||||
} | |||||
func CreatePost(postData *Models.Post) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Create(postData). | |||||
Error | |||||
} | |||||
func UpdatePost(id string, postData *Models.Post) (Models.Post, error) { | |||||
var ( | |||||
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 | |||||
} | |||||
return GetPostById(id) | |||||
} | |||||
func DeletePost(postData *Models.Post) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Delete(postData). | |||||
Error | |||||
} |
@ -0,0 +1,55 @@ | |||||
package Seeder | |||||
import ( | |||||
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database" | |||||
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" | |||||
) | |||||
func createPost(userData Models.User) (Models.Post, error) { | |||||
var ( | |||||
postData Models.Post | |||||
err error | |||||
) | |||||
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 SeedPosts() { | |||||
var ( | |||||
userData Models.User | |||||
i int | |||||
err error | |||||
) | |||||
err = Database.DB. | |||||
Model(Models.User{}). | |||||
First(&userData). | |||||
Error | |||||
if err != nil { | |||||
panic(err) | |||||
} | |||||
for i = 0; i <= 20; i++ { | |||||
_, err = createPost(userData) | |||||
if err != nil { | |||||
panic(err) | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,10 @@ | |||||
package Seeder | |||||
import "log" | |||||
func Seed() { | |||||
log.Println("Seeding users...") | |||||
SeedUsers() | |||||
log.Println("Seeding posts...") | |||||
SeedPosts() | |||||
} |
@ -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(10)) | |||||
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) | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,148 @@ | |||||
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 | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = DB.Model(Models.User{}). | |||||
Where("id = ?", id). | |||||
First(userData). | |||||
Error | |||||
userData.Password = "" | |||||
return err | |||||
} | |||||
func DeleteUser(userData *Models.User) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Delete(userData). | |||||
Error | |||||
} |
@ -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") | |||||
} |
@ -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 | |||||
} |
@ -0,0 +1,56 @@ | |||||
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/posts", | |||||
} | |||||
) | |||||
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("/public/"). | |||||
Handler(http.StripPrefix( | |||||
"/public/", | |||||
http.FileServer(http.Dir("./Frontend/public/")), | |||||
)) | |||||
router.PathPrefix("/").Handler(frontendFS) | |||||
} |
@ -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? |
@ -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/). |
@ -0,0 +1,5 @@ | |||||
module.exports = { | |||||
presets: [ | |||||
'@vue/cli-plugin-babel/preset' | |||||
] | |||||
} |
@ -0,0 +1,19 @@ | |||||
{ | |||||
"compilerOptions": { | |||||
"target": "es5", | |||||
"module": "esnext", | |||||
"baseUrl": "./", | |||||
"moduleResolution": "node", | |||||
"paths": { | |||||
"@/*": [ | |||||
"src/*" | |||||
] | |||||
}, | |||||
"lib": [ | |||||
"esnext", | |||||
"dom", | |||||
"dom.iterable", | |||||
"scripthost" | |||||
] | |||||
} | |||||
} |
@ -0,0 +1,64 @@ | |||||
{ | |||||
"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": { | |||||
"@fortawesome/fontawesome-svg-core": "^6.1.1", | |||||
"@fortawesome/free-brands-svg-icons": "^6.1.1", | |||||
"@fortawesome/free-regular-svg-icons": "^6.1.1", | |||||
"@fortawesome/free-solid-svg-icons": "^6.1.1", | |||||
"@fortawesome/vue-fontawesome": "^3.0.0-5", | |||||
"@meforma/vue-toaster": "^1.3.0", | |||||
"@tiptap/starter-kit": "^2.0.0-beta.183", | |||||
"@tiptap/vue-3": "^2.0.0-beta.90", | |||||
"@vee-validate/rules": "^4.5.10", | |||||
"@vuepic/vue-datepicker": "^3.0.0", | |||||
"axios": "^0.26.1", | |||||
"bootstrap": "^5.1.3", | |||||
"bootstrap-vue-3": "^0.1.10", | |||||
"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" | |||||
] | |||||
} |
@ -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> |
@ -0,0 +1,12 @@ | |||||
<template> | |||||
<router-view /> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'App', | |||||
} | |||||
</script> | |||||
<style> | |||||
</style> |
@ -0,0 +1,157 @@ | |||||
body { | |||||
min-height: 100vh; | |||||
} | |||||
.nav-link svg { | |||||
padding-right: 0.2rem; | |||||
} | |||||
#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; | |||||
} | |||||
.editor-form .input-group { | |||||
display: inline-block; | |||||
width: unset; | |||||
padding-right: 0.3rem; | |||||
} | |||||
.ProseMirror { | |||||
outline: 0; | |||||
height: 100%; | |||||
} | |||||
.ProseMirror p:last-child { | |||||
margin-bottom: 0; | |||||
} | |||||
.image-button-overlay { | |||||
position: relative; | |||||
top: 3rem; | |||||
z-index: 3; | |||||
width: 100%; | |||||
text-align: right; | |||||
padding-right: 1rem; | |||||
} | |||||
.image-delete { | |||||
color: var(--bs-danger); | |||||
z-index: 3; | |||||
font-size: 1.6rem; | |||||
height: 2rem; | |||||
width: 2rem; | |||||
border-radius: 50%; | |||||
} | |||||
.image-delete:hover { | |||||
background-color: white; | |||||
} |
@ -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> |
@ -0,0 +1,204 @@ | |||||
<template> | |||||
<div class="form-outline editor-form" v-if="editor"> | |||||
<div class="mb-3"> | |||||
<div class="input-group"> | |||||
<button | |||||
@click="editor.chain().focus().toggleBold().run()" | |||||
:class="editor.isActive('bold') ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
<i class="fa-solid fa-bold"></i> | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().toggleItalic().run()" | |||||
:class="editor.isActive('italic') ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
<i class="fa-solid fa-italic"></i> | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().toggleStrike().run()" | |||||
:class="editor.isActive('strike') ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
<i class="fa-solid fa-strikethrough"></i> | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().toggleCode().run()" | |||||
:class="editor.isActive('code') ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
<i class="fa-solid fa-code"></i> | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().setParagraph().run()" | |||||
:class="editor.isActive('paragraph') ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
<i class="fa-solid fa-paragraph"></i> | |||||
</button> | |||||
</div> | |||||
<div class="input-group"> | |||||
<button | |||||
@click="editor.chain().focus().toggleHeading({level: 1 }).run()" | |||||
:class="editor.isActive('heading', { level: 1 }) ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
H1 | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().toggleHeading({level: 2 }).run()" | |||||
:class="editor.isActive('heading', { level: 2 }) ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
H2 | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().toggleHeading({level: 3 }).run()" | |||||
:class="editor.isActive('heading', { level: 3 }) ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
H3 | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().toggleHeading({level: 4 }).run()" | |||||
:class="editor.isActive('heading', { level: 4 }) ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
H4 | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()" | |||||
:class="editor.isActive('heading', { level: 5 }) ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
H5 | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()" | |||||
:class="editor.isActive('heading', { level: 6 }) ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
H6 | |||||
</button> | |||||
</div> | |||||
<div class="input-group"> | |||||
<button | |||||
@click="editor.chain().focus().toggleBulletList().run()" | |||||
:class="editor.isActive('bulletList') ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
<i class="fa-solid fa-list"></i> | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().toggleOrderedList().run()" | |||||
:class="editor.isActive('orderedList') ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
<i class="fa-solid fa-list-ol"></i> | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().toggleCodeBlock().run()" | |||||
:class="editor.isActive('codeBlock') ? 'btn-dark' : 'btn-outline-dark'" | |||||
type="button" | |||||
class="btn" | |||||
> | |||||
<i class="fa-solid fa-file-code"></i> | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().setHorizontalRule().run()" | |||||
type="button" | |||||
class="btn btn-outline-dark" | |||||
> | |||||
<i class="fa-solid fa-ruler-horizontal"></i> | |||||
</button> | |||||
</div> | |||||
<div class="input-group"> | |||||
<button | |||||
@click="editor.chain().focus().undo().run()" | |||||
type="button" | |||||
class="btn btn-outline-dark" | |||||
> | |||||
<i class="fa-solid fa-rotate-left"></i> | |||||
</button> | |||||
<button | |||||
@click="editor.chain().focus().redo().run()" | |||||
type="button" | |||||
class="btn btn-outline-dark" | |||||
> | |||||
<i class="fa-solid fa-rotate-right"></i> | |||||
</button> | |||||
</div> | |||||
</div> | |||||
<editor-content :editor="editor" class="form-control"/> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { Editor, EditorContent } from '@tiptap/vue-3' | |||||
import StarterKit from '@tiptap/starter-kit' | |||||
export default { | |||||
props: { | |||||
modelValue: { | |||||
type: String, | |||||
default: '', | |||||
}, | |||||
}, | |||||
data () { | |||||
return { | |||||
editor: null, | |||||
} | |||||
}, | |||||
components: { | |||||
EditorContent, | |||||
}, | |||||
watch: { | |||||
modelValue(value) { | |||||
// HTML | |||||
const isSame = this.editor.getHTML() === value | |||||
// JSON | |||||
// const isSame = JSON.stringify(this.editor.getJSON()) === JSON.stringify(value) | |||||
if (isSame) { | |||||
return | |||||
} | |||||
this.editor.commands.setContent(value, false) | |||||
}, | |||||
}, | |||||
mounted() { | |||||
this.editor = new Editor({ | |||||
extensions: [ | |||||
StarterKit, | |||||
], | |||||
content: this.modelValue, | |||||
onUpdate: () => { | |||||
// HTML | |||||
this.$emit('update:modelValue', this.editor.getHTML()) | |||||
// JSON | |||||
// this.$emit('update:modelValue', this.editor.getJSON()) | |||||
}, | |||||
}) | |||||
}, | |||||
beforeUnmount() { | |||||
this.editor.destroy() | |||||
}, | |||||
} | |||||
</script> |
@ -0,0 +1,61 @@ | |||||
<template> | |||||
<div class="row mb-3"> | |||||
<div class="col-12"> | |||||
<div class="page-nav-container"> | |||||
<div class="row"> | |||||
<div class="col-md-6 col-10"> | |||||
<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="searchFunction" | |||||
> | |||||
<i class="fa-solid fa-magnifying-glass"></i> | |||||
</button> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="col-md-6 float-right col-2"> | |||||
<div class="btn-group float-right" role="group"> | |||||
<router-link :to="{ name: addNewTo }"> | |||||
<button | |||||
type="button" | |||||
class="btn btn-rounded btn-dark d-none d-md-inline-block" | |||||
> | |||||
<i class="fa-solid fa-plus"></i> | |||||
{{ addNewLabel }} | |||||
</button> | |||||
<button | |||||
type="button" | |||||
class="btn btn-rounded btn-dark d-inline-block d-md-none" | |||||
> | |||||
<i class="fa-solid fa-plus"></i> | |||||
</button> | |||||
</router-link> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
props: { | |||||
addNewTo: { type: String }, | |||||
addNewLabel: { type: String }, | |||||
searchFunction: { type: Function }, | |||||
} | |||||
} | |||||
</script> |
@ -0,0 +1,155 @@ | |||||
<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: 'AdminPostsList' }" | |||||
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: 'AdminPostsList' }" | |||||
class="nav-link" | |||||
aria-current="page" | |||||
> | |||||
<i class="fa-solid fa-file"></i> | |||||
Posts | |||||
</router-link> | |||||
</li> | |||||
<li class="nav-item rounded"> | |||||
<router-link | |||||
:to="{ name: 'AdminUsersList' }" | |||||
class="nav-link" | |||||
aria-current="page" | |||||
> | |||||
<i class="fa-solid fa-users"></i> | |||||
Users | |||||
</router-link> | |||||
</li> | |||||
<li class="nav-item rounded d-md-none"> | |||||
<a | |||||
href="#" | |||||
class="nav-link" | |||||
type="button" | |||||
data-bs-toggle="collapse" | |||||
data-bs-target="#navbarDropdown" | |||||
aria-controls="navbarNav" | |||||
aria-expanded="false" | |||||
aria-label="Toggle navigation" | |||||
> | |||||
<i class="fa-solid fa-user"></i> | |||||
Profile | |||||
</a> | |||||
<!-- | |||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" data-bs-target="navbarDropdown" aria-expanded="false"> | |||||
<i class="fa-solid fa-user"></i> | |||||
Profile | |||||
</a> | |||||
--> | |||||
<div class="collapse d-md-none" id="navbarDropdown"> | |||||
<ul class="navbar-nav ms-md-auto gap-2"> | |||||
<li class="nav-item rounded nav-item-indented"> | |||||
<router-link | |||||
v-if="$store.getters.getUser" | |||||
:to="{ name: 'AdminUsersForm', params: { id: $store.getters.getUser.id } }" | |||||
class="nav-link nav-link-indented" | |||||
> | |||||
Account | |||||
</router-link> | |||||
</li> | |||||
<li class="nav-item rounded nav-item-indented"> | |||||
<a href="#" v-on:click="logout" class="nav-link nav-link-indented">Logout</a> | |||||
</li> | |||||
</ul> | |||||
</div> | |||||
</li> | |||||
<li class="nav-item dropdown rounded d-none d-md-block"> | |||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> | |||||
<i class="fa-solid fa-user"></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; | |||||
} | |||||
.nav-item { | |||||
padding-left: 1rem; | |||||
padding-right: 1rem; | |||||
} | |||||
.nav-item-indented { | |||||
padding-left: 2.5rem; | |||||
padding-right: 2.5rem; | |||||
} | |||||
.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> |
@ -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: 'AdminPostsList' }) | |||||
} | |||||
} catch (error) { | |||||
this.$toast.error('An error occured') | |||||
} | |||||
} | |||||
} | |||||
} | |||||
</script> |
@ -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> |
@ -0,0 +1,342 @@ | |||||
<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'" | |||||
> | |||||
Post Details | |||||
</button> | |||||
<button | |||||
type="button" | |||||
class="btn btn-rounded" | |||||
:class="tab === 'images' ? 'btn-dark' : 'btn-outline-dark'" | |||||
@click="tab = 'images'" | |||||
> | |||||
Images | |||||
</button> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="card shadow-2-strong card-registration" v-if="tab === 'details'"> | |||||
<div class="card-body p-4 p-md-5"> | |||||
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Update Post</h3> | |||||
<Form @submit="updatePost" v-slot="{ errors }"> | |||||
<div class="row"> | |||||
<div class="col-md-7 mb-4"> | |||||
<div class="form-outline"> | |||||
<Field | |||||
v-model="post.title" | |||||
type="text" | |||||
id="title" | |||||
name="Title" | |||||
class="form-control form-control-lg" | |||||
:class="errors['Title'] ? 'invalid' : ''" | |||||
rules="required"/> | |||||
<label v-if="!errors['Title']" class="form-label" for="title">Title</label> | |||||
<ErrorMessage name="Title" as="label" class="form-label" for="title"/> | |||||
</div> | |||||
</div> | |||||
<div class="col-md-3 mb-4"> | |||||
<div class="form-outline"> | |||||
<select | |||||
v-model="post.front_page" | |||||
id="front_page" | |||||
name="Front Page" | |||||
class="form-control form-control-lg form-select"> | |||||
<option :value="true">Yes</option> | |||||
<option :value="false">No</option> | |||||
</select> | |||||
<label class="form-label" for="front_page">Front Page</label> | |||||
</div> | |||||
</div> | |||||
<div class="col-md-2 mb-4" v-if="post.front_page"> | |||||
<div class="form-outline"> | |||||
<Field | |||||
v-model="post.order" | |||||
type="text" | |||||
id="order" | |||||
name="Order" | |||||
class="form-control form-control-lg"/> | |||||
<label class="form-label" for="order">Order</label> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="row"> | |||||
<div class="col-12 mb-4"> | |||||
<admin-editor | |||||
v-model="post.content" | |||||
/> | |||||
<label class="form-label" for="editor">Content</label> | |||||
</div> | |||||
</div> | |||||
<div class="row"> | |||||
<div class="col-md-4 mb-4 pb-2"> | |||||
<div class="form-outline"> | |||||
<date-picker | |||||
v-model="post.created_at" | |||||
format="dd/MM/yyyy, HH:mm" | |||||
disabled="disabled" | |||||
id="created_at"/> | |||||
<label class="form-label" for="created_at">Created At</label> | |||||
</div> | |||||
</div> | |||||
<div class="col-md-4 mb-4 pb-2"> | |||||
<div class="form-outline"> | |||||
<date-picker | |||||
v-model="post.updated_at" | |||||
format="dd/MM/yyyy, HH:mm" | |||||
disabled="disabled" | |||||
id="updated_at"/> | |||||
<label class="form-label" for="updated_at">Updated At</label> | |||||
</div> | |||||
</div> | |||||
<div class="col-md-4 mb-4 pb-2"> | |||||
<div class="form-outline"> | |||||
<date-picker | |||||
v-model="post.published_at" | |||||
format="dd/MM/yyyy, HH:mm" | |||||
disabled="disabled" | |||||
id="published_at"/> | |||||
<label class="form-label" for="published_at">Published At</label> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="mt-2 pt-2 right-align"> | |||||
<button class="btn btn-danger btn-md" type="button"> | |||||
Delete | |||||
</button> | |||||
<button class="btn btn-outline-dark btn-md" type="button" @click="publishPost"> | |||||
{{ publishedLabel }} | |||||
</button> | |||||
<button class="btn btn-primary btn-md" type="submit"> | |||||
Update | |||||
</button> | |||||
</div> | |||||
</Form> | |||||
</div> | |||||
</div> | |||||
<div class="card shadow-2-strong card-registration" v-if="tab === 'images'"> | |||||
<div class="card-body p-4 p-md-5"> | |||||
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Upload Images</h3> | |||||
<form @submit.prevent="onUpload"> | |||||
<div class="row"> | |||||
<div class="mb-3"> | |||||
<input | |||||
id="image-input" | |||||
class="form-control" | |||||
type="file" | |||||
accept="image/*" | |||||
multiple="multiple" | |||||
/> | |||||
</div> | |||||
<br/> | |||||
<div class="form-group mb-3 right-align"> | |||||
<button | |||||
type="button" | |||||
class="btn btn-sm btn-outline-danger" | |||||
@click="clearFiles" | |||||
> | |||||
Clear | |||||
</button> | |||||
<button class="btn btn-sm btn-outline-success">Upload</button> | |||||
</div> | |||||
</div> | |||||
<div class="row row-cols-1 row-cols-md-3" v-if="post.images.length"> | |||||
<div v-for="image in post.images" :key="image.id"> | |||||
<div class="image-button-overlay"> | |||||
<div @click="deleteImage(image.id)"> | |||||
<span | |||||
class="fa-solid fa-xmark image-delete" | |||||
></span> | |||||
</div> | |||||
</div> | |||||
<div class="col"> | |||||
<div class="card"> | |||||
<img :src="image.filepath" class="card-img-top"> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="text-center" v-if="!post.images.length"> | |||||
<p class="text-muted">Empty</p> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
</section> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import AdminNavbar from '@/components/admin/components/navbar/AdminNavbar' | |||||
import AdminEditor from '@/components/admin/components/editor/AdminEditor' | |||||
import { Form, Field, ErrorMessage } from 'vee-validate' | |||||
export default { | |||||
data() { | |||||
return { | |||||
tab: 'details', | |||||
post: {}, | |||||
images: [], | |||||
imageLabel: 'Choose File', | |||||
} | |||||
}, | |||||
components: { | |||||
AdminNavbar, | |||||
Form, | |||||
Field, | |||||
ErrorMessage, | |||||
AdminEditor, | |||||
}, | |||||
async mounted () { | |||||
this.getPost() | |||||
}, | |||||
computed: { | |||||
publishedLabel() { | |||||
return this.post.published_at === null ? 'Publish' : 'Unpublish' | |||||
} | |||||
}, | |||||
methods: { | |||||
async getPost () { | |||||
try { | |||||
const response = await this.axios.get(`/admin/post/${this.$route.params.id}`) | |||||
if (response.status === 200) { | |||||
this.post = response.data | |||||
} | |||||
} catch (error) { | |||||
this.$toast.error('An error occurred.') | |||||
} | |||||
}, | |||||
async updatePost () { | |||||
try { | |||||
let response = await this.axios.put( | |||||
`/admin/post/${this.$route.params.id}`, | |||||
{ | |||||
title: this.post.title, | |||||
content: this.post.content, | |||||
front_page: this.post.front_page, | |||||
order: this.post.order, | |||||
audios: this.post.audios, | |||||
images: this.post.images, | |||||
links: this.post.links, | |||||
videos: this.post.videos, | |||||
}, | |||||
) | |||||
if (response.status === 200) { | |||||
this.$toast.success('Successfully updated post.'); | |||||
this.post = response.data | |||||
} | |||||
} catch (error) { | |||||
this.$toast.error('An error occured'); | |||||
} | |||||
}, | |||||
async publishPost () { | |||||
try { | |||||
let response = await this.axios.get(`/admin/post/${this.$route.params.id}/publish`) | |||||
if (response.status === 200) { | |||||
this.post = response.data | |||||
this.$toast.success(this.post.published_at !== null ? 'Successfully published post.' : 'Successfully unpublished post.'); | |||||
} | |||||
} catch (error) { | |||||
this.$toast.error('An error occured'); | |||||
} | |||||
}, | |||||
async onUpload(event) { | |||||
let fd = new FormData() | |||||
let photos = event.target[0].files | |||||
if (photos.length === 0) { | |||||
alert('No Files') | |||||
return | |||||
} | |||||
for (let i = 0; i < photos.length; i++) { | |||||
fd.append('files', photos[i]) | |||||
} | |||||
let response = await this.axios.post( | |||||
`/admin/post/${this.$route.params.id}/image`, | |||||
fd, | |||||
{ | |||||
headers: { | |||||
'Content-Type': 'multipart/form-data', | |||||
} | |||||
} | |||||
) | |||||
if (response.status === 200) { | |||||
this.post = response.data | |||||
this.clearFiles() | |||||
} | |||||
}, | |||||
clearFiles () { | |||||
document.getElementById("image-input").value=null; | |||||
}, | |||||
async deleteImage(id) { | |||||
let response = await this.axios.delete( | |||||
`/admin/post/${this.$route.params.id}/image/${id}`, | |||||
) | |||||
if (response.status === 200) { | |||||
this.clearFiles() | |||||
const indexOfObject = this.post.images.findIndex(object => { | |||||
return object.id === id; | |||||
}); | |||||
this.post.images.splice(indexOfObject, 1); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
</script> |
@ -0,0 +1,144 @@ | |||||
<template> | |||||
<div id="admin-page-container"> | |||||
<admin-navbar/> | |||||
<div class="container table-responsive mt-5 pb-5"> | |||||
<admin-list-header | |||||
addNewTo="AdminUsersCreate" | |||||
addNewLabel="Add Post" | |||||
:searchFunction="searchPosts" | |||||
ref="listHeader" | |||||
/> | |||||
<div class="card shadow-2-strong card-registration"> | |||||
<table class="table table-striped"> | |||||
<thead class="thead-dark"> | |||||
<tr> | |||||
<th scope="col">Title</th> | |||||
<th scope="col">Front Page</th> | |||||
<th scope="col" class="d-none d-sm-table-cell">Created At</th> | |||||
<th scope="col" class="d-none d-sm-table-cell">Published At</th> | |||||
<th scope="col"></th> | |||||
</tr> | |||||
</thead> | |||||
<tbody> | |||||
<tr v-for="post in posts" :key="post.id"> | |||||
<td class="align-middle">{{ post.title }}</td> | |||||
<td class="align-middle">{{ post.front_page }}</td> | |||||
<td class="align-middle d-none d-sm-table-cell">{{ formatDate(post.created_at) }}</td> | |||||
<td v-if="post.published_at" class="align-middle d-none d-sm-table-cell">{{ formatDate(post.published_at) }}</td> | |||||
<td v-if="!post.published_at" class="align-middle d-none d-sm-table-cell">-</td> | |||||
<td class="align-middle"> | |||||
<router-link | |||||
:to="{ name: 'AdminPostsForm', params: { id: post.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/components/navbar/AdminNavbar' | |||||
import AdminListHeader from '@/components/admin/components/list/AdminListHeader.vue' | |||||
export default { | |||||
data() { | |||||
return { | |||||
posts: [], | |||||
page: 0, | |||||
pageSize: 15, | |||||
search: '', | |||||
dataEnd: false, | |||||
} | |||||
}, | |||||
components: { | |||||
AdminNavbar, | |||||
AdminListHeader, | |||||
}, | |||||
async created () { | |||||
try { | |||||
const response = await this.axios.get('/admin/me') | |||||
if (response.status !== 200) { | |||||
return | |||||
} | |||||
} catch (e) { | |||||
return | |||||
} | |||||
this.getInitialPosts() | |||||
this.getNextPosts() | |||||
}, | |||||
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; | |||||
minutes = minutes < 10 ? '0'+minutes : minutes; | |||||
const strTime = hours + ':' + minutes + ' ' + ampm; | |||||
return d.getDate() + "/" + (d.getMonth()+1) + "/" + d.getFullYear() + " " + strTime; | |||||
}, | |||||
async getInitialPosts () { | |||||
try { | |||||
const response = await this.axios.get( | |||||
`/admin/post?page=${this.page}&pageSize=${this.pageSize}&search=${this.search}` | |||||
) | |||||
if (response.status === 200) { | |||||
this.posts = response.data | |||||
} | |||||
} catch (error) { | |||||
if (error.response.status === 404) { | |||||
this.posts = {} | |||||
this.dataEnd = true | |||||
} | |||||
} | |||||
}, | |||||
async getNextPosts () { | |||||
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( | |||||
`/post?page=${this.page}&pageSize=${this.pageSize}&search=${this.search}` | |||||
) | |||||
if (response.status === 200) { | |||||
this.posts.push(...response.data) | |||||
} | |||||
} catch (error) { | |||||
console.log(error) | |||||
if (error.response.status === 404) { | |||||
this.dataEnd = true | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
searchPosts () { | |||||
this.search = this.$refs.listHeader.$refs.search.value | |||||
this.getInitialPosts() | |||||
} | |||||
} | |||||
} | |||||
</script> |
@ -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/components/navbar/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> |
@ -0,0 +1,280 @@ | |||||
<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-4 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 class="col-md-4 mb-4 pb-2"> | |||||
<div class="form-outline"> | |||||
<date-picker | |||||
v-model="user.created_at" | |||||
format="dd/MM/yyyy, HH:mm" | |||||
disabled="disabled" | |||||
id="created_at"/> | |||||
<label class="form-label" for="created_at">Created At</label> | |||||
</div> | |||||
</div> | |||||
<div class="col-md-4 mb-4 pb-2"> | |||||
<div class="form-outline"> | |||||
<date-picker | |||||
v-model="user.updated_at" | |||||
format="dd/MM/yyyy, HH:mm" | |||||
disabled="disabled" | |||||
id="updated_at"/> | |||||
<label class="form-label" for="updated_at">Updated At</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/components/navbar/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, | |||||
created_at: null, | |||||
updated_at: 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, | |||||
created_at: response.data.created_at, | |||||
updated_at: response.data.updated_at, | |||||
} | |||||
}, | |||||
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}`, | |||||
{ | |||||
first_name: this.user.first_name, | |||||
last_name: this.user.last_name, | |||||
email: this.user.email, | |||||
}, | |||||
) | |||||
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> |
@ -0,0 +1,136 @@ | |||||
<template> | |||||
<div id="admin-page-container"> | |||||
<admin-navbar/> | |||||
<div class="container table-responsive mt-5 pb-5"> | |||||
<admin-list-header | |||||
addNewTo="AdminUsersCreate" | |||||
addNewLabel="Add User" | |||||
:searchFunction="searchUsers" | |||||
ref="listHeader" | |||||
/> | |||||
<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-lg-table-cell">Last Login</th> | |||||
<th scope="col" class="d-none d-lg-table-cell">Created At</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-lg-table-cell">{{ formatDate(user.last_login) }}</td> | |||||
<td class="align-middle d-none d-lg-table-cell">{{ formatDate(user.created_at) }}</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/components/navbar/AdminNavbar' | |||||
import AdminListHeader from '@/components/admin/components/list/AdminListHeader' | |||||
export default { | |||||
data() { | |||||
return { | |||||
users: {}, | |||||
pageSize: 15, | |||||
page: 0, | |||||
search: '', | |||||
dataEnd: false, | |||||
} | |||||
}, | |||||
components: { | |||||
AdminNavbar, | |||||
AdminListHeader, | |||||
}, | |||||
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.listHeader.$refs.search.value | |||||
this.getInitialUsers() | |||||
} | |||||
} | |||||
} | |||||
</script> |
@ -0,0 +1,49 @@ | |||||
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 { library } from "@fortawesome/fontawesome-svg-core"; | |||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; | |||||
import { fas } from '@fortawesome/free-solid-svg-icons' | |||||
import { fab } from '@fortawesome/free-brands-svg-icons'; | |||||
import { far } from '@fortawesome/free-regular-svg-icons'; | |||||
import { dom } from "@fortawesome/fontawesome-svg-core"; | |||||
library.add(fas); | |||||
library.add(fab); | |||||
library.add(far); | |||||
dom.watch(); | |||||
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.min.js' | |||||
import '@vuepic/vue-datepicker/dist/main.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.component("font-awesome-icon", FontAwesomeIcon) | |||||
app.mount('#app') |
@ -0,0 +1,100 @@ | |||||
import { createWebHistory, createRouter } from "vue-router"; | |||||
import HelloWorld from "@/components/HelloWorld.vue"; | |||||
import AdminLogin from "@/components/admin/views/auth/AdminLogin.vue"; | |||||
import AdminSignup from "@/components/admin/views/auth/AdminSignup.vue"; | |||||
import AdminPostsList from "@/components/admin/views/posts/AdminPostsList.vue"; | |||||
import AdminPostsForm from "@/components/admin/views/posts/AdminPostsForm.vue"; | |||||
import AdminUsersList from "@/components/admin/views/users/AdminUsersList.vue"; | |||||
import AdminUsersCreate from "@/components/admin/views/users/AdminUsersCreate.vue"; | |||||
import AdminUsersForm from "@/components/admin/views/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/posts", | |||||
name: "AdminPostsList", | |||||
component: AdminPostsList, | |||||
meta: { | |||||
requiresAuth: true, | |||||
}, | |||||
}, | |||||
{ | |||||
path: "/admin/posts/:id", | |||||
name: "AdminPostsForm", | |||||
component: AdminPostsForm, | |||||
meta: { | |||||
requiresAuth: true, | |||||
}, | |||||
}, | |||||
{ | |||||
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 ?? null; | |||||
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; |
@ -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; | |||||
} | |||||
} | |||||
}) |
@ -0,0 +1,30 @@ | |||||
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", | |||||
}, | |||||
withCredentials: true, | |||||
}); | |||||
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 |
@ -0,0 +1,4 @@ | |||||
const { defineConfig } = require('@vue/cli-service') | |||||
module.exports = defineConfig({ | |||||
transpileDependencies: true | |||||
}) |
@ -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 | |||||
@ -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"` | |||||
} |
@ -0,0 +1,10 @@ | |||||
package Util | |||||
import ( | |||||
"net/mail" | |||||
) | |||||
func IsEmailValid(email string) bool { | |||||
_, err := mail.ParseAddress(email) | |||||
return err == nil | |||||
} |
@ -0,0 +1,69 @@ | |||||
package Util | |||||
import ( | |||||
"errors" | |||||
"fmt" | |||||
"io/ioutil" | |||||
"os" | |||||
"strings" | |||||
"github.com/gabriel-vasile/mimetype" | |||||
) | |||||
type FileObject struct { | |||||
Filepath string | |||||
PublicFilepath 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(), | |||||
PublicFilepath: strings.ReplaceAll(file.Name(), "./Frontend", ""), | |||||
Mimetype: mime.String(), | |||||
Size: fi.Size(), | |||||
} | |||||
return fileObject, err | |||||
} |
@ -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 | |||||
} |
@ -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 | |||||
} |
@ -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) | |||||
} |
@ -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 | |||||
} |
@ -1,24 +1,43 @@ | |||||
package main | package main | ||||
import ( | import ( | ||||
"flag" | |||||
"net/http" | "net/http" | ||||
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api" | "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api" | ||||
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database" | "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database" | ||||
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database/Seeder" | |||||
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Frontend" | |||||
"github.com/gorilla/mux" | "github.com/gorilla/mux" | ||||
) | ) | ||||
var ( | |||||
seed bool | |||||
) | |||||
func init() { | |||||
Database.Init() | |||||
flag.BoolVar(&seed, "seed", false, "Seed database for development") | |||||
flag.Parse() | |||||
} | |||||
func main() { | func main() { | ||||
var ( | var ( | ||||
router *mux.Router | 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) | http.ListenAndServe(":8080", router) | ||||
} | } |