Compare commits
No commits in common. "0dc2679005e70c50024bc49e750f3998a0c4c24b" and "6451109f3d07d0430f21ff56653460f94aa8f3e5" have entirely different histories.
0dc2679005
...
6451109f3d
|
@ -1,8 +1,2 @@
|
||||||
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
245
api/auth.go
|
@ -1,245 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
42
api/serve.go
42
api/serve.go
|
@ -1,6 +1,7 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
@ -8,8 +9,6 @@ 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 {
|
||||||
|
@ -18,17 +17,28 @@ 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 = utils.RandomId()
|
context.Id = 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)
|
||||||
|
@ -80,7 +90,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)(VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(TemplateContinuation("home.html", nil, true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
LogRequestContinuation(requestContext, r, w)(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) {
|
||||||
|
@ -88,26 +98,6 @@ 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")
|
||||||
|
|
|
@ -22,13 +22,6 @@ 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)
|
||||||
|
|
||||||
|
|
47
args/args.go
47
args/args.go
|
@ -4,9 +4,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Arguments struct {
|
type Arguments struct {
|
||||||
|
@ -18,9 +15,6 @@ 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) {
|
||||||
|
@ -37,41 +31,11 @@ func GetArgs() (*Arguments, error) {
|
||||||
cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN")
|
cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN")
|
||||||
cloudflareZone := os.Getenv("CLOUDFLARE_ZONE")
|
cloudflareZone := os.Getenv("CLOUDFLARE_ZONE")
|
||||||
|
|
||||||
oauthClientID := os.Getenv("OAUTH_CLIENT_ID")
|
if cloudflareToken == "" {
|
||||||
oauthClientSecret := os.Getenv("OAUTH_CLIENT_SECRET")
|
return nil, errors.New("please set the CLOUDFLARE_TOKEN environment variable")
|
||||||
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 == "" {
|
||||||
for _, envVar := range envVars {
|
return nil, errors.New("please set the CLOUDFLARE_ZONE environment variable")
|
||||||
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{
|
||||||
|
@ -83,9 +47,6 @@ 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
|
||||||
|
|
|
@ -13,10 +13,8 @@ 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 TEXT PRIMARY KEY,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
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 {
|
||||||
|
@ -39,7 +37,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) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
);`)
|
);`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dbConn, err
|
return dbConn, err
|
||||||
|
@ -51,33 +49,18 @@ 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 TEXT PRIMARY KEY,
|
id INTEGER 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) ON DELETE CASCADE);`)
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +69,6 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
|
|
||||||
migrations := []Migrator{
|
migrations := []Migrator{
|
||||||
MigrateUsers,
|
MigrateUsers,
|
||||||
MigrateUserSessions,
|
|
||||||
MigrateApiKeys,
|
MigrateApiKeys,
|
||||||
MigrateDNSRecords,
|
MigrateDNSRecords,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,112 +1,5 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
func getUsers() {
|
||||||
"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
8
go.mod
|
@ -6,11 +6,3 @@ 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
22
go.sum
|
@ -1,26 +1,4 @@
|
||||||
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=
|
|
||||||
|
|
2
main.go
2
main.go
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -22,12 +22,6 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
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:])
|
|
||||||
}
|
|
Loading…
Reference in New Issue