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