summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLizzy Hunt <lizzy.hunt@usu.edu>2024-03-27 15:02:31 -0600
committerLizzy Hunt <lizzy.hunt@usu.edu>2024-03-27 15:02:31 -0600
commit0dc2679005e70c50024bc49e750f3998a0c4c24b (patch)
tree73153522195608ee2ed3bbb4c2ed3cbc621b6b07
parent8d65f4e23026dce5d04e9a4afaf216f0732482a6 (diff)
downloadhatecomputers.club-0dc2679005e70c50024bc49e750f3998a0c4c24b.tar.gz
hatecomputers.club-0dc2679005e70c50024bc49e750f3998a0c4c24b.zip
authentication! oauth2!
-rw-r--r--.env.example6
-rw-r--r--api/auth.go245
-rw-r--r--api/serve.go42
-rw-r--r--api/template.go7
-rw-r--r--args/args.go47
-rw-r--r--database/migrate.go26
-rw-r--r--database/users.go109
-rw-r--r--go.mod8
-rw-r--r--go.sum22
-rw-r--r--main.go2
-rw-r--r--templates/base.html6
-rw-r--r--utils/RandomId.go19
12 files changed, 513 insertions, 26 deletions
diff --git a/.env.example b/.env.example
index af80e67..bef9b08 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,8 @@
CLOUDFLARE_TOKEN=
CLOUDFLARE_ZONE=
+
+OAUTH_CLIENT_ID
+OAUTH_CLIENT_SECRET
+OAUTH_SCOPES=profile,openid,email
+OAUTH_AUTH_URL=https://auth.hatecomputers.club/ui/oauth2
+OAUTH_TOKEN_URL=https://auth.hatecomputers.club/oauth2/token \ No newline at end of file
diff --git a/api/auth.go b/api/auth.go
new file mode 100644
index 0000000..4733971
--- /dev/null
+++ b/api/auth.go
@@ -0,0 +1,245 @@
+package api
+
+import (
+ "crypto/sha256"
+ "database/sql"
+ "encoding/base64"
+ "encoding/json"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/utils"
+ "golang.org/x/oauth2"
+)
+
+func StartSessionContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ verifier := utils.RandomId() + utils.RandomId()
+
+ sha2 := sha256.New()
+ io.WriteString(sha2, verifier)
+ codeChallenge := base64.RawURLEncoding.EncodeToString(sha2.Sum(nil))
+
+ state := utils.RandomId()
+ url := context.Args.OauthConfig.AuthCodeURL(state, oauth2.SetAuthURLParam("code_challenge_method", "S256"), oauth2.SetAuthURLParam("code_challenge", codeChallenge))
+
+ http.SetCookie(resp, &http.Cookie{
+ Name: "verifier",
+ Value: verifier,
+ Path: "/",
+ Secure: true,
+ SameSite: http.SameSiteLaxMode,
+ MaxAge: 60,
+ })
+ http.SetCookie(resp, &http.Cookie{
+ Name: "state",
+ Value: state,
+ Path: "/",
+ Secure: true,
+ SameSite: http.SameSiteLaxMode,
+ MaxAge: 60,
+ })
+
+ http.Redirect(resp, req, url, http.StatusFound)
+ return success(context, req, resp)
+ }
+}
+
+func InterceptCodeContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ state := req.URL.Query().Get("state")
+ code := req.URL.Query().Get("code")
+
+ if code == "" || state == "" {
+ resp.WriteHeader(http.StatusBadRequest)
+ return failure(context, req, resp)
+ }
+
+ if !verifyState(req, "state", state) {
+ resp.WriteHeader(http.StatusBadRequest)
+ return failure(context, req, resp)
+ }
+ verifierCookie, err := req.Cookie("verifier")
+ if err != nil {
+ resp.WriteHeader(http.StatusBadRequest)
+ return failure(context, req, resp)
+ }
+
+ reqContext := req.Context()
+ token, err := context.Args.OauthConfig.Exchange(reqContext, code, oauth2.SetAuthURLParam("code_verifier", verifierCookie.Value))
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ client := context.Args.OauthConfig.Client(reqContext, token)
+ user, err := getOauthUser(context.DBConn, client, context.Args.OauthUserInfoURI)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+
+ return failure(context, req, resp)
+ }
+
+ session, err := database.MakeUserSessionFor(context.DBConn, user)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ http.SetCookie(resp, &http.Cookie{
+ Name: "session",
+ Value: session.ID,
+ Path: "/",
+ SameSite: http.SameSiteLaxMode,
+ Secure: true,
+ })
+
+ redirect := "/"
+ redirectCookie, err := req.Cookie("redirect")
+ if err == nil && redirectCookie.Value != "" {
+ redirect = redirectCookie.Value
+ http.SetCookie(resp, &http.Cookie{
+ Name: "redirect",
+ MaxAge: 0,
+ })
+ }
+
+ http.Redirect(resp, req, redirect, http.StatusFound)
+ return success(context, req, resp)
+ }
+}
+
+func VerifySessionContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ sessionCookie, err := req.Cookie("session")
+ if err != nil {
+ resp.WriteHeader(http.StatusUnauthorized)
+ return failure(context, req, resp)
+ }
+
+ session, err := database.GetSession(context.DBConn, sessionCookie.Value)
+ if err == nil && session.ExpireAt.Before(time.Now()) {
+ session = nil
+ database.DeleteSession(context.DBConn, sessionCookie.Value)
+ }
+ if err != nil || session == nil {
+ http.SetCookie(resp, &http.Cookie{
+ Name: "session",
+ MaxAge: 0,
+ })
+
+ return failure(context, req, resp)
+ }
+
+ user, err := database.GetUser(context.DBConn, session.UserID)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusUnauthorized)
+ return failure(context, req, resp)
+ }
+
+ context.User = user
+ return success(context, req, resp)
+ }
+}
+
+func GoLoginContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ http.SetCookie(resp, &http.Cookie{
+ Name: "redirect",
+ Value: req.URL.Path,
+ Path: "/",
+ Secure: true,
+ SameSite: http.SameSiteLaxMode,
+ })
+
+ http.Redirect(resp, req, "/login", http.StatusFound)
+ return failure(context, req, resp)
+ }
+}
+
+func RefreshSessionContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ sessionCookie, err := req.Cookie("session")
+ if err != nil {
+ resp.WriteHeader(http.StatusUnauthorized)
+ return failure(context, req, resp)
+ }
+
+ _, err = database.RefreshSession(context.DBConn, sessionCookie.Value)
+ if err != nil {
+ resp.WriteHeader(http.StatusUnauthorized)
+ return failure(context, req, resp)
+ }
+
+ return success(context, req, resp)
+ }
+}
+
+func LogoutContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ sessionCookie, err := req.Cookie("session")
+ if err == nil && sessionCookie.Value != "" {
+ _ = database.DeleteSession(context.DBConn, sessionCookie.Value)
+ }
+
+ http.Redirect(resp, req, "/", http.StatusFound)
+ http.SetCookie(resp, &http.Cookie{
+ Name: "session",
+ MaxAge: 0,
+ })
+ return success(context, req, resp)
+ }
+}
+
+func getOauthUser(dbConn *sql.DB, client *http.Client, uri string) (*database.User, error) {
+ userResponse, err := client.Get(uri)
+ if err != nil {
+ return nil, err
+ }
+
+ userStruct, err := createUserFromResponse(userResponse)
+ if err != nil {
+ return nil, err
+ }
+
+ user, err := database.FindOrSaveUser(dbConn, userStruct)
+ if err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
+func createUserFromResponse(response *http.Response) (*database.User, error) {
+ defer response.Body.Close()
+ user := &database.User{
+ CreatedAt: time.Now(),
+ }
+ err := json.NewDecoder(response.Body).Decode(user)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+
+ user.Username = strings.ToLower(user.Username)
+ user.Username = strings.Split(user.Username, "@")[0]
+
+ return user, nil
+}
+
+func verifyState(req *http.Request, stateCookieName string, expectedState string) bool {
+ cookie, err := req.Cookie(stateCookieName)
+ if err != nil || cookie.Value != expectedState {
+ return false
+ }
+
+ return true
+}
diff --git a/api/serve.go b/api/serve.go
index 2b95297..df30e76 100644
--- a/api/serve.go
+++ b/api/serve.go
@@ -1,7 +1,6 @@
package api
import (
- "crypto/rand"
"database/sql"
"fmt"
"log"
@@ -9,6 +8,8 @@ import (
"time"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/args"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/utils"
)
type RequestContext struct {
@@ -17,28 +18,17 @@ type RequestContext struct {
Id string
Start time.Time
+
+ User *database.User
}
type Continuation func(*RequestContext, *http.Request, http.ResponseWriter) ContinuationChain
type ContinuationChain func(Continuation, Continuation) ContinuationChain
-func randomId() string {
- uuid := make([]byte, 16)
- _, err := rand.Read(uuid)
- if err != nil {
- panic(err)
- }
-
- uuid[8] = uuid[8]&^0xc0 | 0x80
- uuid[6] = uuid[6]&^0xf0 | 0x40
-
- return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
-}
-
func LogRequestContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
return func(success Continuation, _failure Continuation) ContinuationChain {
context.Start = time.Now()
- context.Id = randomId()
+ context.Id = utils.RandomId()
log.Println(req.Method, req.URL.Path, req.RemoteAddr, context.Id)
return success(context, req, resp)
@@ -90,7 +80,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext()
- LogRequestContinuation(requestContext, r, w)(TemplateContinuation("home.html", nil, true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(TemplateContinuation("home.html", nil, true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
@@ -98,6 +88,26 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
LogRequestContinuation(requestContext, r, w)(HealthCheckContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
+ mux.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(StartSessionContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("GET /auth", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(InterceptCodeContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("GET /me", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(RefreshSessionContinuation, GoLoginContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("GET /logout", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(LogoutContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext()
name := r.PathValue("name")
diff --git a/api/template.go b/api/template.go
index c666029..a4ccfa8 100644
--- a/api/template.go
+++ b/api/template.go
@@ -22,6 +22,13 @@ func renderTemplate(context *RequestContext, templateName string, showBaseHtml b
return bytes.Buffer{}, err
}
+ if data == nil {
+ data = map[string]interface{}{}
+ }
+ if context.User != nil {
+ data.(map[string]interface{})["User"] = context.User
+ }
+
var buffer bytes.Buffer
err = tmpl.ExecuteTemplate(&buffer, "base", data)
diff --git a/args/args.go b/args/args.go
index 9176d27..a360d57 100644
--- a/args/args.go
+++ b/args/args.go
@@ -4,6 +4,9 @@ import (
"errors"
"flag"
"os"
+ "strings"
+
+ "golang.org/x/oauth2"
)
type Arguments struct {
@@ -15,6 +18,9 @@ type Arguments struct {
Port int
Server bool
Migrate bool
+
+ OauthConfig *oauth2.Config
+ OauthUserInfoURI string
}
func GetArgs() (*Arguments, error) {
@@ -31,11 +37,41 @@ func GetArgs() (*Arguments, error) {
cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN")
cloudflareZone := os.Getenv("CLOUDFLARE_ZONE")
- if cloudflareToken == "" {
- return nil, errors.New("please set the CLOUDFLARE_TOKEN environment variable")
+ oauthClientID := os.Getenv("OAUTH_CLIENT_ID")
+ oauthClientSecret := os.Getenv("OAUTH_CLIENT_SECRET")
+ oauthScopes := os.Getenv("OAUTH_SCOPES")
+ oauthAuthURL := os.Getenv("OAUTH_AUTH_URL")
+ oauthTokenURL := os.Getenv("OAUTH_TOKEN_URL")
+ oauthRedirectURI := os.Getenv("OAUTH_REDIRECT_URI")
+ oauthUserInfoURI := os.Getenv("OAUTH_USER_INFO_URI")
+
+ envVars := [][]string{
+ {cloudflareToken, "CLOUDFLARE_TOKEN"},
+ {cloudflareZone, "CLOUDFLARE_ZONE"},
+ {oauthClientID, "OAUTH_CLIENT_ID"},
+ {oauthClientSecret, "OAUTH_CLIENT_SECRET"},
+ {oauthScopes, "OAUTH_SCOPES"},
+ {oauthAuthURL, "OAUTH_AUTH_URL"},
+ {oauthTokenURL, "OAUTH_TOKEN_URL"},
+ {oauthRedirectURI, "OAUTH_REDIRECT_URI"},
+ {oauthUserInfoURI, "OAUTH_USER_INFO_URI"},
}
- if cloudflareZone == "" {
- return nil, errors.New("please set the CLOUDFLARE_ZONE environment variable")
+
+ for _, envVar := range envVars {
+ if envVar[0] == "" {
+ return nil, errors.New("please set the " + envVar[1] + " environment variable")
+ }
+ }
+
+ oauthConfig := &oauth2.Config{
+ ClientID: oauthClientID,
+ ClientSecret: oauthClientSecret,
+ Scopes: strings.Split(oauthScopes, ","),
+ Endpoint: oauth2.Endpoint{
+ AuthURL: oauthAuthURL,
+ TokenURL: oauthTokenURL,
+ },
+ RedirectURL: oauthRedirectURI,
}
arguments := &Arguments{
@@ -47,6 +83,9 @@ func GetArgs() (*Arguments, error) {
Port: *port,
Server: *server,
Migrate: *migrate,
+
+ OauthConfig: oauthConfig,
+ OauthUserInfoURI: oauthUserInfoURI,
}
return arguments, nil
diff --git a/database/migrate.go b/database/migrate.go
index f10e03b..b75c123 100644
--- a/database/migrate.go
+++ b/database/migrate.go
@@ -13,8 +13,10 @@ func MigrateUsers(dbConn *sql.DB) (*sql.DB, error) {
log.Println("migrating users table")
_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS users (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
+ id TEXT PRIMARY KEY,
+ mail TEXT NOT NULL,
username TEXT NOT NULL,
+ display_name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`)
if err != nil {
@@ -37,7 +39,7 @@ func MigrateApiKeys(dbConn *sql.DB) (*sql.DB, error) {
key TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (user_id) REFERENCES users (id)
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);`)
if err != nil {
return dbConn, err
@@ -49,18 +51,33 @@ func MigrateDNSRecords(dbConn *sql.DB) (*sql.DB, error) {
log.Println("migrating dns_records table")
_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS dns_records (
- id INTEGER PRIMARY KEY,
+ id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
content TEXT NOT NULL,
ttl INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (user_id) REFERENCES users (id)
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE);`)
+ if err != nil {
+ return dbConn, err
+ }
+ return dbConn, nil
+}
+
+func MigrateUserSessions(dbConn *sql.DB) (*sql.DB, error) {
+ log.Println("migrating user_sessions table")
+
+ _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS user_sessions (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ expire_at TIMESTAMP NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);`)
if err != nil {
return dbConn, err
}
+
return dbConn, nil
}
@@ -69,6 +86,7 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) {
migrations := []Migrator{
MigrateUsers,
+ MigrateUserSessions,
MigrateApiKeys,
MigrateDNSRecords,
}
diff --git a/database/users.go b/database/users.go
index 6fb2601..1ba4ebb 100644
--- a/database/users.go
+++ b/database/users.go
@@ -1,5 +1,112 @@
package database
-func getUsers() {
+import (
+ "database/sql"
+ "log"
+ "time"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+const (
+ ExpiryDuration = time.Hour * 24
+)
+
+type User struct {
+ ID string `json:"sub"`
+ Mail string `json:"email"`
+ Username string `json:"preferred_username"`
+ DisplayName string `json:"name"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+type UserSession struct {
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ ExpireAt time.Time `json:"expire_at"`
+}
+
+func GetUser(dbConn *sql.DB, id string) (*User, error) {
+ row := dbConn.QueryRow(`SELECT id, mail, username, display_name, created_at FROM users WHERE id = ?;`, id)
+
+ var user User
+ err := row.Scan(&user.ID, &user.Mail, &user.Username, &user.DisplayName, &user.CreatedAt)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+
+ return &user, nil
+}
+
+func FindOrSaveUser(dbConn *sql.DB, user *User) (*User, error) {
+ _, err := dbConn.Exec(`INSERT OR REPLACE INTO users (id, mail, username, display_name) VALUES (?, ?, ?, ?);`, user.ID, user.Mail, user.Username, user.DisplayName)
+ if err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
+func MakeUserSessionFor(dbConn *sql.DB, user *User) (*UserSession, error) {
+ expireAt := time.Now().Add(time.Hour * 12)
+
+ _, err := dbConn.Exec(`INSERT OR REPLACE INTO user_sessions (id, user_id, expire_at) VALUES (?, ?, ?);`, user.ID, user.ID, time.Now().Add(ExpiryDuration))
+
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+
+ return &UserSession{
+ ID: user.ID,
+ UserID: user.ID,
+ ExpireAt: expireAt,
+ }, nil
+}
+
+func GetSession(dbConn *sql.DB, sessionId string) (*UserSession, error) {
+ row := dbConn.QueryRow(`SELECT id, user_id, expire_at FROM user_sessions WHERE id = ?;`, sessionId)
+
+ var id, userId string
+ var expireAt time.Time
+ err := row.Scan(&id, &userId, &expireAt)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+
+ return &UserSession{
+ ID: id,
+ UserID: userId,
+ ExpireAt: expireAt,
+ }, nil
+}
+
+func DeleteSession(dbConn *sql.DB, sessionId string) error {
+ _, err := dbConn.Exec(`DELETE FROM user_sessions WHERE id = ?;`, sessionId)
+ if err != nil {
+ log.Println(err)
+ return err
+ }
+
+ return nil
+}
+
+func RefreshSession(dbConn *sql.DB, sessionId string) (*UserSession, error) {
+ newExpireAt := time.Now().Add(ExpiryDuration)
+
+ _, err := dbConn.Exec(`UPDATE user_sessions SET expire_at = ? WHERE id = ?;`, newExpireAt, sessionId)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+
+ session, err := GetSession(dbConn, sessionId)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+
+ return session, nil
}
diff --git a/go.mod b/go.mod
index 96a831f..adf01a9 100644
--- a/go.mod
+++ b/go.mod
@@ -6,3 +6,11 @@ require (
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.22
)
+
+require (
+ github.com/golang/protobuf v1.5.3 // indirect
+ golang.org/x/net v0.22.0 // indirect
+ golang.org/x/oauth2 v0.18.0 // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/protobuf v1.31.0 // indirect
+)
diff --git a/go.sum b/go.sum
index c866887..66ea452 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,26 @@
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
diff --git a/main.go b/main.go
index 2d7771b..afa9289 100644
--- a/main.go
+++ b/main.go
@@ -35,10 +35,10 @@ func main() {
if argv.Server {
server := api.MakeServer(argv, dbConn)
- log.Println("server listening on port", argv.Port)
err = server.ListenAndServe()
if err != nil {
log.Fatal(err)
}
+ log.Println("🚀🚀 server listening on port", argv.Port)
}
}
diff --git a/templates/base.html b/templates/base.html
index fcd978e..1846493 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -22,6 +22,12 @@
<div class="header">
<h1>hatecomputers.club</h1>
<a href="javascript:void(0);" id="theme-switcher"></a>
+ <span> | </span>
+ {{ if .User }}
+ <a href="/logout">logout, {{ .User.DisplayName }}.</a>
+ {{ else }}
+ <a href="/login">login.</a>
+ {{ end }}
</div>
<hr>
diff --git a/utils/RandomId.go b/utils/RandomId.go
new file mode 100644
index 0000000..09f089d
--- /dev/null
+++ b/utils/RandomId.go
@@ -0,0 +1,19 @@
+package utils
+
+import (
+ "crypto/rand"
+ "fmt"
+)
+
+func RandomId() string {
+ uuid := make([]byte, 16)
+ _, err := rand.Read(uuid)
+ if err != nil {
+ panic(err)
+ }
+
+ uuid[8] = uuid[8]&^0xc0 | 0x80
+ uuid[6] = uuid[6]&^0xf0 | 0x40
+
+ return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
+}