package gcsql import ( "context" "database/sql" "errors" "fmt" "net/http" "time" "github.com/Eggbertx/durationutil" "github.com/gochan-org/gochan/pkg/config" "github.com/gochan-org/gochan/pkg/gcutil" ) var ( ErrUnrecognizedUsername = errors.New("invalid username") ) // createDefaultAdminIfNoStaff creates a new default admin account if no accounts exist func createDefaultAdminIfNoStaff() error { const query = `SELECT COUNT(id) FROM DBPREFIXstaff` var count int err := QueryRowTimeoutSQL(nil, query, nil, []any{&count}) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } if count > 0 { return nil } _, err = NewStaff("admin", "password", 3) return err } func NewStaff(username string, password string, rank int) (*Staff, error) { const sqlINSERT = `INSERT INTO DBPREFIXstaff (username, password_checksum, global_rank) VALUES(?,?,?)` passwordChecksum := gcutil.BcryptSum(password) _, err := ExecTimeoutSQL(nil, sqlINSERT, username, passwordChecksum, rank) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } return &Staff{ Username: username, PasswordChecksum: passwordChecksum, Rank: rank, AddedOn: time.Now(), IsActive: true, }, nil } // SetActive changes the active status of the staff member. If `active` is false, the login sessions are cleared func (s *Staff) SetActive(active bool) error { const updateActive = `UPDATE DBPREFIXstaff SET is_active = FALSE WHERE username = ?` _, err := ExecTimeoutSQL(nil, updateActive, s.Username) if err != nil { return err } if active { return nil } return s.ClearSessions() } // ClearSessions clears all login sessions for the user, requiring them to login again func (s *Staff) ClearSessions() error { const query = `SELECT id FROM DBPREFIXstaff WHERE username = ?` const deleteSessions = `DELETE FROM DBPREFIXsessions WHERE staff_id = ?` var err error ctx, cancel := context.WithTimeout(context.Background(), gcdb.defaultTimeout) defer cancel() if s.ID == 0 { // ID field not set, get it from the DB err = QueryRowContextSQL(ctx, nil, query, []any{s.Username}, []any{&s.ID}) if errors.Is(err, sql.ErrNoRows) { return ErrUnrecognizedUsername } if err != nil { return err } } _, err = ExecContextSQL(ctx, nil, deleteSessions, s.ID) return err } func (s *Staff) RankTitle() string { if s.Rank == 3 { return "Administrator" } else if s.Rank == 2 { return "Moderator" } else if s.Rank == 1 { return "Janitor" } return "" } // UpdateStaff sets the rank and password of the staff account with the given username func UpdateStaff(username string, rank int, password string) error { // first check if it's a recognized username id, err := GetStaffID(username) if err != nil { return err } const sqlUpdate = `UPDATE DBPREFIXstaff SET global_rank = ?, password_checksum = ? WHERE id = ?` checksum := gcutil.BcryptSum(password) _, err = ExecTimeoutSQL(nil, sqlUpdate, rank, checksum, id) return err } // UpdateStaff sets the password of the staff account with the given username func UpdatePassword(username string, newPassword string) error { const sqlUPDATE = `UPDATE DBPREFIXstaff SET password_checksum = ? WHERE id = ?` id, err := GetStaffID(username) if err != nil { return err } checksum := gcutil.BcryptSum(newPassword) _, err = ExecTimeoutSQL(nil, sqlUPDATE, checksum, id) return err } // EndStaffSession deletes any session rows associated with the requests session cookie and then // makes the cookie expire, essentially deleting it func EndStaffSession(writer http.ResponseWriter, request *http.Request) error { session, err := request.Cookie("sessiondata") if err != nil { // No staff session cookie, presumably not logged in so nothing to do return nil } // make it so that the next time the page is loaded, the browser will delete it sessionVal := session.Value session.MaxAge = -1 http.SetCookie(writer, session) ctx, cancel := context.WithTimeout(context.Background(), gcdb.defaultTimeout) defer cancel() staffID := 0 if err = QueryRowContextSQL(ctx, nil, `SELECT staff_id FROM DBPREFIXsessions WHERE data = ?`, []any{session.Value}, []any{&staffID}); err != nil && err != sql.ErrNoRows { // something went wrong with the query and it's not caused by no rows being returned return errors.New("failed getting staff ID: " + err.Error()) } _, err = ExecContextSQL(ctx, nil, `DELETE FROM DBPREFIXsessions WHERE data = ?`, sessionVal) if err != nil && !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("failed clearing session for staff with id %d", staffID) } return nil } func DeactivateStaff(username string) error { s := Staff{Username: username} return s.SetActive(false) } func GetStaffUsernameFromID(id int) (string, error) { const query = `SELECT username FROM DBPREFIXstaff WHERE id = ?` var username string err := QueryRowTimeoutSQL(nil, query, []any{id}, []any{&username}) return username, err } // GetStaffID gets the ID of the given staff, given the username, and returns ErrUnrecognizedUsername if none match func GetStaffID(username string) (int, error) { const query = `SELECT id FROM DBPREFIXstaff WHERE username = ?` var id int err := QueryRowTimeoutSQL(nil, query, []any{username}, []any{&id}) if errors.Is(err, sql.ErrNoRows) { err = ErrUnrecognizedUsername } return id, err } // GetStaffBySession gets the staff that is logged in in the given session func GetStaffBySession(session string) (*Staff, error) { const query = `SELECT staff.id, staff.username, staff.password_checksum, staff.global_rank, staff.added_on, staff.last_login FROM DBPREFIXstaff as staff JOIN DBPREFIXsessions as sessions ON sessions.staff_id = staff.id WHERE sessions.data = ?` var staff Staff err := QueryRowTimeoutSQL(nil, query, []any{session}, []any{ &staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Rank, &staff.AddedOn, &staff.LastLogin}) return &staff, err } // GetStaffFromRequest returns the staff making the request. If the request does not have // a staff cookie, it will return a staff object with rank 0. func GetStaffFromRequest(request *http.Request) (*Staff, error) { sessionCookie, err := request.Cookie("sessiondata") if err != nil { return &Staff{Rank: 0}, nil } staff, err := GetStaffBySession(sessionCookie.Value) if errors.Is(err, sql.ErrNoRows) { return &Staff{Rank: 0}, nil } else if err != nil { return nil, err } return staff, nil } func GetStaffByUsername(username string, onlyActive bool) (*Staff, error) { query := `SELECT id, username, password_checksum, global_rank, added_on, last_login, is_active FROM DBPREFIXstaff WHERE username = ?` if onlyActive { query += ` AND is_active = TRUE` } staff := new(Staff) err := QueryRowTimeoutSQL(nil, query, []any{username}, []any{ &staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Rank, &staff.AddedOn, &staff.LastLogin, &staff.IsActive, }) if errors.Is(err, sql.ErrNoRows) { return nil, ErrUnrecognizedUsername } return staff, err } // CreateLoginSession inserts a session for a given key and username into the database func (staff *Staff) CreateLoginSession(key string) error { const insertSQL = `INSERT INTO DBPREFIXsessions (staff_id,data,expires) VALUES(?,?,?)` const updateSQL = `UPDATE DBPREFIXstaff SET last_login = CURRENT_TIMESTAMP WHERE id = ?` ctx, cancel := context.WithTimeout(context.Background(), gcdb.defaultTimeout) defer cancel() tx, err := BeginContextTx(ctx) if err != nil { return err } defer tx.Rollback() dur, err := durationutil.ParseLongerDuration(config.GetSiteConfig().StaffSessionDuration) if err != nil { return err } _, err = ExecContextSQL(ctx, tx, insertSQL, staff.ID, key, time.Now().Add(dur)) if err != nil { return err } _, err = ExecContextSQL(ctx, tx, updateSQL, staff.ID) if err != nil { return err } return tx.Commit() }