Compare commits

..

2 Commits

Author SHA1 Message Date
Lizzy Hunt 0dc2679005
authentication! oauth2!
continuous-integration/drone/push Build is passing Details
2024-03-27 15:02:31 -06:00
Elizabeth Hunt 8d65f4e230
fix padding on container 2024-03-26 23:53:42 -06:00
13 changed files with 514 additions and 27 deletions

View File

@ -1,2 +1,8 @@
CLOUDFLARE_TOKEN= CLOUDFLARE_TOKEN=
CLOUDFLARE_ZONE= 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

245
api/auth.go Normal file
View File

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

View File

@ -1,7 +1,6 @@
package api package api
import ( import (
"crypto/rand"
"database/sql" "database/sql"
"fmt" "fmt"
"log" "log"
@ -9,6 +8,8 @@ import (
"time" "time"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/args" "git.hatecomputers.club/hatecomputers/hatecomputers.club/args"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/utils"
) )
type RequestContext struct { type RequestContext struct {
@ -17,28 +18,17 @@ type RequestContext struct {
Id string Id string
Start time.Time Start time.Time
User *database.User
} }
type Continuation func(*RequestContext, *http.Request, http.ResponseWriter) ContinuationChain type Continuation func(*RequestContext, *http.Request, http.ResponseWriter) ContinuationChain
type ContinuationChain func(Continuation, Continuation) 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 { func LogRequestContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
return func(success Continuation, _failure Continuation) ContinuationChain { return func(success Continuation, _failure Continuation) ContinuationChain {
context.Start = time.Now() context.Start = time.Now()
context.Id = randomId() context.Id = utils.RandomId()
log.Println(req.Method, req.URL.Path, req.RemoteAddr, context.Id) log.Println(req.Method, req.URL.Path, req.RemoteAddr, context.Id)
return success(context, req, resp) 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) { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext() 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) { 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) 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) { mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext() requestContext := makeRequestContext()
name := r.PathValue("name") name := r.PathValue("name")

View File

@ -22,6 +22,13 @@ func renderTemplate(context *RequestContext, templateName string, showBaseHtml b
return bytes.Buffer{}, err 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 var buffer bytes.Buffer
err = tmpl.ExecuteTemplate(&buffer, "base", data) err = tmpl.ExecuteTemplate(&buffer, "base", data)

View File

@ -4,6 +4,9 @@ import (
"errors" "errors"
"flag" "flag"
"os" "os"
"strings"
"golang.org/x/oauth2"
) )
type Arguments struct { type Arguments struct {
@ -15,6 +18,9 @@ type Arguments struct {
Port int Port int
Server bool Server bool
Migrate bool Migrate bool
OauthConfig *oauth2.Config
OauthUserInfoURI string
} }
func GetArgs() (*Arguments, error) { func GetArgs() (*Arguments, error) {
@ -31,11 +37,41 @@ func GetArgs() (*Arguments, error) {
cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN") cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN")
cloudflareZone := os.Getenv("CLOUDFLARE_ZONE") cloudflareZone := os.Getenv("CLOUDFLARE_ZONE")
if cloudflareToken == "" { oauthClientID := os.Getenv("OAUTH_CLIENT_ID")
return nil, errors.New("please set the CLOUDFLARE_TOKEN environment variable") 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{ arguments := &Arguments{
@ -47,6 +83,9 @@ func GetArgs() (*Arguments, error) {
Port: *port, Port: *port,
Server: *server, Server: *server,
Migrate: *migrate, Migrate: *migrate,
OauthConfig: oauthConfig,
OauthUserInfoURI: oauthUserInfoURI,
} }
return arguments, nil return arguments, nil

View File

@ -13,8 +13,10 @@ func MigrateUsers(dbConn *sql.DB) (*sql.DB, error) {
log.Println("migrating users table") log.Println("migrating users table")
_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS users ( _, 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, username TEXT NOT NULL,
display_name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`) );`)
if err != nil { if err != nil {
@ -37,7 +39,7 @@ func MigrateApiKeys(dbConn *sql.DB) (*sql.DB, error) {
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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 { if err != nil {
return dbConn, err return dbConn, err
@ -49,18 +51,33 @@ func MigrateDNSRecords(dbConn *sql.DB) (*sql.DB, error) {
log.Println("migrating dns_records table") log.Println("migrating dns_records table")
_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS dns_records ( _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS dns_records (
id INTEGER PRIMARY KEY, id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
type TEXT NOT NULL, type TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
ttl INTEGER NOT NULL, ttl INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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 { if err != nil {
return dbConn, err return dbConn, err
} }
return dbConn, nil return dbConn, nil
} }
@ -69,6 +86,7 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) {
migrations := []Migrator{ migrations := []Migrator{
MigrateUsers, MigrateUsers,
MigrateUserSessions,
MigrateApiKeys, MigrateApiKeys,
MigrateDNSRecords, MigrateDNSRecords,
} }

View File

@ -1,5 +1,112 @@
package database 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
} }

8
go.mod
View File

@ -6,3 +6,11 @@ require (
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.22 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
)

22
go.sum
View File

@ -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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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=

View File

@ -35,10 +35,10 @@ func main() {
if argv.Server { if argv.Server {
server := api.MakeServer(argv, dbConn) server := api.MakeServer(argv, dbConn)
log.Println("server listening on port", argv.Port)
err = server.ListenAndServe() err = server.ListenAndServe()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Println("🚀🚀 server listening on port", argv.Port)
} }
} }

View File

@ -33,9 +33,9 @@ a:hover {
.container { .container {
max-width: 1600px; max-width: 1600px;
margin: auto; margin: auto;
margin-top: 1rem;
background-color: var(--container-bg); background-color: var(--container-bg);
padding: 1rem; padding: 1rem;
opacity: 0.95;
} }
hr { hr {

View File

@ -22,6 +22,12 @@
<div class="header"> <div class="header">
<h1>hatecomputers.club</h1> <h1>hatecomputers.club</h1>
<a href="javascript:void(0);" id="theme-switcher"></a> <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> </div>
<hr> <hr>

19
utils/RandomId.go Normal file
View File

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