Implemented the rest of the methods except for GetKillSwitchStatus

Change-Id: Ia5b1a1c44c2e52653b9845ecc9557f525e6d41b1
diff --git a/internal/db/authorized_users.go b/internal/db/authorized_users.go
new file mode 100644
index 0000000..13a272f
--- /dev/null
+++ b/internal/db/authorized_users.go
@@ -0,0 +1,152 @@
+package db
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+
+	pb "gomodules.avm99963.com/twpt-server/api_proto"
+)
+
+func GetAuthorizedUserById(db *sql.DB, ctx context.Context, id int32) (*pb.KillSwitchAuthorizedUser, error) {
+	query := db.QueryRowContext(ctx, "SELECT user_id, google_uid, email, access_level FROM KillSwitchAuthorizedUser WHERE user_id = ?", id)
+	var u pb.KillSwitchAuthorizedUser
+	if err := query.Scan(&u.Id, &u.GoogleUid, &u.Email, &u.AccessLevel); err != nil {
+		if err == sql.ErrNoRows {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("GetAuthorizedUserById: %v.", err)
+	}
+	return &u, nil
+}
+
+func AddAuthorizedUser(db *sql.DB, ctx context.Context, u *pb.KillSwitchAuthorizedUser) error {
+	tx, err := db.BeginTx(ctx, nil)
+	if err != nil {
+		return err
+	}
+
+	result, err := db.ExecContext(ctx, "INSERT INTO KillSwitchAuthorizedUser (google_uid, email, access_level) VALUES (?, ?, ?)", u.GoogleUid, u.Email, u.AccessLevel)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	id, err := result.LastInsertId()
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	u.Id = int32(id)
+
+	logEntry := &pb.KillSwitchAuditLogEntry{
+		Description: &pb.KillSwitchAuditLogEntry_AuthorizedUserAdded_{
+			&pb.KillSwitchAuditLogEntry_AuthorizedUserAdded{
+				User: u,
+			},
+		},
+	}
+	if err := AddKillSwitchAuditLogEntry(tx, ctx, logEntry); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	return tx.Commit()
+}
+
+func UpdateAuthorizedUser(db *sql.DB, ctx context.Context, id int32, newUser *pb.KillSwitchAuthorizedUser) error {
+	oldUser, err := GetAuthorizedUserById(db, ctx, id)
+	if err != nil {
+		return err
+	}
+	if oldUser == nil {
+		return fmt.Errorf("Such user doesn't exist")
+	}
+
+	tx, err := db.BeginTx(ctx, nil)
+	if err != nil {
+		return err
+	}
+
+	if _, err := tx.ExecContext(ctx, "UPDATE KillSwitchAuthorizedUser SET google_uid = ?, email = ?, access_level = ? WHERE user_id = ?", newUser.GoogleUid, newUser.Email, newUser.AccessLevel, id); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	newUser.Id = id
+
+	logEntry := &pb.KillSwitchAuditLogEntry{
+		Description: &pb.KillSwitchAuditLogEntry_AuthorizedUserUpdated_{
+			&pb.KillSwitchAuditLogEntry_AuthorizedUserUpdated{
+				Transformation: &pb.AuthorizedUserTransformation{
+					Old: oldUser,
+					New: newUser,
+				},
+			},
+		},
+	}
+	if err := AddKillSwitchAuditLogEntry(tx, ctx, logEntry); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	return tx.Commit()
+}
+
+func DeleteAuthorizedUser(db *sql.DB, ctx context.Context, id int32) error {
+	u, err := GetAuthorizedUserById(db, ctx, id)
+	if err != nil {
+		return err
+	}
+	if u == nil {
+		return fmt.Errorf("Such user doesn't exist")
+	}
+
+	tx, err := db.BeginTx(ctx, nil)
+	if err != nil {
+		return err
+	}
+
+	if _, err := tx.ExecContext(ctx, "DELETE FROM KillSwitchAuthorizedUser WHERE user_id = ?", id); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	logEntry := &pb.KillSwitchAuditLogEntry{
+		Description: &pb.KillSwitchAuditLogEntry_AuthorizedUserDeleted_{
+			&pb.KillSwitchAuditLogEntry_AuthorizedUserDeleted{
+				OldUser: u,
+			},
+		},
+	}
+	if err := AddKillSwitchAuditLogEntry(tx, ctx, logEntry); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	return tx.Commit()
+}
+
+func ListAuthorizedUsers(db *sql.DB, ctx context.Context) ([]*pb.KillSwitchAuthorizedUser, error) {
+	var rows *sql.Rows
+	var err error
+	rows, err = db.QueryContext(ctx, "SELECT user_id, google_uid, email, access_level FROM KillSwitchAuthorizedUser")
+	if err != nil {
+		return nil, fmt.Errorf("ListAuthorizedUsers: %v", err)
+	}
+	defer rows.Close()
+
+	var users []*pb.KillSwitchAuthorizedUser
+	for rows.Next() {
+		var u pb.KillSwitchAuthorizedUser
+		if err := rows.Scan(&u.Id, &u.GoogleUid, &u.Email, &u.AccessLevel); err != nil {
+			return nil, fmt.Errorf("ListAuthorizedUsers: %v", err)
+		}
+		users = append(users, &u)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, fmt.Errorf("ListAuthorizedUsers: %v", err)
+	}
+	return users, nil
+}
diff --git a/internal/db/common.go b/internal/db/common.go
new file mode 100644
index 0000000..845db2c
--- /dev/null
+++ b/internal/db/common.go
@@ -0,0 +1,26 @@
+package db
+
+import (
+	"context"
+	"database/sql"
+
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	pb "gomodules.avm99963.com/twpt-server/api_proto"
+)
+
+func AddKillSwitchAuditLogEntry(tx *sql.Tx, ctx context.Context, logEntry *pb.KillSwitchAuditLogEntry) error {
+	logEntry.Timestamp = timestamppb.Now()
+
+	logEntryBytes, err := proto.Marshal(logEntry)
+	if err != nil {
+		return err
+	}
+
+	if _, err := tx.ExecContext(ctx, "INSERT INTO KillSwitchAuditLog (data) VALUES (?)", logEntryBytes); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/internal/db/features.go b/internal/db/features.go
index 4a263b3..5b68228 100644
--- a/internal/db/features.go
+++ b/internal/db/features.go
@@ -8,6 +8,18 @@
 	pb "gomodules.avm99963.com/twpt-server/api_proto"
 )
 
+func GetFeatureById(db *sql.DB, ctx context.Context, id int32) (*pb.Feature, error) {
+	query := db.QueryRowContext(ctx, "SELECT feat_id, codename, feat_type FROM Feature WHERE feat_id = ?", id)
+	var f pb.Feature
+	if err := query.Scan(&f.Id, &f.Codename, &f.Type); err != nil {
+		if err == sql.ErrNoRows {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("Error while querying feature by codename: %v.", err)
+	}
+	return &f, nil
+}
+
 func GetFeatureByCodename(db *sql.DB, ctx context.Context, codename string) (*pb.Feature, error) {
 	query := db.QueryRowContext(ctx, "SELECT feat_id, codename, feat_type FROM Feature WHERE codename = ?", codename)
 	var f pb.Feature
@@ -29,7 +41,7 @@
     rows, err = db.QueryContext(ctx, "SELECT feat_id, codename, feat_type FROM Feature WHERE feat_type <> ?", pb.Feature_TYPE_DEPRECATED)
 	}
 	if err != nil {
-		return nil, fmt.Errorf("ListFeatures: ", err)
+		return nil, fmt.Errorf("ListFeatures: %v", err)
 	}
 	defer rows.Close()
 
@@ -37,12 +49,12 @@
 	for rows.Next() {
 		var f pb.Feature
 		if err := rows.Scan(&f.Id, &f.Codename, &f.Type); err != nil {
-			return nil, fmt.Errorf("ListFeatures: ", err)
+			return nil, fmt.Errorf("ListFeatures: %v", err)
 		}
 		features = append(features, &f)
 	}
 	if err := rows.Err(); err != nil {
-		return nil, fmt.Errorf("ListFeatures: ", err)
+		return nil, fmt.Errorf("ListFeatures: %v", err)
 	}
 	return features, nil
 }
diff --git a/internal/db/kill_switch.go b/internal/db/kill_switch.go
new file mode 100644
index 0000000..8b80286
--- /dev/null
+++ b/internal/db/kill_switch.go
@@ -0,0 +1,206 @@
+package db
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+
+	"github.com/Masterminds/semver/v3"
+
+	pb "gomodules.avm99963.com/twpt-server/api_proto"
+)
+
+func isValidSemVersion(v string) bool {
+	_, err := semver.NewVersion(v)
+	return err == nil
+}
+
+func fillRawKillSwitch(db *sql.DB, ctx context.Context, k *pb.KillSwitch, featureId int32) error {
+	f, err := GetFeatureById(db, ctx, featureId)
+	if err != nil {
+		return fmt.Errorf("fillRawKillSwitch: %v", err)
+	}
+	if f == nil {
+		return fmt.Errorf("fillRawKillSwitch: linked feature doesn't exist.")
+	}
+	k.Feature = f
+
+	rows, err := db.QueryContext(ctx, "SELECT browser FROM KillSwitch2Browser WHERE kswitch_id = ?", k.Id)
+	if err != nil {
+		return fmt.Errorf("fillRawKillSwitch: %v", err)
+	}
+	defer rows.Close()
+
+	var browsers []pb.Environment_Browser
+	for rows.Next() {
+		var b pb.Environment_Browser
+		if err := rows.Scan(&b); err != nil {
+			return fmt.Errorf("fillRawKillSwitch: %v", err)
+		}
+		browsers = append(browsers, b)
+	}
+	k.Browsers = browsers
+
+	return nil
+}
+
+func GetKillSwitchById(db *sql.DB, ctx context.Context, id int32) (*pb.KillSwitch, error) {
+	query := db.QueryRowContext(ctx, "SELECT kswitch_id, feat_id, min_version, max_version, active FROM KillSwitch WHERE kswitch_id = ?", id)
+	var featureId int32
+	var k pb.KillSwitch
+	if err := query.Scan(&k.Id, &featureId, &k.MinVersion, &k.MaxVersion, &k.Active); err != nil {
+		if err == sql.ErrNoRows {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("GetKillSwitchById: %v.", err)
+	}
+
+	err := fillRawKillSwitch(db, ctx, &k, featureId)
+	if err != nil {
+		return nil, fmt.Errorf("GetKillSwitchById: $v.", err)
+	}
+
+	return &k, nil
+}
+
+func ListKillSwitches(db *sql.DB, ctx context.Context) ([]*pb.KillSwitch, error) {
+	var rows *sql.Rows
+	var err error
+	rows, err = db.QueryContext(ctx, "SELECT kswitch_id, feat_id, min_version, max_version, active FROM KillSwitch")
+	if err != nil {
+		return nil, fmt.Errorf("ListKillSwitches: ", err)
+	}
+	defer rows.Close()
+
+	var killSwitches []*pb.KillSwitch
+	for rows.Next() {
+		var featureId int32
+		var k pb.KillSwitch
+		if err := rows.Scan(&k.Id, &featureId, &k.MinVersion, &k.MaxVersion, &k.Active); err != nil {
+			return nil, fmt.Errorf("ListKillSwitches: %v.", err)
+		}
+		err := fillRawKillSwitch(db, ctx, &k, featureId)
+		if err != nil {
+			return nil, fmt.Errorf("ListKillSwitches: $v.", err)
+		}
+		killSwitches = append(killSwitches, &k)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, fmt.Errorf("ListKillSwitches: ", err)
+	}
+	return killSwitches, nil
+}
+
+func EnableKillSwitch(db *sql.DB, ctx context.Context, k *pb.KillSwitch) error {
+	if k.MinVersion != "" && !isValidSemVersion(k.MinVersion) {
+		return fmt.Errorf("min_version is not a valid semantic version.")
+	}
+	if k.MaxVersion != "" && !isValidSemVersion(k.MaxVersion) {
+		return fmt.Errorf("max_version is not a valid semantic version.")
+	}
+	if k.MinVersion != "" && k.MaxVersion != "" {
+		minVersion, _ := semver.NewVersion(k.MinVersion)
+		maxVersion, _ := semver.NewVersion(k.MaxVersion)
+		if minVersion.GreaterThan(maxVersion) {
+			return fmt.Errorf("min_version must be less than max_version.")
+		}
+	}
+	for _, b := range k.GetBrowsers() {
+		if b == pb.Environment_BROWSER_UNKNOWN {
+			return fmt.Errorf("browsers cannot contain BROWSER_UNKNOWN.")
+		}
+	}
+
+	k.Active = true
+
+	f, err := GetFeatureById(db, ctx, k.Feature.Id)
+	if err != nil {
+		return fmt.Errorf("EnableKillSwitch: %v", err)
+	}
+	if f == nil {
+		return fmt.Errorf("EnableKillSwitch: this feature doesn't exist.")
+	}
+	k.Feature = f
+
+	tx, err := db.BeginTx(ctx, nil)
+	if err != nil {
+		return err
+	}
+
+	result, err := tx.ExecContext(ctx, "INSERT INTO KillSwitch (feat_id, min_version, max_version, active) VALUES (?, ?, ?, ?)", k.Feature.Id, k.MinVersion, k.MaxVersion, k.Active)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	id, err := result.LastInsertId()
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+	k.Id = int32(id)
+
+	for _, b := range k.GetBrowsers() {
+		if _, err := tx.ExecContext(ctx, "INSERT INTO KillSwitch2Browser (kswitch_id, browser) VALUES (?, ?)", k.Id, b); err != nil {
+			tx.Rollback()
+			return err
+		}
+	}
+
+	logEntry := &pb.KillSwitchAuditLogEntry{
+		Description: &pb.KillSwitchAuditLogEntry_KillSwitchEnabled_{
+			&pb.KillSwitchAuditLogEntry_KillSwitchEnabled{
+				KillSwitch: k,
+			},
+		},
+	}
+	if err := AddKillSwitchAuditLogEntry(tx, ctx, logEntry); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	return tx.Commit()
+}
+
+func DisableKillSwitch(db *sql.DB, ctx context.Context, id int32) error {
+	oldKillSwitch, err := GetKillSwitchById(db, ctx, id)
+	if err != nil {
+		return err
+	}
+	if oldKillSwitch == nil {
+		return fmt.Errorf("Such kill switch doesn't exist")
+	}
+	if oldKillSwitch.GetActive() != true {
+		return fmt.Errorf("The kill switch is not active")
+	}
+
+	tx, err := db.BeginTx(ctx, nil)
+	if err != nil {
+		return err
+	}
+
+	newKillSwitch := *oldKillSwitch
+	newKillSwitch.Active = false
+
+	if _, err := tx.ExecContext(ctx, "UPDATE KillSwitch SET active = ? WHERE kswitch_id = ?", newKillSwitch.Active, id); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	logEntry := &pb.KillSwitchAuditLogEntry{
+		Description: &pb.KillSwitchAuditLogEntry_KillSwitchDisabled_{
+			&pb.KillSwitchAuditLogEntry_KillSwitchDisabled{
+				Transformation: &pb.KillSwitchTransformation{
+					Old: oldKillSwitch,
+					New: &newKillSwitch,
+				},
+			},
+		},
+	}
+	if err := AddKillSwitchAuditLogEntry(tx, ctx, logEntry); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	return tx.Commit()
+}