diff --git a/docs/Themes.md b/docs/Themes.md new file mode 100644 index 0000000..cbfde9e --- /dev/null +++ b/docs/Themes.md @@ -0,0 +1,22 @@ +# Themes + +Inside of the [themes folder](../run/themes/), you may find the CSS files for +each theme. Each theme lives inside a single file CSS. The name of the theme +will be inherited from the name of the file (omitting '.css'). + +The themes will be loaded via a link HTML tag, so expect the entire CSS file to +have an effect on the site. + +ALL CSS variables inside a theme must be followed by `!important`. This will let +the browser know to use our CSS variable definitions instead the defaults from +GreenScoutJS. + +```css +--font-color: #1e2226 !important; +``` + +If a theme does not provide a definition for a CSS variable, its default will be +used in place. The default theme can be found +[here in the GreenScoutJS repo. **Reference this file.**](https://github.com/TheGreenMachine/GreenScoutJS/blob/main/greenscoutjs/src/index.css) +**for all available CSS variables to override.** Again, make sure to include +`!important` when copy and pasting. diff --git a/internal/server.go b/internal/server.go index 99c948c..a233bd1 100644 --- a/internal/server.go +++ b/internal/server.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "path/filepath" + "slices" "strconv" "strings" "time" @@ -141,6 +142,7 @@ func SetupServer() *http.Server { http.HandleFunc("/getPfp", handleWithCORS(handlePfpRequest, true)) http.HandleFunc("/generalInfo", handleWithCORS(handleGeneralInfoRequest, true)) http.HandleFunc("/allEvents", handleWithCORS(handleEventsRequest, true)) + http.HandleFunc("/allThemes", handleWithCORS(handleThemesRequest, false)) http.HandleFunc("/gallery", handleWithCORS(handleGalleryRequest, true)) //Provides Authentication @@ -151,13 +153,15 @@ func SetupServer() *http.Server { http.HandleFunc("/dataEntry", handleWithCORS(postTeamData, true)) http.HandleFunc("/pitScout", handleWithCORS(postPitScout, true)) http.HandleFunc("/singleSchedule", handleWithCORS(serveScouterSchedule, true)) - http.HandleFunc("/theme", handleWithCORS(serveTheme, false)) + http.HandleFunc("/getTheme", handleWithCORS(serveTheme, false)) //Admin or curr user http.HandleFunc("/setDisplayName", handleWithCORS(setDisplayName, true)) http.HandleFunc("/setUserPfp", handleWithCORS(setPfp, true)) http.HandleFunc("/provideAdditions", handleWithCORS(handleFrontendAdditions, true)) http.HandleFunc("/setColor", handleWithCORS(handleColorChange, true)) + http.HandleFunc("/setTheme", handleWithCORS(setTheme, true)) + http.HandleFunc("/currTheme", handleWithCORS(getTheme, false)) //Admin or verified http.HandleFunc("/spreadsheet", handleWithCORS(serveSpreadsheet, true)) @@ -459,19 +463,166 @@ func serveScouterSchedule(writer http.ResponseWriter, request *http.Request) { httpResponsef(writer, "Problem serving scouter schedule", "%s", response) } +// Serves a theme's css files func serveTheme(writer http.ResponseWriter, request *http.Request) { auth := getAuthFromCookies(request) - _ = auth + + if auth.Preflight { + writer.WriteHeader(200) + return + } + + if !auth.Authed { + writer.WriteHeader(401) + httpResponsef(writer, "Could not get user theme", "Not authenticated :(") + return + } writer.Header().Set("Vary", "Cookie") - writer.Header().Set("Cache-Control", "private, max-age=0, must-revalidate") + writer.Header().Set("Cache-Control", "private, max-age=2, must-revalidate") writer.Header().Set("Content-Type", "text/css; charset=utf-8") - theme := "light" // getThemeFromCookies(auth.UUID) not implemented + // Optionally if you want to grab a theme independent of the user + directTheme := request.Header.Get("theme") + + theme := GetTheme(auth.UUID) + + if directTheme != "" { + allThemes, err := ListAllThemes() + if err != nil { + LogError(err, "Failed to fetch all themes: ") + + writer.WriteHeader(500) + httpResponsef(writer, "Could not find specified theme", "An unexpected error occurred preventing validation of the theme name") + return + } + // this prevents the client from deciding it wants just ANY file on our system (mucho bado) + if slices.Contains(allThemes, directTheme) { + theme = directTheme + } else { + writer.WriteHeader(404) + httpResponsef(writer, "Could not find specified theme", "Theme %s does not exist.", directTheme) + return + } + + } else if theme == "" { + theme = "Light" + } + + writer.WriteHeader(200) http.ServeFile(writer, request, "run/themes/"+theme+".css") } +// serves the current theme that the user is using +func getTheme(writer http.ResponseWriter, request *http.Request) { + auth := getAuthFromCookies(request) + + if !auth.Authed { + writer.WriteHeader(401) + httpResponsef(writer, "Could not set specified theme", "Not authenticated :(") + return + } + + data := struct { + Theme string `json:"theme"` + }{ + Theme: GetTheme(auth.UUID), + } + + writer.Header().Add("Content-Type", "application/json") + encodeErr := json.NewEncoder(writer).Encode(data) + if encodeErr != nil { + LogErrorf(encodeErr, "Problem encoding %v", data) + } else { + writer.WriteHeader(200) + } + +} + +// sets the theme of the authed user +func setTheme(writer http.ResponseWriter, request *http.Request) { + auth := getAuthFromCookies(request) + + if !auth.Authed { + writer.WriteHeader(401) + httpResponsef(writer, "Could not set specified theme", "Not authenticated :(") + return + } + + requestBytes, err := io.ReadAll(request.Body) + if err != nil { + LogErrorf(err, "Problem reading %v", request.Body) + writer.WriteHeader(500) + return + } + + var data map[string]interface{} + err = json.Unmarshal(requestBytes, &data) + if err != nil { + LogErrorf(err, "Problem unmarshalling %v", requestBytes) + writer.WriteHeader(400) + httpResponsef(writer, "Could not set specified theme", "The json body coukd not be nnmarshalled") + return + } + wantedTheme, ok := data["theme"].(string) + if !ok { + writer.WriteHeader(400) + httpResponsef(writer, "Could not set specified theme", "Missing or invalid 'theme' field") + return + } + + if wantedTheme != "" { + allThemes, err := ListAllThemes() + if err != nil { + LogError(err, "Failed to fetch all themes: ") + + writer.WriteHeader(500) + httpResponsef(writer, "Could not set specified theme", "An unexpected error occurred preventing validation of the theme name") + return + } + + // Do the checking early when we set the theme + if slices.Contains(allThemes, wantedTheme) { + SetTheme(auth.UUID, wantedTheme) + + writer.WriteHeader(200) + httpResponsef(writer, "Could set specified theme", "Successfully switched to \"%s\".", wantedTheme) + } else { + writer.WriteHeader(404) + httpResponsef(writer, "Could not set specified theme", "Theme \"%s\" does not exist.", wantedTheme) + return + } + } + +} + +// Serves a list of every theme +func handleThemesRequest(writer http.ResponseWriter, request *http.Request) { + allThemes, err := ListAllThemes() + if err != nil { + LogError(err, "Failed to fetch all themes: ") + + writer.WriteHeader(500) + httpResponsef(writer, "Could not fetch all themes", "An unexpected error occurred preventing the reading of the theme list") + return + } + + data := struct { + Themes []string `json:"themes"` + }{ + Themes: allThemes, + } + + writer.Header().Add("Content-Type", "application/json") + encodeErr := json.NewEncoder(writer).Encode(data) + if encodeErr != nil { + LogErrorf(encodeErr, "Problem encoding %v", allThemes) + } else { + writer.WriteHeader(200) + } +} + // Handles adding schedules to a given scouter func addIndividualSchedule(writer http.ResponseWriter, request *http.Request) { auth := getAuthFromCookies(request) @@ -497,12 +648,15 @@ func addIndividualSchedule(writer http.ResponseWriter, request *http.Request) { // Handles requests for the various leaderboards func serveLeaderboard(writer http.ResponseWriter, request *http.Request) { var lbType string - var typeHeader string = request.Header.Get("type") - if typeHeader == "HighScore" { + + wantedType := request.Header.Get("type") + + switch wantedType { + case "HighScore": lbType = "highscore" - } else if typeHeader == "LifeScore" { + case "LifeScore": lbType = "lifescore" - } else { + default: lbType = "score" } @@ -559,6 +713,7 @@ func handleWithCORS(handler http.HandlerFunc, okCode bool) http.HandlerFunc { if okCode { w.WriteHeader(200) } + handler(w, r) } } @@ -831,6 +986,7 @@ type RequestAuth struct { Username string Role string Authed bool + Preflight bool } func (a RequestAuth) IsAdmin() bool { @@ -840,15 +996,21 @@ func (a RequestAuth) IsAdmin() bool { func getAuthFromCookies(request *http.Request) RequestAuth { var auth RequestAuth + // checks for preflight requests + if request.Method == http.MethodOptions { + auth.Preflight = true + return auth + } + if c, err := request.Cookie("uuid"); err == nil && c != nil { auth.UUID = c.Value - } else if err != nil { + } else if err != nil && err != http.ErrNoCookie { LogError(err, "error getting request cookie 'uuid'") } if c, err := request.Cookie("certificate"); err == nil && c != nil { auth.Certificate = c.Value - } else if err != nil { + } else if err != nil && err != http.ErrNoCookie { LogError(err, "error getting request cookie 'certificate'") } diff --git a/internal/sheet_writer.go b/internal/sheet_writer.go index 5641a42..5a1afc8 100644 --- a/internal/sheet_writer.go +++ b/internal/sheet_writer.go @@ -17,6 +17,12 @@ import ( yaml "sigs.k8s.io/yaml/goyaml.v2" ) +var useLocalAuth bool = false + +func UseLocalAuth(val bool) { + useLocalAuth = val +} + // Early methods (setup) are from google's quickstart, so I didn't change much about them // Retrieve a token, saves the token, then returns the generated client. @@ -92,10 +98,22 @@ var Srv *sheets.Service func SetupSheetsAPI(creds []byte) { ctx := context.Background() - client, err := google.DefaultClient(context.Background(), sheets.SpreadsheetsScope) - if err != nil { - FatalError(err, "Unable to parse client secret file to config: %v") + var client *http.Client + var err error + + if useLocalAuth { + config, err := google.ConfigFromJSON(creds, "https://www.googleapis.com/auth/spreadsheets") + if err != nil { + FatalError(err, "Unable to parse client secret file to config: %v") + } + client = getClient(config) + } else { + client, err = google.DefaultClient(context.Background(), sheets.SpreadsheetsScope) + if err != nil { + FatalError(err, "Unable to parse client secret file to config: %v") + } } + Srv, err = sheets.NewService(ctx, option.WithHTTPClient(client)) if err != nil { FatalError(err, "Unable to retrieve Sheets client: %v") diff --git a/internal/user_db.go b/internal/user_db.go index cb30579..080fa83 100644 --- a/internal/user_db.go +++ b/internal/user_db.go @@ -5,8 +5,12 @@ package internal import ( "database/sql" "encoding/json" + "errors" + "fmt" + "os" "path/filepath" "slices" + "strings" "github.com/google/uuid" _ "github.com/mattn/go-sqlite3" @@ -15,16 +19,367 @@ import ( // The reference to users.db var userDB *sql.DB +type UserColumn struct { + columnName string // The name of the SQL column + valueType string // The type of the column (TEXT, INT, etc...) + defaultValue sql.NullString // The value that will be in row if not replaced + notNull bool // Prevents a column from being null + primaryKey bool // If the column should be used to look up a row. Also enforces uniqueness + unique bool // Each row has to have a different value in the column +} + // Initializes users.db and stores the reference to memory func InitUserDB() { dbPath := filepath.Join(CachedConfigs.PathToDatabases, "users.db") - dbRef, dbOpenErr := sql.Open(CachedConfigs.SqliteDriver, dbPath) + _, err := os.Stat(dbPath) + dbMissing := err != nil && errors.Is(err, os.ErrNotExist) + dbRef, err := sql.Open(CachedConfigs.SqliteDriver, dbPath) + if err != nil { + FatalError(err, "Problem opening database "+dbPath) + } userDB = dbRef - if dbOpenErr != nil { - FatalError(dbOpenErr, "Problem opening database "+dbPath) + // when changing this, please make sure it you modify NewUser() as new account creation may fail for whatever reason + schema := []UserColumn{ + {columnName: "uuid", valueType: "TEXT", unique: true, primaryKey: true}, + {columnName: "username", valueType: "TEXT", unique: true}, + {columnName: "displayname", valueType: "TEXT"}, + {columnName: "certificate", valueType: "TEXT"}, + {columnName: "badges", valueType: "TEXT[]"}, + {columnName: "score", valueType: "INT"}, + {columnName: "pfp", valueType: "TEXT", defaultValue: sql.NullString{String: "'" + DefaultPfpPath + "'", Valid: true}}, + {columnName: "lifescore", valueType: "INT"}, + {columnName: "highscore", valueType: "INT"}, + {columnName: "accolades", valueType: "TEXT", defaultValue: sql.NullString{String: "'[]'", Valid: true}}, + {columnName: "color", valueType: "INT"}, + {columnName: "theme", valueType: "TEXT", defaultValue: sql.NullString{String: "'Light'", Valid: true}}, + } + + if dbMissing { + LogMessage("Creating new user database. Populating...") + err = populateNewDB(schema) + if err != nil { + FatalError(err, "Failed to update the database with new schema: ") + } + } else { + err = updateCurrentDB(schema) + if err != nil { + FatalError(err, "Failed to update the database with new schema: ") + } + } + +} + +// updateCurrentDB compares an existing SQLite table with the desired schema +// and migrates it to match. It adds missing columns when possible, and rebuilds +// the table when incompatible changes are detected +func updateCurrentDB(schema []UserColumn) error { + exists, err := tableExists() + if err != nil { + return err + } + if !exists { + return createTable(schema) + } + + current, err := readCurrentSchema() + if err != nil { + return err + } + + desiredMap := map[string]UserColumn{} + for _, column := range schema { + desiredMap[column.columnName] = column + } + + currentMap := map[string]UserColumn{} + for _, column := range current { + currentMap[column.columnName] = column + } + + needsRebuild := false + + // checks if theres an extra column in teh database (not mucho gracias) + for name := range currentMap { + if _, ok := desiredMap[name]; !ok { + needsRebuild = true + break + } + } + + // see if existing columns dont like changes in schema + if !needsRebuild { + for _, want := range schema { + got, ok := currentMap[want.columnName] + if !ok { + continue + } + + if !sameColumn(got, want) { + needsRebuild = true + break + } + } + } + + if needsRebuild { + return rebuildTable(schema, currentMap) + } + + for _, want := range schema { + if _, ok := currentMap[want.columnName]; ok { + continue + } + + if _, err := userDB.Exec("ALTER TABLE users ADD COLUMN " + buildColumnDef(want)); err != nil { + return fmt.Errorf("add column %s: %w", want.columnName, err) + } + } + + return nil +} + +func tableExists() (bool, error) { + var n int + err := userDB.QueryRow( + `SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?`, + "users", + ).Scan(&n) + + return n > 0, err +} + +func createTable(schema []UserColumn) error { + columnConstructors := make([]string, 0, len(schema)) + for _, column := range schema { + columnConstructors = append(columnConstructors, buildColumnDef(column)) + } + + _, err := userDB.Exec(fmt.Sprintf("CREATE TABLE users (%s)", strings.Join(columnConstructors, ", "))) + return err +} + +// gets a user column array from the loaded database +func readCurrentSchema() ([]UserColumn, error) { + rows, err := userDB.Query("PRAGMA table_info(users)") + if err != nil { + return nil, err + } + defer rows.Close() + + var cols []UserColumn + for rows.Next() { + var cid int + var name, typee string // more like bladee + var notNull, primaryKey int + var defaultValue sql.NullString + + if err := rows.Scan(&cid, &name, &typee, ¬Null, &defaultValue, &primaryKey); err != nil { + return nil, err + } + + cols = append(cols, UserColumn{ + columnName: name, + valueType: strings.ToUpper(strings.TrimSpace(typee)), + defaultValue: defaultValue, + notNull: notNull == 1, + primaryKey: primaryKey == 1, + unique: false, // filled below + }) + } + if err := rows.Err(); err != nil { + return nil, err + } + + uniqueColumns, err := readUniqueColumns() + if err != nil { + return nil, err + } + for i := range cols { + if uniqueColumns[cols[i].columnName] { + cols[i].unique = true + } + } + + return cols, nil +} + +func readUniqueColumns() (map[string]bool, error) { + result := map[string]bool{} + + idxRows, err := userDB.Query("PRAGMA index_list(users)") + if err != nil { + return nil, err + } + defer idxRows.Close() + + type idx struct { + name string + unique int + } + var idxs []idx + for idxRows.Next() { + var seq int + var name string + var unique int + var origin string + var partial int + if err := idxRows.Scan(&seq, &name, &unique, &origin, &partial); err != nil { + return nil, err + } + if unique == 1 { + idxs = append(idxs, idx{name: name, unique: unique}) + } + } + if err := idxRows.Err(); err != nil { + return nil, err + } + + for _, ix := range idxs { + infoRows, err := userDB.Query(fmt.Sprintf("PRAGMA index_info(%s)", friendlySqlStr(ix.name))) + if err != nil { + return nil, err + } + + var columns []string + for infoRows.Next() { + var seqno, cid int + var colName string + if err := infoRows.Scan(&seqno, &cid, &colName); err != nil { + infoRows.Close() + return nil, err + } + columns = append(columns, colName) + } + infoRows.Close() + + if len(columns) == 1 { + result[columns[0]] = true + } + } + + return result, nil +} + +// compares two UserColumns against each other to if theyre the same +func sameColumn(oldColumn UserColumn, newColumn UserColumn) bool { + if !strings.EqualFold(strings.TrimSpace(oldColumn.valueType), strings.TrimSpace(newColumn.valueType)) { + return false + } + if oldColumn.notNull != newColumn.notNull { + return false + } + if oldColumn.primaryKey != newColumn.primaryKey { + return false + } + if oldColumn.unique != newColumn.unique { + return false + } + + gotDefault := "" + if oldColumn.defaultValue.Valid { + gotDefault = normalizeDefault(oldColumn.defaultValue.String) + } + wantDef := normalizeDefault(newColumn.defaultValue.String) + return gotDefault == wantDef +} + +func rebuildTable(schema []UserColumn, currentSchema map[string]UserColumn) error { + tempName := "users__new" + + tx, err := userDB.Begin() + if err != nil { + return err + } + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + + columnConstructors := make([]string, 0, len(schema)) + for _, c := range schema { + columnConstructors = append(columnConstructors, buildColumnDef(c)) + } + if _, err = tx.Exec(fmt.Sprintf("CREATE TABLE %s (%s)", friendlySqlStr(tempName), strings.Join(columnConstructors, ", "))); err != nil { + return err + } + + // copies over the existing columns + var shared []string + for _, column := range schema { + if _, ok := currentSchema[column.columnName]; ok { + shared = append(shared, friendlySqlStr(column.columnName)) + } + } + if len(shared) > 0 { + columns := strings.Join(shared, ", ") + if _, err = tx.Exec(fmt.Sprintf("INSERT INTO %s (%s) SELECT %s FROM users", friendlySqlStr(tempName), columns, columns)); err != nil { + return err + } + } + + if _, err = tx.Exec("DROP TABLE users"); err != nil { + return err + } + if _, err = tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO users", friendlySqlStr(tempName))); err != nil { + return err + } + + for _, column := range schema { + if column.unique && !column.primaryKey { + idxName := fmt.Sprintf("idx_users_%s_unique", column.columnName) + stmt := fmt.Sprintf( + "CREATE UNIQUE INDEX IF NOT EXISTS %s ON users (%s)", + friendlySqlStr(idxName), friendlySqlStr(column.columnName), + ) + if _, err = tx.Exec(stmt); err != nil { + return err + } + } + } + + return tx.Commit() +} + +func buildColumnDef(column UserColumn) string { + parts := []string{friendlySqlStr(column.columnName), strings.ToUpper(strings.TrimSpace(column.valueType))} + if column.primaryKey { + parts = append(parts, "PRIMARY KEY") + } + if column.notNull { + parts = append(parts, "NOT NULL") + } + if column.defaultValue.String != "" { + parts = append(parts, "DEFAULT "+column.defaultValue.String) + } + if column.unique && !column.primaryKey { + parts = append(parts, "UNIQUE") + } + return strings.Join(parts, " ") +} + +// jarvis make sure my strings make SQL happy +func friendlySqlStr(input string) string { + return `"` + strings.ReplaceAll(input, `"`, `""`) + `"` +} + +func normalizeDefault(s string) string { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "(") + s = strings.TrimSuffix(s, ")") + return strings.TrimSpace(s) +} + +// creates a table from the schema into the users database +func populateNewDB(schema []UserColumn) error { + columnConstructors := make([]string, 0, len(schema)) + for _, column := range schema { + columnConstructors = append(columnConstructors, buildColumnDef(column)) } + _, err := userDB.Exec(fmt.Sprintf("CREATE TABLE users (%s)", strings.Join(columnConstructors, ", "))) + return err } // Creates a new user @@ -36,7 +391,7 @@ func NewUser(username string, uuid string) { } //The only reason most of these columns don't have default values is that sqlite doesn't let you alter column default values and I don't feel like deleting and remaking every column - _, err := userDB.Exec("insert into users values(?,?,?,?,?,?,?, 0, 0, ?, 0)", uuid, username, username, nil, string(badgeBytes), 0, DefaultPfpPath, "[]") + _, err := userDB.Exec("insert into users values(?,?,?,?,?,?,?, 0, 0, ?, 0, ?)", uuid, username, username, nil, string(badgeBytes), 0, DefaultPfpPath, "[]", "light") if err != nil { LogErrorf(err, "Problem creating new user with args: %v, %v, %v, %v, %v, %v, %v", uuid, username, username, "nil", badgeBytes, 0, DefaultPfpPath) @@ -55,7 +410,7 @@ func userExists(username string) bool { LogError(scanErr, "Problem scanning response to sql query SELECT COUNT(1) FROM users WHERE username = ? with arg: "+username) } - // If we ever get more than 1, something is horribly wrong.s + // If we ever get more than 1, something is horribly wrong. return resultstore == 1 } @@ -457,12 +812,33 @@ func getPfp(uuid string) string { } // Sets a given user's path to profile picture -func SetPfp(username string, pfp string) { - uuid, _ := GetUUID(username, true) - +func SetPfp(uuid string, pfp string) { _, execErr := userDB.Exec("update users set pfp = ? where uuid = ?", pfp, uuid) if execErr != nil { LogErrorf(execErr, "Problem executing sql query UPDATE users SET pfp = ? WHERE uuid = ? with args: %v, %v", pfp, uuid) } } + +// Gets the relative path of a given user's profile picture +func GetTheme(uuid string) string { + var theme string + response := userDB.QueryRow("select theme from users where uuid = ?", uuid) + scanErr := response.Scan(&theme) + if scanErr != nil { + // LogError(scanErr, "Problem scanning response to sql query SELECT theme FROM users WHERE uuid = ? with arg: "+uuid) + SetTheme(uuid, "light") + return "light" + } + + return theme +} + +// Gets the relative path of a given user's profile picture +func SetTheme(uuid string, themeName string) { + _, execErr := userDB.Exec("update users set theme = ? where uuid = ?", themeName, uuid) + + if execErr != nil { + LogErrorf(execErr, "Problem executing sql query UPDATE users SET theme = ? WHERE uuid = ? with args: %v, %v", themeName, uuid) + } +} diff --git a/internal/utils.go b/internal/utils.go index b1fe1e3..80d9905 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -14,6 +14,24 @@ import ( "strings" ) +func ListAllThemes() ([]string, error) { + files, err := os.ReadDir("run/themes") + if err != nil { + return nil, err + } + var cleanNames []string + for _, file := range files { + if file.IsDir() { + continue + } + fileName := file.Name() + if strings.HasSuffix(fileName, ".css") { + cleanNames = append(cleanNames, strings.TrimSuffix(fileName, ".css")) + } + } + return cleanNames, nil +} + // Simple wrapper for converting bool to string for replays func GetReplayString(isReplay bool) string { if isReplay { @@ -380,7 +398,7 @@ func GetAllMatching(checkAgainst string) []string { for _, jsonFile := range writtenJson { splitFile := strings.Split(jsonFile.Name(), "_") - if len(splitFile) < 4 { + if len(splitFile) < 3 || len(splitAgainst) < 3 { continue } diff --git a/main.go b/main.go index 21dd354..7219b02 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,8 @@ func main() { /// Setup isSetup := slices.Contains(os.Args, "setup") - internal.SetSecureCookies(slices.Contains(os.Args, "test")) + internal.SetSecureCookies(slices.Contains(os.Args, "local")) + internal.UseLocalAuth(slices.Contains(os.Args, "local")) publicHosting := false //Allows setup to bypass ip and domain validation to run localhost serveTLS := false //switches it between https and http [true for https] updateDB := false diff --git a/run/themes/2Slimey.css b/run/themes/2Slimey.css new file mode 100644 index 0000000..99e7cff --- /dev/null +++ b/run/themes/2Slimey.css @@ -0,0 +1,34 @@ +/* viva mexico */ + +:root { + --background-color: #0f0f0f !important; + --font-color: #e8ffe8 !important; + --font-color-prompt-title: #ffffff !important; + --font-color-header: #ffffff !important; + --accent-color: #00ff66 !important; + --accent-color2: #ff1744 !important; + --accent-color3: #1affd5 !important; + --blue-color: #1affd5 !important; + --kelly-green: #00ff66 !important; + --error-color: #ff1744 !important; + --error-message-color: #ff1744 !important; + --disabled-color: #5a5a5a !important; + --faded-text-color: #9cffc7 !important; + --drop-shadow-color: rgba(0, 255, 102, 0.6) !important; + --border-color-faded: #1f1f1f !important; + --border-color-active: #00ff66 !important; + --sidenav-hover-color: #ff1744 !important; + --sidenav-button-color: #1a1a1a !important; + --check-background-color: #00ff66 !important; + --checkbox-default-color: #2a2a2a !important; + --checkbox-hover-color: #3a3a3a !important; +} + +* { + text-shadow: 0 0 6px rgba(0, 255, 102, 0.6); +} + +button, +.accent { + box-shadow: 0 0 12px rgba(0, 255, 102, 0.7); +} diff --git a/run/themes/Best Buy.css b/run/themes/Best Buy.css new file mode 100644 index 0000000..c8bb981 --- /dev/null +++ b/run/themes/Best Buy.css @@ -0,0 +1,24 @@ +:root { + --background-color: #f6f8fb !important; + --font-color: #0b0f14 !important; + --font-color-header: #0b0f14 !important; + --font-color-prompt-title: #0b0f14 !important; + --accent-color: #ffd400 !important; + --accent-color2: #1f4fbf !important; + --accent-color3: #dbe8ff !important; + --blue-color: #2a66ff !important; + --border-color-active: #1f4fbf !important; + --border-color-faded: #d6dde8 !important; + --sidenav-button-color: #eef2f7 !important; + --drop-shadow-color: rgba(11, 15, 20, 0.35) !important; + --sidenav-hover-color: #163b8e !important; + --faded-text-color: #4d5a6b !important; + --disabled-color: #7a8798 !important; + --kelly-green: #2ecc71 !important; + --check-background-color: #2ecc71 !important; + --error-color: #d92d20 !important; + --error-message-color: #d92d20 !important; + --check-background-color: #ffd400 !important; + --checkbox-default-color: #f2f4f7 !important; + --checkbox-hover-color: #e4e8ef !important; +} diff --git a/run/themes/Cloudflare.css b/run/themes/Cloudflare.css new file mode 100644 index 0000000..b40b0a3 --- /dev/null +++ b/run/themes/Cloudflare.css @@ -0,0 +1,24 @@ +/* oh cloudflare why you litttlee if i everrrrr*/ + +:root { + --background-color: #ffffff !important; + --font-color: #111111 !important; + --font-color-prompt-title: #111111 !important; + --font-color-header: #111111 !important; + --accent-color: #f48120 !important; + --accent-color2: #fcb44b !important; + --accent-color3: #ffe2be !important; + --disabled-color: #6b7280 !important; + --faded-text-color: #4b5563 !important; + --border-color-faded: #e5e7eb !important; + --border-color-active: #f48120 !important; + --drop-shadow-color: rgba(17, 24, 39, 0.25) !important; + --sidenav-button-color: #f3f4f6 !important; + --sidenav-hover-color: #2b2f36 !important; + --kelly-green: #22c55e !important; + --check-background-color: #22c55e !important; + --error-color: #ef4444 !important; + --error-message-color: #ef4444 !important; + --checkbox-default-color: #f3f4f6 !important; + --checkbox-hover-color: #e5e7eb !important; +} diff --git a/run/themes/Dark.css b/run/themes/Dark.css new file mode 100644 index 0000000..94bdc32 --- /dev/null +++ b/run/themes/Dark.css @@ -0,0 +1,25 @@ +/* Like the light one but dark */ + +:root { + --background-color: #181a1b !important; + --accent-color: #4c8ed9 !important; + --accent-color2: #3a6ea5 !important; + --accent-color3: #25364a !important; + --blue-color: #4c8ed9 !important; + --error-color: var(--disabled-color) !important; + --disabled-color: #44484b !important; + --font-color: #f5f6fa !important; + --font-color-prompt-title: #f5f6fa !important; + --font-color-header: #f5f6fa !important; + --drop-shadow-color: #101112 !important; + --sidenav-hover-color: #222 !important; + --faded-text-color: #a0a0a0 !important; + --kelly-green: #4cbb17 !important; + --error-message-color: var(--disabled-color) !important; + --check-background-color: #4cbb17 !important; + --checkbox-default-color: #222 !important; + --checkbox-hover-color: #444 !important; + --border-color-faded: #23272a !important; + --border-color-active: #4c8ed9 !important; + --sidenav-button-color: #23272a !important; +} diff --git a/run/themes/FL Studio.css b/run/themes/FL Studio.css new file mode 100644 index 0000000..0db1d4d --- /dev/null +++ b/run/themes/FL Studio.css @@ -0,0 +1,25 @@ +/* when fl studio die */ + +:root { + --background-color: #223b44 !important; + --accent-color: #ff8a1f !important; + --accent-color2: #ffa84b !important; + --accent-color3: #ffd2a3 !important; + --blue-color: #6fbad1 !important; + --error-color: #ff4d4d !important; + --disabled-color: #7f969d !important; + --font-color: #eef6f8 !important; + --font-color-prompt-title: #f6fbff !important; + --font-color-header: #f6fbff !important; + --drop-shadow-color: rgba(0, 0, 0, 0.45) !important; + --sidenav-hover-color: #3b6674 !important; + --faded-text-color: #b6c9cf !important; + --kelly-green: #63d13a !important; + --error-message-color: #ff6b6b !important; + --check-background-color: #63d13a !important; + --checkbox-default-color: #1d3138 !important; + --checkbox-hover-color: #24404a !important; + --border-color-faded: #365c67 !important; + --border-color-active: #ff8a1f !important; + --sidenav-button-color: #2b4a55 !important; +} diff --git a/run/themes/Hot Pink.css b/run/themes/Hot Pink.css new file mode 100644 index 0000000..6c5a18d --- /dev/null +++ b/run/themes/Hot Pink.css @@ -0,0 +1,25 @@ +/* Theme de Jarrpa */ + +:root { + --background-color: #fff0f6 !important; + --accent-color: #ff4da6 !important; + --accent-color2: #ff66b2 !important; + --accent-color3: #ffd6eb !important; + --blue-color: #ff4da6 !important; + --error-color: #ff99c8 !important; + --disabled-color: #b36b8c !important; + --font-color: #1a1a1a !important; + --font-color-prompt-title: #1a1a1a !important; + --font-color-header: #1a1a1a !important; + --drop-shadow-color: rgba(255, 77, 166, 0.4) !important; + --sidenav-hover-color: #ff3399 !important; + --faded-text-color: #a64d79 !important; + --kelly-green: #ff4da6 !important; + --error-message-color: #ff99c8 !important; + --check-background-color: #ff4da6 !important; + --checkbox-default-color: #ffe6f2 !important; + --checkbox-hover-color: #ffb3d9 !important; + --border-color-faded: #ffcce6 !important; + --border-color-active: #ff4da6 !important; + --sidenav-button-color: #ffd6eb !important; +} diff --git a/run/themes/Light.css b/run/themes/Light.css new file mode 100644 index 0000000..f74fb82 --- /dev/null +++ b/run/themes/Light.css @@ -0,0 +1,25 @@ +/* Classic GreenScout Theme */ + +:root { + --background-color: white !important; + --accent-color: #9bcbfb !important; + --accent-color2: #8fbdeb !important; + --accent-color3: #cee5ff !important; + --blue-color: #9bcbfb !important; + --error-color: var(--disabled-color) !important; + --disabled-color: #717172 !important; + --font-color: black !important; + --font-color-prompt-title: black !important; + --font-color-header: black !important; + --drop-shadow-color: #626263 !important; + --sidenav-hover-color: #444 !important; + --faded-text-color: #5f6167 !important; + --kelly-green: #4cbb17 !important; + --error-message-color: var(--disabled-color) !important; + --check-background-color: #4cbb17 !important; + --checkbox-default-color: #eee !important; + --checkbox-hover-color: #ccc !important; + --border-color-faded: #d9d9db !important; + --border-color-active: #417096 !important; + --sidenav-button-color: #dfdfdf !important; +} diff --git a/run/themes/Pittsburgh.css b/run/themes/Pittsburgh.css new file mode 100644 index 0000000..d8ca213 --- /dev/null +++ b/run/themes/Pittsburgh.css @@ -0,0 +1,25 @@ +/* the theme from the 2026 Pittsburgh trip */ + +:root { + --background-color: #141415 !important; + --accent-color: #4cbb17 !important; + --accent-color2: #4cbb17 !important; + --accent-color3: #4cbb17 !important; + --blue-color: #9bcbfb !important; + --error-color: var(--disabled-color) !important; + --disabled-color: #717172 !important; + --font-color: white !important; + --font-color-prompt-title: #4cbb17 !important; + --font-color-header: #58db1c !important; + --drop-shadow-color: #626263 !important; + --sidenav-hover-color: #444 !important; + --faded-text-color: rgb(210, 209, 209) !important; + --kelly-green: #4cbb17 !important; + --error-message-color: var(--disabled-color) !important; + --check-background-color: #4cbb17 !important; + --checkbox-default-color: #eee !important; + --checkbox-hover-color: #ccc !important; + --border-color-faded: #d9d9db !important; + --border-color-active: #4cbb17 !important; + --sidenav-button-color: #626263 !important; +} diff --git a/run/themes/Prettifun.css b/run/themes/Prettifun.css new file mode 100644 index 0000000..95a162d --- /dev/null +++ b/run/themes/Prettifun.css @@ -0,0 +1,25 @@ +/* pretti! buahahaha */ + +:root { + --background-color: #00c8e6 !important; + --accent-color: #ff3300 !important; + --accent-color2: #ff8c00 !important; + --accent-color3: #c400d4 !important; + --blue-color: #1b3fa8 !important; + --error-color: #ff3300 !important; + --disabled-color: #5a7ab0 !important; + --font-color: #001833 !important; + --font-color-prompt-title: #001833 !important; + --font-color-header: #ffffff !important; + --drop-shadow-color: #003344 !important; + --sidenav-hover-color: #ff3300 !important; + --faded-text-color: #004466 !important; + --kelly-green: #39ff14 !important; + --error-message-color: #ff3300 !important; + --check-background-color: #39ff14 !important; + --checkbox-default-color: #b3eeff !important; + --checkbox-hover-color: #ffe500 !important; + --border-color-faded: #00a0bb !important; + --border-color-active: #ff3300 !important; + --sidenav-button-color: #b3eeff !important; +} diff --git a/run/themes/Whatsapp.css b/run/themes/Whatsapp.css new file mode 100644 index 0000000..1b73535 --- /dev/null +++ b/run/themes/Whatsapp.css @@ -0,0 +1,25 @@ +/* Dear future devs, please make the background whatsapp half-life λ*/ + +:root { + --background-color: #ece5dd !important; + --accent-color: #25d366 !important; + --accent-color2: #075e54 !important; + --accent-color3: #dcf8c6 !important; + --blue-color: #34b7f1 !important; + --error-color: #f44336 !important; + --disabled-color: #bdbdbd !important; + --font-color: #222 !important; + --font-color-prompt-title: #075e54 !important; + --font-color-header: #075e54 !important; + --drop-shadow-color: #0002 !important; + --sidenav-hover-color: #f0f0f0 !important; + --faded-text-color: #888 !important; + --kelly-green: #25d366 !important; + --error-message-color: #f44336 !important; + --check-background-color: #25d366 !important; + --checkbox-default-color: #fff !important; + --checkbox-hover-color: #dcf8c6 !important; + --border-color-faded: #ece5dd !important; + --border-color-active: #25d366 !important; + --sidenav-button-color: #fff !important; +} diff --git a/run/themes/light.css b/run/themes/light.css deleted file mode 100644 index 1a141b6..0000000 --- a/run/themes/light.css +++ /dev/null @@ -1,84 +0,0 @@ -:root { - /* Color Variables */ - /*--background-color: #f8f9fe; - --accent-color: #9bcbfb; - --accent-color2: #8fbdeb; - --accent-color3: #cee5ff; - --error-color: lightcoral; - --disabled-color: #a1a2a6; - --font-color: #1e2226; - --sidenav-hover-color: #444; - --faded-text-color: #5f6167; - --kelly-green: #4cbb17; /* Kelly Green is the color of Edina's "The Green Machine" - --error-message-color: #d32f2f; - --check-background-color: #2f628d; - --checkbox-default-color: #eee; - --checkbox-hover-color: #ccc; - --border-color-gray: #93979f; - --border-color-blue: #417096; - --sidenav-button-color: #dfdfdf;*/ - /* General Sizing Variables */ - --background-color: #ff0000 !important; - --accent-color: #ff1a1a !important; - --accent-color2: #ff3333 !important; - --accent-color3: #ff6666 !important; - --error-color: #ff0000 !important; - --disabled-color: #ff8080 !important; - --font-color: #7a0000 !important; - --sidenav-hover-color: #b30000 !important; - --faded-text-color: #cc0000 !important; - --kelly-green: #ff0000 !important; /* Kelly Green is the color of Edina's "The Green Machine" */ - --error-message-color: #ff0000 !important; - --check-background-color: #990000 !important; - --checkbox-default-color: #ffcccc !important; - --checkbox-hover-color: #ffb3b3 !important; - --border-color-gray: #cc0000 !important; - --border-color-blue: #ff0000 !important; - --sidenav-button-color: #ff9999 !important; - --font-size-large: 3vh; - --font-size-small: 2.5vh; - --page-height: 100vh; - --page-width: 100vw; - --content-height: calc(var(--page-height) - var(--navbar-height)); - --content-width: var(--page-width); - /* Navbar Sizing Variables */ - --sidenav-width: 70vw; - --navbar-height: 8vh; - --sidenav-usertile-height: 0%; /* Set to 0 while user tile is diabled */ - --sidenav-usertile-width: 15vh; - --sidenav-section-height: calc((100% - var(--sidenav-usertile-height)) / 2); - /* Login Sizing Variables */ - - /* Match Form Sizing Variables */ - - /* Match Form Checkbox Sizing Variables */ - - /* Match Form Counter Sizing Variables */ - - /* Match Form Dropdown Sizing Variables */ - - /* Match Form Trigger Sizing Variables */ - - /* Match Form Submit/Replay Sizing Variables */ - - /* General Cycles Sizing Variables */ - --font-size-cycle: 1.8vh; - /* Cycle Start/Stop Button Sizing Variables */ - - /* Score Cycle Sizing Variables */ - - /* Shuttle Cycle Sizing Variables */ - - /* Home Page Sizing Variables */ - --refresh-button-size: 7.9vh; - --new-matchpit-button-height: 8vh; - /* Sizing Variables */ - - /* Leader Board Sizing Variables */ - --leader-board-element-height: 5vh; - - /* Settings Sizing Variables */ - --settings-element-width: 80%; - --settings-element-height: calc(var(--font-size-large) * 2); - --settings-element-spacing: calc(var(--font-size-small) / 2); -}