diff --git a/adapters/cloudflare/cloudflare.go b/adapters/cloudflare/cloudflare.go new file mode 100644 index 0000000..40b04a5 --- /dev/null +++ b/adapters/cloudflare/cloudflare.go @@ -0,0 +1,71 @@ +package cloudflare + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "git.hatecomputers.club/hatecomputers/hatecomputers.club/database" +) + +type CloudflareDNSResponse struct { + Result database.DNSRecord `json:"result"` +} + +func CreateDNSRecord(zoneId string, apiToken string, record *database.DNSRecord) (string, error) { + url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneId) + + reqBody := fmt.Sprintf(`{"type":"%s","name":"%s","content":"%s","ttl":%d,"proxied":false}`, record.Type, record.Name, record.Content, record.TTL) + payload := strings.NewReader(reqBody) + + req, _ := http.NewRequest("POST", url, payload) + + req.Header.Add("Authorization", "Bearer "+apiToken) + req.Header.Add("Content-Type", "application/json") + + res, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + if res.StatusCode != 200 { + return "", fmt.Errorf("error creating dns record: %s", body) + } + + var response CloudflareDNSResponse + err = json.Unmarshal(body, &response) + if err != nil { + return "", err + } + + result := &response.Result + + return result.ID, nil +} + +func DeleteDNSRecord(zoneId string, apiToken string, id string) error { + url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneId, id) + + req, _ := http.NewRequest("DELETE", url, nil) + + req.Header.Add("Authorization", "Bearer "+apiToken) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + if res.StatusCode != 200 { + return fmt.Errorf("error deleting dns record: %s", body) + } + + return nil +} diff --git a/api/api_keys.go b/api/api_keys.go new file mode 100644 index 0000000..17ed6c9 --- /dev/null +++ b/api/api_keys.go @@ -0,0 +1,84 @@ +package api + +import ( + "log" + "net/http" + + "git.hatecomputers.club/hatecomputers/hatecomputers.club/database" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/utils" +) + +const MAX_USER_API_KEYS = 5 + +func ListAPIKeysContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { + return func(success Continuation, failure Continuation) ContinuationChain { + apiKeys, err := database.ListUserAPIKeys(context.DBConn, context.User.ID) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + (*context.TemplateData)["APIKeys"] = apiKeys + return success(context, req, resp) + } +} + +func CreateAPIKeyContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { + return func(success Continuation, failure Continuation) ContinuationChain { + formErrors := FormError{ + Errors: []string{}, + } + + apiKeys, err := database.ListUserAPIKeys(context.DBConn, context.User.ID) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + if len(apiKeys) >= MAX_USER_API_KEYS { + formErrors.Errors = append(formErrors.Errors, "max api keys reached") + } + + _, err = database.SaveAPIKey(context.DBConn, &database.UserApiKey{ + UserID: context.User.ID, + Key: utils.RandomId(), + }) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + http.Redirect(resp, req, "/keys", http.StatusFound) + return success(context, req, resp) + } +} + +func DeleteAPIKeyContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { + return func(success Continuation, failure Continuation) ContinuationChain { + key := req.FormValue("key") + + apiKey, err := database.GetAPIKey(context.DBConn, key) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + if (apiKey == nil) || (apiKey.UserID != context.User.ID) { + resp.WriteHeader(http.StatusUnauthorized) + return failure(context, req, resp) + } + + err = database.DeleteAPIKey(context.DBConn, key) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + http.Redirect(resp, req, "/keys", http.StatusFound) + return success(context, req, resp) + } +} diff --git a/api/auth.go b/api/auth.go index 4733971..dcddf5a 100644 --- a/api/auth.go +++ b/api/auth.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/base64" "encoding/json" + "fmt" "io" "log" "net/http" @@ -116,32 +117,69 @@ func InterceptCodeContinuation(context *RequestContext, req *http.Request, resp } } +func getUserFromAuthHeader(dbConn *sql.DB, bearerToken string) (*database.User, error) { + if bearerToken == "" { + return nil, nil + } + + parts := strings.Split(bearerToken, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + return nil, nil + } + + apiKey, err := database.GetAPIKey(dbConn, parts[1]) + if err != nil { + return nil, err + } + if apiKey == nil { + return nil, nil + } + + user, err := database.GetUser(dbConn, apiKey.UserID) + if err != nil { + return nil, err + } + + return user, nil +} + +func getUserFromSession(dbConn *sql.DB, sessionId string) (*database.User, error) { + session, err := database.GetSession(dbConn, sessionId) + if err != nil { + return nil, err + } + + if session.ExpireAt.Before(time.Now()) { + session = nil + database.DeleteSession(dbConn, sessionId) + return nil, fmt.Errorf("session expired") + } + + user, err := database.GetUser(dbConn, session.UserID) + if err != nil { + return nil, err + } + + return user, nil +} + func VerifySessionContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { return func(success Continuation, failure Continuation) ContinuationChain { + authHeader := req.Header.Get("Authorization") + user, userErr := getUserFromAuthHeader(context.DBConn, authHeader) + sessionCookie, err := req.Cookie("session") - if err != nil { - resp.WriteHeader(http.StatusUnauthorized) - return failure(context, req, resp) + if err == nil { + user, userErr = getUserFromSession(context.DBConn, sessionCookie.Value) } - 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 { + if userErr != nil || user == nil { + log.Println(userErr, user) + http.SetCookie(resp, &http.Cookie{ Name: "session", - MaxAge: 0, + MaxAge: 0, // reset session cookie in case }) - - 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) } diff --git a/api/dns.go b/api/dns.go index 3105f91..5123acc 100644 --- a/api/dns.go +++ b/api/dns.go @@ -3,10 +3,23 @@ package api import ( "log" "net/http" + "strconv" + "strings" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare" "git.hatecomputers.club/hatecomputers/hatecomputers.club/database" ) +const MAX_USER_RECORDS = 20 + +type FormError struct { + Errors []string +} + +func userCanFuckWithDNSRecord(user *database.User, record *database.DNSRecord) bool { + return user.ID == record.UserID && (record.Name == user.Username || strings.HasSuffix(record.Name, "."+user.Username)) +} + func ListDNSRecordsContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { return func(success Continuation, failure Continuation) ContinuationChain { dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID) @@ -17,7 +30,108 @@ func ListDNSRecordsContinuation(context *RequestContext, req *http.Request, resp } (*context.TemplateData)["DNSRecords"] = dnsRecords - + return success(context, req, resp) + } +} + +func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { + return func(success Continuation, failure Continuation) ContinuationChain { + formErrors := FormError{ + Errors: []string{}, + } + + name := req.FormValue("name") + recordType := req.FormValue("type") + recordContent := req.FormValue("content") + ttl := req.FormValue("ttl") + ttlNum, err := strconv.Atoi(ttl) + if err != nil { + formErrors.Errors = append(formErrors.Errors, "invalid ttl") + } + + dnsRecord := &database.DNSRecord{ + UserID: context.User.ID, + Name: name, + Type: recordType, + Content: recordContent, + TTL: ttlNum, + } + + dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + if len(dnsRecords) >= MAX_USER_RECORDS { + formErrors.Errors = append(formErrors.Errors, "max records reached") + } + + if !userCanFuckWithDNSRecord(context.User, dnsRecord) { + formErrors.Errors = append(formErrors.Errors, "'name' must end with "+context.User.Username) + } + + if len(formErrors.Errors) == 0 { + cloudflareRecordId, err := cloudflare.CreateDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, dnsRecord) + if err != nil { + log.Println(err) + formErrors.Errors = append(formErrors.Errors, err.Error()) + } + + dnsRecord.ID = cloudflareRecordId + } + + if len(formErrors.Errors) == 0 { + _, err := database.SaveDNSRecord(context.DBConn, dnsRecord) + if err != nil { + log.Println(err) + formErrors.Errors = append(formErrors.Errors, "error saving record") + } + } + + if len(formErrors.Errors) == 0 { + http.Redirect(resp, req, "/dns", http.StatusFound) + return success(context, req, resp) + } + + (*context.TemplateData)["DNSRecords"] = dnsRecords + (*context.TemplateData)["FormError"] = &formErrors + (*context.TemplateData)["RecordForm"] = dnsRecord + + resp.WriteHeader(http.StatusBadRequest) + return failure(context, req, resp) + } +} + +func DeleteDNSRecordContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { + return func(success Continuation, failure Continuation) ContinuationChain { + recordId := req.FormValue("id") + record, err := database.GetDNSRecord(context.DBConn, recordId) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + if !userCanFuckWithDNSRecord(context.User, record) { + resp.WriteHeader(http.StatusUnauthorized) + return failure(context, req, resp) + } + + err = cloudflare.DeleteDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, recordId) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + err = database.DeleteDNSRecord(context.DBConn, recordId) + if err != nil { + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + http.Redirect(resp, req, "/dns", http.StatusFound) return success(context, req, resp) } } diff --git a/api/serve.go b/api/serve.go index 38b65b2..d16ea99 100644 --- a/api/serve.go +++ b/api/serve.go @@ -70,7 +70,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { mux := http.NewServeMux() fileServer := http.FileServer(http.Dir(argv.StaticPath)) - mux.Handle("/static/", http.StripPrefix("/static/", fileServer)) + mux.Handle("GET /static/", http.StripPrefix("/static/", fileServer)) makeRequestContext := func() *RequestContext { return &RequestContext{ @@ -81,7 +81,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { } } - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { requestContext := makeRequestContext() LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(TemplateContinuation("home.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) }) @@ -116,6 +116,31 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(ListDNSRecordsContinuation, GoLoginContinuation)(TemplateContinuation("dns.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) }) + mux.HandleFunc("POST /dns", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(CreateDNSRecordContinuation, GoLoginContinuation)(IdContinuation, TemplateContinuation("dns.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) + + mux.HandleFunc("POST /dns/delete", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(DeleteDNSRecordContinuation, GoLoginContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) + + mux.HandleFunc("GET /keys", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(ListAPIKeysContinuation, GoLoginContinuation)(TemplateContinuation("api_keys.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) + + mux.HandleFunc("POST /keys", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(CreateAPIKeyContinuation, GoLoginContinuation)(IdContinuation, TemplateContinuation("api_keys.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) + + mux.HandleFunc("POST /keys/delete", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(DeleteAPIKeyContinuation, GoLoginContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) + mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) { requestContext := makeRequestContext() name := r.PathValue("name") diff --git a/database/dns.go b/database/dns.go index 17487b7..bb5c1ef 100644 --- a/database/dns.go +++ b/database/dns.go @@ -8,13 +8,13 @@ import ( ) type DNSRecord struct { - ID string - UserID string - Name string - Type string - Content string - TTL int - CreatedAt time.Time + ID string `json:"id"` + UserID string `json:"user_id"` + Name string `json:"name"` + Type string `json:"type"` + Content string `json:"content"` + TTL int `json:"ttl"` + CreatedAt time.Time `json:"created_at"` } func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) { @@ -38,3 +38,37 @@ func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) { return records, nil } + +func SaveDNSRecord(db *sql.DB, record *DNSRecord) (*DNSRecord, error) { + log.Println("saving dns record", record) + + record.CreatedAt = time.Now() + _, err := db.Exec("INSERT OR REPLACE INTO dns_records (id, user_id, name, type, content, ttl, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", record.ID, record.UserID, record.Name, record.Type, record.Content, record.TTL, record.CreatedAt) + + if err != nil { + return nil, err + } + return record, nil +} + +func GetDNSRecord(db *sql.DB, recordID string) (*DNSRecord, error) { + log.Println("getting dns record", recordID) + + row := db.QueryRow("SELECT * FROM dns_records WHERE id = ?", recordID) + var record DNSRecord + err := row.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.CreatedAt) + if err != nil { + return nil, err + } + return &record, nil +} + +func DeleteDNSRecord(db *sql.DB, recordID string) error { + log.Println("deleting dns record", recordID) + + _, err := db.Exec("DELETE FROM dns_records WHERE id = ?", recordID) + if err != nil { + return err + } + return nil +} diff --git a/database/users.go b/database/users.go index d2b4f20..f9e4436 100644 --- a/database/users.go +++ b/database/users.go @@ -12,6 +12,12 @@ const ( ExpiryDuration = time.Hour * 24 ) +type UserApiKey struct { + Key string `json:"key"` + UserID string `json:"user_id"` + CreatedAt time.Time `json:"created_at"` +} + type User struct { ID string `json:"sub"` Mail string `json:"email"` @@ -119,3 +125,60 @@ func DeleteExpiredSessions(dbConn *sql.DB) error { } return nil } + +func ListUserAPIKeys(dbConn *sql.DB, userId string) ([]*UserApiKey, error) { + rows, err := dbConn.Query(`SELECT key, user_id, created_at FROM api_keys WHERE user_id = ?;`, userId) + if err != nil { + log.Println(err) + return nil, err + } + defer rows.Close() + + var apiKeys []*UserApiKey + for rows.Next() { + var apiKey UserApiKey + err := rows.Scan(&apiKey.Key, &apiKey.UserID, &apiKey.CreatedAt) + if err != nil { + log.Println(err) + return nil, err + } + + apiKeys = append(apiKeys, &apiKey) + } + + return apiKeys, nil +} + +func SaveAPIKey(dbConn *sql.DB, apiKey *UserApiKey) (*UserApiKey, error) { + _, err := dbConn.Exec(`INSERT OR REPLACE INTO api_keys (key, user_id) VALUES (?, ?);`, apiKey.Key, apiKey.UserID) + if err != nil { + log.Println(err) + return nil, err + } + + apiKey.CreatedAt = time.Now() + return apiKey, nil +} + +func GetAPIKey(dbConn *sql.DB, key string) (*UserApiKey, error) { + row := dbConn.QueryRow(`SELECT key, user_id, created_at FROM api_keys WHERE key = ?;`, key) + + var apiKey UserApiKey + err := row.Scan(&apiKey.Key, &apiKey.UserID, &apiKey.CreatedAt) + if err != nil { + log.Println(err) + return nil, err + } + + return &apiKey, nil +} + +func DeleteAPIKey(dbConn *sql.DB, key string) error { + _, err := dbConn.Exec(`DELETE FROM api_keys WHERE key = ?;`, key) + if err != nil { + log.Println(err) + return err + } + + return nil +} diff --git a/static/css/colors.css b/static/css/colors.css index 69e3e4b..c68bf8e 100644 --- a/static/css/colors.css +++ b/static/css/colors.css @@ -5,6 +5,7 @@ --link-color-light: #d291bc; --container-bg-light: #fff7f87a; --border-color-light: #692fcc; + --error-color-light: #a83254; --background-color-dark: #333; --background-color-dark-2: #2c2c2c; @@ -12,6 +13,7 @@ --link-color-dark: #b86b77; --container-bg-dark: #424242ea; --border-color-dark: #956ade; + --error-color-dark: #851736; } [data-theme="DARK"] { @@ -21,6 +23,7 @@ --link-color: var(--link-color-dark); --container-bg: var(--container-bg-dark); --border-color: var(--border-color-dark); + --error-color: var(--error-color-dark); } [data-theme="LIGHT"] { @@ -30,4 +33,10 @@ --link-color: var(--link-color-light); --container-bg: var(--container-bg-light); --border-color: var(--border-color-light); + --error-color: var(--error-color-light); +} + +.error { + background-color: var(--error-color); + padding: 1rem; } diff --git a/static/css/form.css b/static/css/form.css index 4e14b68..1378d75 100644 --- a/static/css/form.css +++ b/static/css/form.css @@ -1,4 +1,4 @@ -form { +.form { max-width: 600px; padding: 1em; background: var(--background-color-2); diff --git a/static/css/table.css b/static/css/table.css index 640ad83..75a961d 100644 --- a/static/css/table.css +++ b/static/css/table.css @@ -11,8 +11,13 @@ td { border-bottom: 1px solid var(--border-color); } +th, +thead { + background-color: var(--background-color-2); +} + tbody tr:nth-child(odd) { - background-color: var(--link-color); + background-color: var(--background-color); color: var(--text-color); } diff --git a/static/img/blinkies/capitalism.gif b/static/img/blinkies/capitalism.gif new file mode 100644 index 0000000..b851831 Binary files /dev/null and b/static/img/blinkies/capitalism.gif differ diff --git a/templates/api_keys.html b/templates/api_keys.html new file mode 100644 index 0000000..93eebd5 --- /dev/null +++ b/templates/api_keys.html @@ -0,0 +1,40 @@ +{{ define "content" }} + + + + + + + {{ if (eq (len .APIKeys) 0) }} + + + + {{ end }} + {{ range $key := .APIKeys }} + + + + + + {{ end }} +
KeyCreated AtRevoke
No API Keys Found
{{ $key.Key }}{{ $key.CreatedAt }} +
+ + +
+
+
+
+

Add An API Key

+
+ +
+ + {{ if .FormError }} + {{ if (len .FormError.Errors) }} + {{ range $error := .FormError.Errors }} +
{{ $error }}
+ {{ end }} + {{ end }} + {{ end }} +{{ end }} diff --git a/templates/base.html b/templates/base.html index 79e0d12..9f5a903 100644 --- a/templates/base.html +++ b/templates/base.html @@ -35,7 +35,10 @@ {{ if .User }} dns. | + api keys. + | logout, {{ .User.DisplayName }}. + {{ else }} login. {{ end }} @@ -74,6 +77,7 @@ + diff --git a/templates/dns.html b/templates/dns.html index 0a40cab..e317d05 100644 --- a/templates/dns.html +++ b/templates/dns.html @@ -5,10 +5,11 @@ Name Content TTL + Delete {{ if (eq (len .DNSRecords) 0) }} - No DNS records found + No DNS records found {{ end }} {{ range $record := .DNSRecords }} @@ -17,21 +18,60 @@ {{ $record.Name }} {{ $record.Content }} {{ $record.TTL }} + +
+ + +
+ {{ end }}
-
+

Add DNS Records

+

note that the name must be a subdomain of {{ .User.Username }}


- + - + - + - +
+ + {{ if .FormError }} + {{ if (len .FormError.Errors) }} + {{ range $error := .FormError.Errors }} +
{{ $error }}
+ {{ end }} + {{ end }} + {{ end }} {{ end }} diff --git a/utils/RandomId.go b/utils/RandomId.go index 09f089d..1b03ec8 100644 --- a/utils/RandomId.go +++ b/utils/RandomId.go @@ -6,14 +6,11 @@ import ( ) func RandomId() string { - uuid := make([]byte, 16) - _, err := rand.Read(uuid) + id := make([]byte, 16) + _, err := rand.Read(id) 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:]) + return fmt.Sprintf("%x", id) }