diff --git a/EduSync.exe b/EduSync.exe index 35d309a..9d5f093 100644 Binary files a/EduSync.exe and b/EduSync.exe differ diff --git a/adminHandler.go b/adminHandler.go new file mode 100644 index 0000000..421bc3c --- /dev/null +++ b/adminHandler.go @@ -0,0 +1,252 @@ +package main + +import ( + "encoding/json" + "html/template" + "net/http" + + "github.com/gorilla/mux" +) + +func AdminHandler(router *mux.Router) { + router.HandleFunc("/admin", func(res http.ResponseWriter, req *http.Request) { + t, err := template.ParseFiles("templates/admin/index.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, nil) + }).Methods("GET") + + router.HandleFunc("/admin/search_student", func(res http.ResponseWriter, req *http.Request) { + t, err := template.ParseFiles("templates/admin/search_student.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, nil) + }).Methods("GET") + + router.HandleFunc("/admin/api/search_student", func(res http.ResponseWriter, req *http.Request) { + name := req.URL.Query().Get("name") + class := req.URL.Query().Get("class") + students, err := searchStudents(name, class) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + res.Header().Set("Content-Type", "application/json") + json.NewEncoder(res).Encode(students) + }).Methods("GET") + + router.HandleFunc("/admin/student/{googleID}/edit", func(res http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + googleID := vars["googleID"] + + currentUser, err := GetCurrentUser(req) + if err != nil { + http.Error(res, "Unauthorized", http.StatusUnauthorized) + return + } + + student, err := readStudent(currentUser, googleID) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + t, err := template.ParseFiles("templates/admin/edit_student.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, student) + }).Methods("GET") + + router.HandleFunc("/admin/student/{googleID}", func(res http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + googleID := vars["googleID"] + + currentUser, err := GetCurrentUser(req) + if err != nil { + http.Error(res, "Unauthorized", http.StatusUnauthorized) + return + } + + switch req.Method { + case http.MethodGet: + student, err := readStudent(currentUser, googleID) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + res.Header().Set("Content-Type", "application/json") + json.NewEncoder(res).Encode(student) + case http.MethodPut: + var updates map[string]interface{} + if err := json.NewDecoder(req.Body).Decode(&updates); err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return + } + if err := updateStudent(currentUser, googleID, updates); err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + res.WriteHeader(http.StatusNoContent) + } + }).Methods("GET", "PUT") + + router.HandleFunc("/admin/search_parent", func(res http.ResponseWriter, req *http.Request) { + t, err := template.ParseFiles("templates/admin/search_parent.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, nil) + }).Methods("GET") + + router.HandleFunc("/admin/api/search_parent", func(res http.ResponseWriter, req *http.Request) { + name := req.URL.Query().Get("name") + parents, err := searchParents(name) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + res.Header().Set("Content-Type", "application/json") + json.NewEncoder(res).Encode(parents) + }).Methods("GET") + + router.HandleFunc("/admin/parent/{googleID}/edit", func(res http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + googleID := vars["googleID"] + + currentUser, err := GetCurrentUser(req) + if err != nil { + http.Error(res, "Unauthorized", http.StatusUnauthorized) + return + } + + parent, err := readParent(currentUser, googleID) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + t, err := template.ParseFiles("templates/admin/edit_parent.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, parent) + }).Methods("GET") + + router.HandleFunc("/admin/parent/{googleID}", func(res http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + googleID := vars["googleID"] + + currentUser, err := GetCurrentUser(req) + if err != nil { + http.Error(res, "Unauthorized", http.StatusUnauthorized) + return + } + + switch req.Method { + case http.MethodGet: + parent, err := readParent(currentUser, googleID) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + res.Header().Set("Content-Type", "application/json") + json.NewEncoder(res).Encode(parent) + case http.MethodPut: + var updates map[string]interface{} + if err := json.NewDecoder(req.Body).Decode(&updates); err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return + } + if err := updateParent(currentUser, googleID, updates); err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + res.WriteHeader(http.StatusNoContent) + } + }).Methods("GET", "PUT") + + router.HandleFunc("/admin/search_instructor", func(res http.ResponseWriter, req *http.Request) { + t, err := template.ParseFiles("templates/admin/search_instructor.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, nil) + }).Methods("GET") + + router.HandleFunc("/admin/api/search_instructor", func(res http.ResponseWriter, req *http.Request) { + name := req.URL.Query().Get("name") + instructors, err := searchInstructors(name) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + res.Header().Set("Content-Type", "application/json") + json.NewEncoder(res).Encode(instructors) + }).Methods("GET") + + router.HandleFunc("/admin/instructor/{googleID}/edit", func(res http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + googleID := vars["googleID"] + + currentUser, err := GetCurrentUser(req) + if err != nil { + http.Error(res, "Unauthorized", http.StatusUnauthorized) + return + } + + instructor, err := readInstructor(currentUser, googleID) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + t, err := template.ParseFiles("templates/admin/edit_instructor.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, instructor) + }).Methods("GET") + + router.HandleFunc("/admin/instructor/{googleID}", func(res http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + googleID := vars["googleID"] + + currentUser, err := GetCurrentUser(req) + if err != nil { + http.Error(res, "Unauthorized", http.StatusUnauthorized) + return + } + + switch req.Method { + case http.MethodGet: + instructor, err := readInstructor(currentUser, googleID) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + res.Header().Set("Content-Type", "application/json") + json.NewEncoder(res).Encode(instructor) + case http.MethodPut: + var updates map[string]interface{} + if err := json.NewDecoder(req.Body).Decode(&updates); err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return + } + if err := updateInstructor(currentUser, googleID, updates); err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + res.WriteHeader(http.StatusNoContent) + } + }).Methods("GET", "PUT") +} diff --git a/assets/css/style.css b/assets/css/style.css index a1349bb..2f13749 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -60,6 +60,20 @@ html { -webkit-text-size-adjust: 100%; /* 2 */ } +.content{ + font-size: 16px; + margin-bottom: 100px; +} + +.page-footer { + bottom: 0; +} + +.button-container { + display: flex; + gap: 10px; /* Adjust the gap between buttons as needed */ +} + /* Sections ========================================================================== */ diff --git a/authHandler.go b/authHandler.go index 54a459d..9a9cbbb 100644 --- a/authHandler.go +++ b/authHandler.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "html/template" "log" @@ -24,10 +25,7 @@ func AuthHandler(router *mux.Router, config *Config) { store.Options.Secure = isProd gothic.Store = store - - goth.UseProviders( - google.New(config.GoogleClientID, config.GoogleClientSecret, "https://localhost:8080/auth/google/callback", "email", "profile"), - ) + goth.UseProviders(google.New(config.GoogleClientID, config.GoogleClientSecret, "https://localhost:8080/auth/google/callback", "email", "profile")) router.HandleFunc("/auth/{provider}/callback", func(res http.ResponseWriter, req *http.Request) { user, err := gothic.CompleteUserAuth(res, req) @@ -36,35 +34,63 @@ func AuthHandler(router *mux.Router, config *Config) { return } - // Role assignment logic - var role string - switch user.Email { - case "admin@nk.com": - role = "Admin" - case "instructor@nk.com": - role = "Instructor" - case "parent@nk.com": - role = "Parent" - default: - role = "Student" - } - - // Create or update the user in Firebase with the assigned role - // student := NewStudent(user.UserID, user.Name, user.Email, "91234567", "TE", "Mr. Smith", "Jane Doe", role, 10, 10.0) - student := NewStudent(user.UserID, user.Name, user.Email, "91234567", "TE", "Jane Doe", role, 10, 10.0) - err = createStudent(student.User, student) + userObj, userRole, err := getUserRole(user.Email) if err != nil { - log.Println("Error creating student:", err) - } else { - log.Println("Student created/updated successfully!") + fmt.Fprintln(res, err) + return } + log.Println("User role:", userRole) - t, err := template.ParseFiles("templates/success.html") - if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return + // Only store the user object into the session if userRole is not an empty string + if userRole != "" { + // Create a User object with the user role + currentUser := User{ + GoogleID: user.UserID, + Name: user.Name, + Email: user.Email, + ContactNumber: userObj.ContactNumber, // Use contact number from the retrieved user object + Role: userObj.Role, + CreatedAt: userObj.CreatedAt, + UpdatedAt: userObj.UpdatedAt, + } + + // Serialize the user object to JSON + userData, err := json.Marshal(currentUser) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + // Get the session and store the user data + session, err := store.Get(req, "auth-session") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + session.Values["user"] = userData + err = session.Save(req, res) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + // Redirect based on user role + if userRole == "Admin" { + AdminHandler(router) + http.Redirect(res, req, "/admin", http.StatusFound) + } else if userRole == "Instructor" { + InstructorHandler(router) + http.Redirect(res, req, "/instructor", http.StatusFound) + } else if userRole == "Student" { + StudentHandler(router) + http.Redirect(res, req, "/student", http.StatusFound) + } else if userRole == "Parent" { + ParentHandler(router) + http.Redirect(res, req, "/parent", http.StatusFound) + } + } else { + http.Redirect(res, req, "/unregistered", http.StatusFound) } - t.Execute(res, user) }).Methods("GET") router.HandleFunc("/auth/{provider}", func(res http.ResponseWriter, req *http.Request) { diff --git a/database.go b/database.go index ef290f4..ff15f19 100644 --- a/database.go +++ b/database.go @@ -2,72 +2,49 @@ package main import ( "context" + "encoding/json" "fmt" - "net/http" - "log" - "os" - - "github.com/joho/godotenv" + "net/http" + "strings" firebase "firebase.google.com/go" "firebase.google.com/go/db" - "google.golang.org/api/option" + "github.com/gorilla/sessions" ) -// Use godot package to load/read the .env file and -// return the value of the key (for local env) -func goDotEnvVariable(key string) string { - - // load .env file - err := godotenv.Load(".env") +var firebaseClient *db.Client +var store = sessions.NewCookieStore([]byte("1L6x-SPtG8-EqJUkR7htTJx-5K4rt-ZTKeh-rxPw-AM=")) +func initDB(app *firebase.App) error { + // Initialize Firebase client + client, err := app.Database(context.Background()) if err != nil { - log.Fatalf("Error loading .env file") + return fmt.Errorf("error creating firebase DB client: %v", err) } - - return os.Getenv(key) + firebaseClient = client + return nil } -// Initialize Firebase client -var firebaseClient *db.Client - -// InitializeFirebase initializes the Firebase app and sets the global firebaseClient variable -func initializeFirebase() error { - ctx := context.Background() - - databaseURL, found := os.LookupEnv("DATABASE_URL") - if !found { - log.Fatalf("DATABASE_URL is not set in the environment variables") +// Utility function to get current user +func GetCurrentUser(req *http.Request) (User, error) { + session, err := store.Get(req, "auth-session") + if err != nil { + return User{}, err } - // databaseURL := goDotEnvVariable("DATABASE_URL") - // if databaseURL == "" { - // return fmt.Errorf("DATABASE_URL is not set in the environment variables") - // } - - conf := &firebase.Config{DatabaseURL: databaseURL} - - opt := option.WithCredentialsFile("edusync-7bd5e-firebase-adminsdk-x49uh-af084a6314.json") - // opt := option.WithCredentialsFile("edusync-test-firebase-adminsdk-hk5kl-9af0162b09.json") - app, err := firebase.NewApp(ctx, conf, opt) - if err != nil { - return fmt.Errorf("error initializing firebase app: %v", err) + userData, ok := session.Values["user"].([]byte) + if !ok || userData == nil { + return User{}, fmt.Errorf("user not found in session") } - client, err := app.Database(ctx) + var user User + err = json.Unmarshal(userData, &user) if err != nil { - return fmt.Errorf("error creating firebase DB client: %v", err) + return User{}, err } - firebaseClient = client - return nil -} - -// Utility function to get current user -func getCurrentUser(req *http.Request) (User, error) { - // Implement session or context based user retrieval - return User{}, nil + return user, nil } // Utility functions to check roles @@ -134,6 +111,48 @@ func isParentChildInClass(currentUser User, students []Student, class Class) boo return false } +func getUserRole(email string) (User, string, error) { + ctx := context.Background() + var user User + var userRole string + + // Check if firebaseClient is initialized + if firebaseClient == nil { + log.Println("Firebase client is not initialized") + return user, userRole, fmt.Errorf("firebase client is not initialized") + } + + // Map categories to Firebase references + categoryRefs := map[string]string{ + "Student": "/students", + "Parent": "/parents", + "Instructor": "/instructors", + "Admin": "/admins", + } + + // Iterate through each category and check if the email exists + for category, ref := range categoryRefs { + categoryRef := firebaseClient.NewRef(ref) + dataSnapshot, err := categoryRef.OrderByChild("email").EqualTo(email).GetOrdered(ctx) + if err != nil { + log.Printf("Error fetching data from %s: %v", category, err) + continue + } + + // Check if dataSnapshot has any children + if len(dataSnapshot) > 0 { + userRole = category + // Assuming dataSnapshot[0] is the first match and it contains the user data + if err := dataSnapshot[0].Unmarshal(&user); err != nil { + log.Printf("Error unmarshalling data for %s: %v", category, err) + continue + } + break + } + } + return user, userRole, nil +} + // CRUD operations with role checks // Student CRUD @@ -149,7 +168,18 @@ func createStudent(currentUser User, student Student) error { return ref.Set(context.TODO(), student) } -func readStudent(currentUser User, student Student, classes []Class) (Student, error) { +func readStudent(currentUser User, studentGoogleID string) (Student, error) { + ref := firebaseClient.NewRef("students/" + studentGoogleID) + var student Student + if err := ref.Get(context.TODO(), &student); err != nil { + return Student{}, fmt.Errorf("error reading student: %v", err) + } + + classes, err := readAllClasses(currentUser) + if err != nil { + return Student{}, fmt.Errorf("error reading classes: %v", err) + } + // If user is not an admin, instructor, or parent, return error when attempting to read student if !isAdmin(currentUser) && //not admin !(isSelf(currentUser, student.GoogleID) && isStudent(currentUser)) && //not student and reading self @@ -158,15 +188,53 @@ func readStudent(currentUser User, student Student, classes []Class) (Student, e return Student{}, fmt.Errorf("unauthorized access: you can only read your own details or the details of students you are authorized to access") } - ref := firebaseClient.NewRef("students/" + student.GoogleID) - // var student Student - if err := ref.Get(context.TODO(), &student); err != nil { - return Student{}, fmt.Errorf("error reading student: %v", err) - } return student, nil } -func updateStudent(currentUser User, student Student, classes []Class, updates map[string]interface{}) error { +func readStudents() ([]Student, error) { + var studentsMap map[string]Student + ref := firebaseClient.NewRef("students") + if err := ref.Get(context.TODO(), &studentsMap); err != nil { + return nil, fmt.Errorf("error reading students: %v", err) + } + // Convert map to slice + students := make([]Student, 0, len(studentsMap)) + for _, student := range studentsMap { + students = append(students, student) + } + return students, nil +} + +func searchStudents(name, class string) ([]Student, error) { + if name == "" && class == "" { + return readStudents() + } + students, err := readStudents() + if err != nil { + return nil, err + } + var filteredStudents []Student + for _, student := range students { + if name == "" || strings.Contains(strings.ToLower(student.Name), strings.ToLower(name)) { + filteredStudents = append(filteredStudents, student) + } + } + return filteredStudents, nil +} + +func updateStudent(currentUser User, studentGoogleID string, updates map[string]interface{}) error { + // Fetch the student information using the provided GoogleID + student, err := readStudent(currentUser, studentGoogleID) + if err != nil { + return fmt.Errorf("error fetching student: %v", err) + } + + // Fetch all classes + classes, err := readAllClasses(currentUser) + if err != nil { + return fmt.Errorf("error reading classes: %v", err) + } + // If user is not admin, instructor, or parent, return error when attempting to update student if !isAdmin(currentUser) && //not admin !(isSelf(currentUser, student.GoogleID) && isStudent(currentUser)) && //not student and reading self @@ -175,10 +243,11 @@ func updateStudent(currentUser User, student Student, classes []Class, updates m return fmt.Errorf("unauthorized access: you can only update your own details") } ref := firebaseClient.NewRef("students/" + student.GoogleID) + if err := ref.Update(context.TODO(), updates); err != nil { return fmt.Errorf("error updating student: %v", err) } - return ref.Update(context.TODO(), updates) + return nil } func deleteStudent(currentUser User, student Student) error { @@ -206,20 +275,60 @@ func createInstructor(currentUser User, instructor Instructor) error { return ref.Set(context.TODO(), instructor) } -func readInstructor(currentUser User, instructor Instructor) (Instructor, error) { +func readInstructor(currentUser User, instructorGoogleID string) (Instructor, error) { + ref := firebaseClient.NewRef("instructors/" + instructorGoogleID) + var instructor Instructor + if err := ref.Get(context.TODO(), &instructor); err != nil { + return Instructor{}, fmt.Errorf("error reading instructor: %v", err) + } + // If user is not admin or (self & instructor), return error when attempting to read instructor if !isAdmin(currentUser) && //not admin !(isSelf(currentUser, instructor.GoogleID) && isInstructor(currentUser)) { return Instructor{}, fmt.Errorf("unauthorized access: you can only read your own details") } - ref := firebaseClient.NewRef("instructors/" + instructor.GoogleID) - if err := ref.Get(context.TODO(), &instructor); err != nil { - return Instructor{}, fmt.Errorf("error reading instructor: %v", err) - } + return instructor, nil } -func updateInstructor(currentUser User, instructor Instructor, updates map[string]interface{}) error { +func readInstructors() ([]Instructor, error) { + var instructorsMap map[string]Instructor + ref := firebaseClient.NewRef("instructors") + if err := ref.Get(context.TODO(), &instructorsMap); err != nil { + return nil, fmt.Errorf("error reading students: %v", err) + } + // Convert map to slice + instructors := make([]Instructor, 0, len(instructorsMap)) + for _, instructor := range instructorsMap { + instructors = append(instructors, instructor) + } + return instructors, nil +} + +func searchInstructors(name string) ([]Instructor, error) { + if name == "" { + return readInstructors() + } + instructors, err := readInstructors() + if err != nil { + return nil, err + } + var filteredInstructors []Instructor + for _, instructor := range instructors { + if name == "" || strings.Contains(strings.ToLower(instructor.Name), strings.ToLower(name)) { + filteredInstructors = append(filteredInstructors, instructor) + } + } + return filteredInstructors, nil +} + +func updateInstructor(currentUser User, instructorGoogleID string, updates map[string]interface{}) error { + // Fetch the instructor information using the provided GoogleID + instructor, err := readInstructor(currentUser, instructorGoogleID) + if err != nil { + return fmt.Errorf("error fetching instructor: %v", err) + } + // If user is not admin or (self & instructor), return error when attempting to update instructor if !isAdmin(currentUser) && //not admin !(isSelf(currentUser, instructor.GoogleID) && isInstructor(currentUser)) { @@ -257,19 +366,27 @@ func createAdmin(currentUser User, admin Admin) error { return ref.Set(context.TODO(), admin) } -func readAdmin(currentUser User, admin Admin) (Admin, error) { +func readAdmin(currentUser User, adminGoogleID string) (Admin, error) { + ref := firebaseClient.NewRef("admins/" + adminGoogleID) + var admin Admin + if err := ref.Get(context.TODO(), &admin); err != nil { + return Admin{}, fmt.Errorf("error reading admin: %v", err) + } + // If user is not admin, return error when attempting to read admin if !isAdmin(currentUser) { return Admin{}, fmt.Errorf("unauthorized access: you can only read your own details") } - ref := firebaseClient.NewRef("admins/" + admin.GoogleID) - if err := ref.Get(context.TODO(), &admin); err != nil { - return Admin{}, fmt.Errorf("error reading admin: %v", err) - } return admin, nil } -func updateAdmin(currentUser User, admin Admin, updates map[string]interface{}) error { +func updateAdmin(currentUser User, adminGoogleID string, updates map[string]interface{}) error { + // Fetch the admin information using the provided GoogleID + admin, err := readAdmin(currentUser, adminGoogleID) + if err != nil { + return fmt.Errorf("error fetching admin: %v", err) + } + // If user is not admin, return error when attempting to update admin if !isAdmin(currentUser) { return fmt.Errorf("unauthorized access: you can only update your own details") @@ -306,21 +423,60 @@ func createParent(currentUser User, parent Parent) error { return ref.Set(context.TODO(), parent) } -func readParent(currentUser User, parent Parent) (Parent, error) { +func readParent(currentUser User, parentGoogleID string) (Parent, error) { + ref := firebaseClient.NewRef("parents/" + parentGoogleID) + var parent Parent + if err := ref.Get(context.TODO(), &parent); err != nil { + return Parent{}, fmt.Errorf("error reading parent: %v", err) + } + // If user is not admin or (self and parent), return error when attempting to update parent if !isAdmin(currentUser) && //not admin !(isSelf(currentUser, parent.GoogleID) && isParent(currentUser)) && //not parent and reading self !(currentUser.Role == "Student" && canChildAccessParent(currentUser, parent)) { return Parent{}, fmt.Errorf("unauthorized access: you can only read your own details") } - ref := firebaseClient.NewRef("parents/" + parent.GoogleID) - if err := ref.Get(context.TODO(), &parent); err != nil { - return Parent{}, fmt.Errorf("error reading parent: %v", err) - } return parent, nil } -func updateParent(currentUser User, parent Parent, updates map[string]interface{}) error { +func readParents() ([]Parent, error) { + var parentsMap map[string]Parent + ref := firebaseClient.NewRef("parents") + if err := ref.Get(context.TODO(), &parentsMap); err != nil { + return nil, fmt.Errorf("error reading students: %v", err) + } + // Convert map to slice + parents := make([]Parent, 0, len(parentsMap)) + for _, parent := range parentsMap { + parents = append(parents, parent) + } + return parents, nil +} + +func searchParents(name string) ([]Parent, error) { + if name == "" { + return readParents() + } + parents, err := readParents() + if err != nil { + return nil, err + } + var filteredParents []Parent + for _, parent := range parents { + if name == "" || strings.Contains(strings.ToLower(parent.Name), strings.ToLower(name)) { + filteredParents = append(filteredParents, parent) + } + } + return filteredParents, nil +} + +func updateParent(currentUser User, parentGoogleID string, updates map[string]interface{}) error { + // Fetch the parent information using the provided GoogleID + parent, err := readParent(currentUser, parentGoogleID) + if err != nil { + return fmt.Errorf("error fetching parent: %v", err) + } + if !isAdmin(currentUser) && //not admin !(isSelf(currentUser, parent.GoogleID) && isParent(currentUser)) { return fmt.Errorf("unauthorized access: you can only update your own details") @@ -345,7 +501,6 @@ func deleteParent(currentUser User, parent Parent) error { } // class CRUD - func createClass(currentUser User, class Class) error { // If user is not admin, return error when attempting to create class if !isAdmin(currentUser) { @@ -358,7 +513,13 @@ func createClass(currentUser User, class Class) error { return ref.Set(context.TODO(), class) } -func readClass(currentUser User, students []Student, class Class) (Class, error) { +func readClass(currentUser User, students []Student, classID string) (Class, error) { + ref := firebaseClient.NewRef("classes/" + classID) + var class Class + if err := ref.Get(context.TODO(), &class); err != nil { + return Class{}, fmt.Errorf("error reading class: %v", err) + } + // If user is not admin or (self and class), return error when attempting to read class if !isAdmin(currentUser) && //not admin !isInstructor(currentUser) && //not instructor @@ -366,13 +527,30 @@ func readClass(currentUser User, students []Student, class Class) (Class, error) !(isParent(currentUser) && isParentChildInClass(currentUser, students, class)) { return Class{}, fmt.Errorf("unauthorized access: you can only read your own details") } - ref := firebaseClient.NewRef("classes/" + class.ClassID) - if err := ref.Get(context.TODO(), &class); err != nil { - return Class{}, fmt.Errorf("error reading class: %v", err) - } return class, nil } +func readAllClasses(currentUser User) ([]Class, error) { + ref := firebaseClient.NewRef("classes") + + var classesMap map[string]Class + if err := ref.Get(context.TODO(), &classesMap); err != nil { + return nil, fmt.Errorf("error reading classes: %v", err) + } + + var classes []Class + for _, class := range classesMap { + // If user is not authorized to read the class, skip it + if !isAdmin(currentUser) && // not admin + !isInstructor(currentUser) { // not instructor + continue + } + classes = append(classes, class) + } + + return classes, nil +} + func updateClass(currentUser User, class Class, updates map[string]interface{}) error { // If user is not admin or (self and class), return error when attempting to update class if !isAdmin(currentUser) { @@ -396,3 +574,152 @@ func deleteClass(currentUser User, class Class) error { } return ref.Delete(context.TODO()) } + +// func database() { +// // Find home directory. +// // home, err := os.Getwd() +// // if err != nil { +// // return err +// // } + +// ctx := context.Background() + +// // configure database URL +// // databaseURL := goDotEnvVariable("DATABASE_URL") +// // if databaseURL == "" { +// // return fmt.Errorf("DATABASE_URL is not set in the .env file") +// // } +// // databaseURL, found := os.LookupEnv("DATABASE_URL") +// // if !found { +// // log.Fatalf("DATABASE_URL is not set in the environment variables") +// // } +// // conf := &firebase.Config{DatabaseURL: databaseURL} + +// conf := &firebase.Config{ +// DatabaseURL: "https://edusync-test-default-rtdb.firebaseio.com/", +// } + +// // Set up the Firebase app with the provided JSON file containing the service account key. +// // opt := option.WithCredentialsFile(home + "edusync-test-firebase-adminsdk-hk5kl-9af0162b09.json") +// opt := option.WithCredentialsFile("edusync-test-firebase-adminsdk-hk5kl-9af0162b09.json") +// // opt := option.WithCredentialsFile("edusync-7bd5e-firebase-adminsdk-x49uh-af084a6314.json") +// // opt := option.WithCredentialsFile("$HOME/secrets/edusync-7bd5e-firebase-adminsdk-x49uh-af084a6314.json") + +// app, err := firebase.NewApp(ctx, conf, opt) +// if err != nil { +// log.Fatalln("error in initializing firebase app: ", err) +// } + +// client, err := app.Database(ctx) +// if err != nil { +// log.Fatalln("error in creating firebase DB client: ", err) +// } + +// // Student operations +// // student := NewStudent("Jane Doe", 7, 119.5, "jane_doe@nk.com", "91234567", "Tech Explorer", "Scott Smith", "Jackie Doe") +// // err = createStudent(client, student.ID.String(), student) +// googleIDStudent := "google-id-student" +// student := NewStudent(googleIDStudent, "Jane Doe", 7, 119.5, "jane_doe@nk.com", "91234567", "Tech Explorer", "Scott Smith", "Jackie Doe") +// err = createStudent(client, student.GoogleID, student) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Student added/updated successfully!") + +// // readStudent, err := readStudent(client, student.ID.String()) +// readStudent, err := readStudent(client, student.GoogleID) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Student read successfully:", readStudent) + +// studentUpdates := map[string]interface{}{ +// "class": "Tech Explorer 2", +// "updated_at": time.Now(), +// } +// // err = updateStudent(client, student.ID.String(), studentUpdates) +// err = updateStudent(client, student.GoogleID, studentUpdates) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Student updated successfully!") + +// // err = deleteStudent(client, student.ID.String()) +// err = deleteStudent(client, student.GoogleID) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Student deleted successfully!") + +// // Instructor operations +// // instructor := NewInstructor("Scott Smith", "123-456-7890", "scott@example.com", 50000.00, 10) +// // err = createInstructor(client, instructor.ID.String(), instructor) +// googleIDInstructor := "google-id-instructor" +// instructor := NewInstructor(googleIDInstructor, "Scott Smith", "123-456-7890", "scott@example.com", 50000.00, 10) +// err = createInstructor(client, instructor.GoogleID, instructor) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Instructor added/updated successfully!") + +// // readInstructor, err := readInstructor(client, instructor.ID.String()) +// readInstructor, err := readInstructor(client, instructor.GoogleID) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Instructor read successfully:", readInstructor) + +// instructorUpdates := map[string]interface{}{ +// "base_pay": 55000.00, +// "updated_at": time.Now(), +// } +// // err = updateInstructor(client, instructor.ID.String(), instructorUpdates) +// err = updateInstructor(client, instructor.GoogleID, instructorUpdates) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Instructor updated successfully!") + +// // err = deleteInstructor(client, instructor.ID.String()) +// err = deleteInstructor(client, instructor.GoogleID) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Instructor deleted successfully!") + +// // Parent operations +// // parent := NewParent("Jackie Doe", "jackjack@example.com", "98765432") +// // err = createParent(client, parent.ID.String(), parent) +// googleIDParent := "google-id-parent" +// parent := NewParent(googleIDParent, "Jackie Doe", "jackjack@example.com", "98765432") +// err = createParent(client, parent.GoogleID, parent) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Parent added/updated successfully!") + +// // readParent, err := readParent(client, parent.ID.String()) +// readParent, err := readParent(client, parent.GoogleID) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Parent read successfully:", readParent) + +// parentUpdates := map[string]interface{}{ +// "email": "jackiejack@nk.com", +// "updated_at": time.Now(), +// } +// // err = updateParent(client, parent.ID.String(), parentUpdates) +// err = updateParent(client, parent.GoogleID, parentUpdates) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Parent updated successfully!") + +// // err = deleteParent(client, parent.ID.String()) +// err = deleteParent(client, parent.GoogleID) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("Parent deleted successfully!") +// } diff --git a/database_test.go b/database_test.go index 45b4091..1b2b6dd 100644 --- a/database_test.go +++ b/database_test.go @@ -13,7 +13,7 @@ var students = []Student{ User: User{ GoogleID: "test-student", Name: "John Doe", - Email: "johndoe_student@example.com", + Email: "jeyvianangjieen@gmail.com", ContactNumber: "91234567", Role: "Student", }, @@ -86,11 +86,12 @@ func TestInitializeFirebase(t *testing.T) { // Testing for student CRUD operations func TestCreateStudent(t *testing.T) { err := createStudent(currentUser, students[0]) + if err != nil { t.Fatalf("Error creating student: %v", err) } - readStudent, err := readStudent(currentUser, students[0], classes) + readStudent, err := readStudent(currentUser, students[0].GoogleID) if err != nil { t.Fatalf("Error reading student: %v", err) } @@ -101,7 +102,7 @@ func TestCreateStudent(t *testing.T) { } func TestReadStudent(t *testing.T) { - readStudent, err := readStudent(currentUser, students[0], classes) + readStudent, err := readStudent(currentUser, students[0].GoogleID) if err != nil { t.Fatalf("Error reading student: %v", err) } @@ -117,13 +118,13 @@ func TestUpdateStudent(t *testing.T) { "name": "Updated Student", } - err := updateStudent(currentUser, students[0], classes, updates) + err := updateStudent(currentUser, students[0].GoogleID, updates) if err != nil { t.Fatalf("Error updating student: %v", err) } // Read the updated student - readStudent, err := readStudent(currentUser, students[0], classes) + readStudent, err := readStudent(currentUser, students[0].GoogleID) if err != nil { t.Fatalf("Error reading student after updating: %v", err) } @@ -156,7 +157,7 @@ func TestCreateInstructor(t *testing.T) { } // Read the created instructor - readInstructor, err := readInstructor(currentUser, instructor) + readInstructor, err := readInstructor(currentUser, instructor.GoogleID) if err != nil { t.Fatalf("Error reading instructor: %v", err) } @@ -169,7 +170,7 @@ func TestCreateInstructor(t *testing.T) { func TestReadInstructor(t *testing.T) { - instructor, err := readInstructor(currentUser, instructor) + instructor, err := readInstructor(currentUser, instructor.GoogleID) if err != nil { t.Fatalf("Failed to read instructor: %v", err) } @@ -185,13 +186,13 @@ func TestUpdateInstructor(t *testing.T) { "email": "amazing_instructor@nk.com", } - err := updateInstructor(currentUser, instructor, updates) + err := updateInstructor(currentUser, instructor.GoogleID, updates) if err != nil { t.Fatalf("Error updating instructor: %v", err) } // Read the updated instructor - readInstructor, err := readInstructor(currentUser, instructor) + readInstructor, err := readInstructor(currentUser, instructor.GoogleID) if err != nil { t.Fatalf("Error reading instructor: %v", err) } @@ -224,7 +225,7 @@ func TestCreateAdmin(t *testing.T) { } // Read the created admin - readAdmin, err := readAdmin(currentUser, admin) + readAdmin, err := readAdmin(currentUser, admin.GoogleID) if err != nil { t.Fatalf("Error reading admin: %v", err) } @@ -236,7 +237,7 @@ func TestCreateAdmin(t *testing.T) { } func TestReadAdmin(t *testing.T) { - admin, err := readAdmin(currentUser, admin) + admin, err := readAdmin(currentUser, admin.GoogleID) if err != nil { t.Fatalf("Failed to read instructor: %v", err) } @@ -252,13 +253,13 @@ func TestUpdateAdmin(t *testing.T) { "email": "amazing_admin@nk.com", } - err := updateAdmin(currentUser, admin, updates) + err := updateAdmin(currentUser, admin.GoogleID, updates) if err != nil { t.Fatalf("Error updating admin: %v", err) } // Read the updated admin - readAdmin, err := readAdmin(currentUser, admin) + readAdmin, err := readAdmin(currentUser, admin.GoogleID) if err != nil { t.Fatalf("Error reading admin: %v", err) } @@ -291,7 +292,7 @@ func TestCreateParent(t *testing.T) { } // Read the created parent - readParent, err := readParent(currentUser, parent) + readParent, err := readParent(currentUser, parent.GoogleID) if err != nil { t.Fatalf("Error reading parent: %v", err) } @@ -303,7 +304,7 @@ func TestCreateParent(t *testing.T) { } func TestReadParent(t *testing.T) { - parent, err := readParent(currentUser, parent) + parent, err := readParent(currentUser, parent.GoogleID) if err != nil { t.Fatalf("Failed to read parent: %v", err) } @@ -319,13 +320,13 @@ func TestUpdateParent(t *testing.T) { "email": "jane_doe_parent@nk.com", } - err := updateParent(currentUser, parent, updates) + err := updateParent(currentUser, parent.GoogleID, updates) if err != nil { t.Fatalf("Error updating parent: %v", err) } // Read the updated parent - readParent, err := readParent(currentUser, parent) + readParent, err := readParent(currentUser, parent.GoogleID) if err != nil { t.Fatalf("Error reading parent: %v", err) } @@ -359,7 +360,7 @@ func TestCreateClass(t *testing.T) { } // Read the created class - readClass, err := readClass(currentUser, students, classes[0]) + readClass, err := readClass(currentUser, students, classes[0].ClassID) if err != nil { t.Fatalf("Error reading class: %v", err) } @@ -371,7 +372,7 @@ func TestCreateClass(t *testing.T) { } func TestReadClass(t *testing.T) { - class, err := readClass(currentUser, students, classes[0]) + class, err := readClass(currentUser, students, classes[0].ClassID) if err != nil { t.Fatalf("Failed to read class: %v", err) } @@ -393,7 +394,7 @@ func TestUpdateClass(t *testing.T) { } // Read the updated class - readClass, err := readClass(currentUser, students, classes[0]) + readClass, err := readClass(currentUser, students, classes[0].ClassID) if err != nil { t.Fatalf("Error reading class: %v", err) } diff --git a/firebase.go b/firebase.go new file mode 100644 index 0000000..d77323d --- /dev/null +++ b/firebase.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + //"github.com/joho/godotenv" + + firebase "firebase.google.com/go" + "google.golang.org/api/option" +) + +// Use godot package to load/read the .env file and +// return the value of the key (for local env) +// func goDotEnvVariable(key string) string { + +// // load .env file +// err := godotenv.Load(".env") + +// if err != nil { +// log.Fatalf("Error loading .env file") +// } + +// return os.Getenv(key) +// } + +// InitializeFirebase initializes the Firebase app and sets the global firebaseClient variable +func initializeFirebase() error { + ctx := context.Background() + + databaseURL, found := os.LookupEnv("DATABASE_URL") + if !found { + log.Fatalf("DATABASE_URL is not set in the environment variables") + } + + // databaseURL := goDotEnvVariable("DATABASE_URL") + // if databaseURL == "" { + // return fmt.Errorf("DATABASE_URL is not set in the environment variables") + // } + + conf := &firebase.Config{DatabaseURL: databaseURL} + + opt := option.WithCredentialsFile("edusync-7bd5e-firebase-adminsdk-x49uh-af084a6314.json") + //opt := option.WithCredentialsFile("edusync-test-firebase-adminsdk-hk5kl-9af0162b09.json") + + app, err := firebase.NewApp(ctx, conf, opt) + if err != nil { + return fmt.Errorf("error initializing firebase app: %v", err) + } + + var firebaseApp = app + + err = initDB(firebaseApp) + if err != nil { + return fmt.Errorf("error initializing database: %v", err) + } + return nil +} diff --git a/handler_test.go b/handler_test.go new file mode 100644 index 0000000..8c30f4d --- /dev/null +++ b/handler_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" +) + +func TestRoleHandler(t *testing.T) { + router := mux.NewRouter() + MainHandler(router) + InstructorHandler(router) + StudentHandler(router) + ParentHandler(router) + AdminHandler(router) + + tests := []struct { + route string + expectedStatus int + }{ + {"/", http.StatusOK}, + {"/admin", http.StatusOK}, + {"/student", http.StatusOK}, + {"/instructor", http.StatusOK}, + {"/parent", http.StatusOK}, + } + + for _, tt := range tests { + req, err := http.NewRequest("GET", tt.route, nil) + if err != nil { + t.Fatalf("could not create request: %v", err) + } + res := httptest.NewRecorder() + + router.ServeHTTP(res, req) + + if res.Code != tt.expectedStatus { + t.Errorf("expected status %v; got %v", tt.expectedStatus, res.Code) + } + } +} diff --git a/instructorHandler.go b/instructorHandler.go new file mode 100644 index 0000000..8c8a89a --- /dev/null +++ b/instructorHandler.go @@ -0,0 +1,19 @@ +package main + +import ( + "html/template" + "net/http" + + "github.com/gorilla/mux" +) + +func InstructorHandler(router *mux.Router) { + router.HandleFunc("/instructor", func(res http.ResponseWriter, req *http.Request) { + t, err := template.ParseFiles("templates/instructor/index.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, nil) + }).Methods("GET") +} diff --git a/main.go b/main.go index b8d8b40..b69f687 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,10 @@ import ( func init() { // database() initializeFirebase() + err := initializeFirebase() + if err != nil { + log.Fatalf("Failed to initialize Firebase: %v", err) + } } func main() { @@ -27,7 +31,8 @@ func main() { // Set up authentication routes AuthHandler(router, config) - RoleHandler(router) + MainHandler(router) + AdminHandler(router) log.Println("listening on localhost:8080") err = http.ListenAndServeTLS(":8080", "cert.pem", "key.pem", router) diff --git a/mainHandler.go b/mainHandler.go new file mode 100644 index 0000000..da44a74 --- /dev/null +++ b/mainHandler.go @@ -0,0 +1,28 @@ +package main + +import ( + "html/template" + "net/http" + + "github.com/gorilla/mux" +) + +func MainHandler(router *mux.Router) { + router.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { + t, err := template.ParseFiles("templates/index.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, false) + }).Methods("GET") + + router.HandleFunc("/unregistered", func(res http.ResponseWriter, req *http.Request) { + t, err := template.ParseFiles("templates/unregistered.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, false) + }).Methods("GET") +} diff --git a/parentHandler.go b/parentHandler.go new file mode 100644 index 0000000..8c97886 --- /dev/null +++ b/parentHandler.go @@ -0,0 +1,19 @@ +package main + +import ( + "html/template" + "net/http" + + "github.com/gorilla/mux" +) + +func ParentHandler(router *mux.Router) { + router.HandleFunc("/parent", func(res http.ResponseWriter, req *http.Request) { + t, err := template.ParseFiles("templates/parent/index.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, nil) + }).Methods("GET") +} diff --git a/roleHandler.go b/roleHandler.go deleted file mode 100644 index 65c4902..0000000 --- a/roleHandler.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "html/template" - "net/http" - - "github.com/gorilla/mux" -) - -func RoleHandler(router *mux.Router) { - router.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { - t, err := template.ParseFiles("templates/index.html") - if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return - } - t.Execute(res, false) - }).Methods("GET") - - router.HandleFunc("/admin", func(res http.ResponseWriter, req *http.Request) { - t, err := template.ParseFiles("templates/admin/index.html") - if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return - } - t.Execute(res, nil) - }).Methods("GET") - - router.HandleFunc("/student", func(res http.ResponseWriter, req *http.Request) { - t, err := template.ParseFiles("templates/student/index.html") - if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return - } - t.Execute(res, nil) - }).Methods("GET") - - router.HandleFunc("/instructor", func(res http.ResponseWriter, req *http.Request) { - t, err := template.ParseFiles("templates/instructor/index.html") - if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return - } - t.Execute(res, nil) - }).Methods("GET") -} diff --git a/studentHandler.go b/studentHandler.go new file mode 100644 index 0000000..1276a1c --- /dev/null +++ b/studentHandler.go @@ -0,0 +1,19 @@ +package main + +import ( + "html/template" + "net/http" + + "github.com/gorilla/mux" +) + +func StudentHandler(router *mux.Router) { + router.HandleFunc("/student", func(res http.ResponseWriter, req *http.Request) { + t, err := template.ParseFiles("templates/student/index.html") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + t.Execute(res, nil) + }).Methods("GET") +} diff --git a/templates/admin/edit_instructor.html b/templates/admin/edit_instructor.html new file mode 100644 index 0000000..ca019aa --- /dev/null +++ b/templates/admin/edit_instructor.html @@ -0,0 +1,145 @@ + + +
+