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 @@

hatecomputers.club

+ | + {{ if .User }} + logout, {{ .User.DisplayName }}. + {{ else }} + login. + {{ end }}

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:]) +}