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_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
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")

View File

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

View File

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

View File

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

View File

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

8
go.mod
View File

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

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/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=

View File

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

View File

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

View File

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

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