dns api #1
			
				
			
		
		
		
	|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										74
									
								
								api/auth.go
								
								
								
								
							
							
						
						
									
										74
									
								
								api/auth.go
								
								
								
								
							|  | @ -5,6 +5,7 @@ import ( | ||||||
| 	"database/sql" | 	"database/sql" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"log" | 	"log" | ||||||
| 	"net/http" | 	"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 { | func VerifySessionContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { | ||||||
| 	return func(success Continuation, failure Continuation) ContinuationChain { | 	return func(success Continuation, failure Continuation) ContinuationChain { | ||||||
|  | 		authHeader := req.Header.Get("Authorization") | ||||||
|  | 		user, userErr := getUserFromAuthHeader(context.DBConn, authHeader) | ||||||
|  | 
 | ||||||
| 		sessionCookie, err := req.Cookie("session") | 		sessionCookie, err := req.Cookie("session") | ||||||
| 		if err != nil { | 		if err == nil { | ||||||
| 			resp.WriteHeader(http.StatusUnauthorized) | 			user, userErr = getUserFromSession(context.DBConn, sessionCookie.Value) | ||||||
| 			return failure(context, req, resp) |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		session, err := database.GetSession(context.DBConn, sessionCookie.Value) | 		if userErr != nil || user == nil { | ||||||
| 		if err == nil && session.ExpireAt.Before(time.Now()) { | 			log.Println(userErr, user) | ||||||
| 			session = nil | 
 | ||||||
| 			database.DeleteSession(context.DBConn, sessionCookie.Value) |  | ||||||
| 		} |  | ||||||
| 		if err != nil || session == nil { |  | ||||||
| 			http.SetCookie(resp, &http.Cookie{ | 			http.SetCookie(resp, &http.Cookie{ | ||||||
| 				Name:   "session", | 				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) | 			return failure(context, req, resp) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										116
									
								
								api/dns.go
								
								
								
								
							
							
						
						
									
										116
									
								
								api/dns.go
								
								
								
								
							|  | @ -3,10 +3,23 @@ package api | ||||||
| import ( | import ( | ||||||
| 	"log" | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
|  | 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare" | ||||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/database" | 	"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 { | func ListDNSRecordsContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { | ||||||
| 	return func(success Continuation, failure Continuation) ContinuationChain { | 	return func(success Continuation, failure Continuation) ContinuationChain { | ||||||
| 		dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID) | 		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 | 		(*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) | 		return success(context, req, resp) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										29
									
								
								api/serve.go
								
								
								
								
							
							
						
						
									
										29
									
								
								api/serve.go
								
								
								
								
							|  | @ -70,7 +70,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { | ||||||
| 	mux := http.NewServeMux() | 	mux := http.NewServeMux() | ||||||
| 
 | 
 | ||||||
| 	fileServer := http.FileServer(http.Dir(argv.StaticPath)) | 	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 { | 	makeRequestContext := func() *RequestContext { | ||||||
| 		return &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() | 		requestContext := makeRequestContext() | ||||||
| 		LogRequestContinuation(requestContext, r, w)(VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(TemplateContinuation("home.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | 		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) | 		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) { | 	mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		requestContext := makeRequestContext() | 		requestContext := makeRequestContext() | ||||||
| 		name := r.PathValue("name") | 		name := r.PathValue("name") | ||||||
|  |  | ||||||
|  | @ -8,13 +8,13 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type DNSRecord struct { | type DNSRecord struct { | ||||||
| 	ID        string | 	ID        string    `json:"id"` | ||||||
| 	UserID    string | 	UserID    string    `json:"user_id"` | ||||||
| 	Name      string | 	Name      string    `json:"name"` | ||||||
| 	Type      string | 	Type      string    `json:"type"` | ||||||
| 	Content   string | 	Content   string    `json:"content"` | ||||||
| 	TTL       int | 	TTL       int       `json:"ttl"` | ||||||
| 	CreatedAt time.Time | 	CreatedAt time.Time `json:"created_at"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) { | 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 | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -12,6 +12,12 @@ const ( | ||||||
| 	ExpiryDuration = time.Hour * 24 | 	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 { | type User struct { | ||||||
| 	ID          string    `json:"sub"` | 	ID          string    `json:"sub"` | ||||||
| 	Mail        string    `json:"email"` | 	Mail        string    `json:"email"` | ||||||
|  | @ -119,3 +125,60 @@ func DeleteExpiredSessions(dbConn *sql.DB) error { | ||||||
| 	} | 	} | ||||||
| 	return nil | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ | ||||||
|   --link-color-light: #d291bc; |   --link-color-light: #d291bc; | ||||||
|   --container-bg-light: #fff7f87a; |   --container-bg-light: #fff7f87a; | ||||||
|   --border-color-light: #692fcc; |   --border-color-light: #692fcc; | ||||||
|  |   --error-color-light: #a83254; | ||||||
| 
 | 
 | ||||||
|   --background-color-dark: #333; |   --background-color-dark: #333; | ||||||
|   --background-color-dark-2: #2c2c2c; |   --background-color-dark-2: #2c2c2c; | ||||||
|  | @ -12,6 +13,7 @@ | ||||||
|   --link-color-dark: #b86b77; |   --link-color-dark: #b86b77; | ||||||
|   --container-bg-dark: #424242ea; |   --container-bg-dark: #424242ea; | ||||||
|   --border-color-dark: #956ade; |   --border-color-dark: #956ade; | ||||||
|  |   --error-color-dark: #851736; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| [data-theme="DARK"] { | [data-theme="DARK"] { | ||||||
|  | @ -21,6 +23,7 @@ | ||||||
|   --link-color: var(--link-color-dark); |   --link-color: var(--link-color-dark); | ||||||
|   --container-bg: var(--container-bg-dark); |   --container-bg: var(--container-bg-dark); | ||||||
|   --border-color: var(--border-color-dark); |   --border-color: var(--border-color-dark); | ||||||
|  |   --error-color: var(--error-color-dark); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| [data-theme="LIGHT"] { | [data-theme="LIGHT"] { | ||||||
|  | @ -30,4 +33,10 @@ | ||||||
|   --link-color: var(--link-color-light); |   --link-color: var(--link-color-light); | ||||||
|   --container-bg: var(--container-bg-light); |   --container-bg: var(--container-bg-light); | ||||||
|   --border-color: var(--border-color-light); |   --border-color: var(--border-color-light); | ||||||
|  |   --error-color: var(--error-color-light); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .error { | ||||||
|  |   background-color: var(--error-color); | ||||||
|  |   padding: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| form { | .form { | ||||||
|   max-width: 600px; |   max-width: 600px; | ||||||
|   padding: 1em; |   padding: 1em; | ||||||
|   background: var(--background-color-2); |   background: var(--background-color-2); | ||||||
|  |  | ||||||
|  | @ -11,8 +11,13 @@ td { | ||||||
|   border-bottom: 1px solid var(--border-color); |   border-bottom: 1px solid var(--border-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | th, | ||||||
|  | thead { | ||||||
|  |   background-color: var(--background-color-2); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| tbody tr:nth-child(odd) { | tbody tr:nth-child(odd) { | ||||||
|   background-color: var(--link-color); |   background-color: var(--background-color); | ||||||
|   color: var(--text-color); |   color: var(--text-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 20 KiB | 
|  | @ -0,0 +1,40 @@ | ||||||
|  | {{ define "content" }} | ||||||
|  |   <table> | ||||||
|  |     <tr> | ||||||
|  |       <th>Key</th> | ||||||
|  |       <th>Created At</th> | ||||||
|  |       <th>Revoke</th> | ||||||
|  |     </tr> | ||||||
|  |     {{ if (eq (len .APIKeys) 0) }} | ||||||
|  |     <tr> | ||||||
|  |       <td colspan="5"><span class="blinky">No API Keys Found</span></td> | ||||||
|  |     </tr> | ||||||
|  |     {{ end }} | ||||||
|  |     {{ range $key := .APIKeys }} | ||||||
|  |       <tr> | ||||||
|  | 	<td>{{ $key.Key }}</td> | ||||||
|  | 	<td>{{ $key.CreatedAt }}</td> | ||||||
|  | 	<td> | ||||||
|  | 	  <form method="POST" action="/keys/delete"> | ||||||
|  | 	    <input type="hidden" name="key" value="{{ $key.Key }}" /> | ||||||
|  | 	    <input type="submit" value="Revoke" /> | ||||||
|  | 	  </form> | ||||||
|  | 	</td> | ||||||
|  |       </tr> | ||||||
|  |     {{ end }} | ||||||
|  |   </table> | ||||||
|  |   <br> | ||||||
|  |   <form method="POST" action="/keys" class="form"> | ||||||
|  |     <h2>Add An API Key</h2> | ||||||
|  |     <hr> | ||||||
|  |     <input type="submit" value="Generate" /> | ||||||
|  |   </form> | ||||||
|  | 
 | ||||||
|  |   {{ if .FormError }} | ||||||
|  |     {{ if (len .FormError.Errors) }} | ||||||
|  |       {{ range $error := .FormError.Errors }} | ||||||
|  |         <div class="error">{{ $error }}</div> | ||||||
|  |       {{ end }} | ||||||
|  |     {{ end }} | ||||||
|  |   {{ end }} | ||||||
|  | {{ end }} | ||||||
|  | @ -35,7 +35,10 @@ | ||||||
| 	{{ if .User }} | 	{{ if .User }} | ||||||
| 	<a href="/dns">dns.</a> | 	<a href="/dns">dns.</a> | ||||||
| 	<span> | </span> | 	<span> | </span> | ||||||
|  | 	<a href="/keys">api keys.</a> | ||||||
|  | 	<span> | </span> | ||||||
| 	<a href="/logout">logout, {{ .User.DisplayName }}.</a> | 	<a href="/logout">logout, {{ .User.DisplayName }}.</a> | ||||||
|  | 	 | ||||||
| 	{{ else }} | 	{{ else }} | ||||||
| 	<a href="/login">login.</a> | 	<a href="/login">login.</a> | ||||||
| 	{{ end }} | 	{{ end }} | ||||||
|  | @ -74,6 +77,7 @@ | ||||||
| 	  <img width='150' height='20' src='/static/img/blinkies/connection.gif'> | 	  <img width='150' height='20' src='/static/img/blinkies/connection.gif'> | ||||||
| 	  <img width='150' height='20' src='/static/img/blinkies/eepy.gif'> | 	  <img width='150' height='20' src='/static/img/blinkies/eepy.gif'> | ||||||
| 	  <img width='150' height='20' src='/static/img/blinkies/loveuguys.gif'> | 	  <img width='150' height='20' src='/static/img/blinkies/loveuguys.gif'> | ||||||
|  | 	  <img width='150' height='20' src='/static/img/blinkies/capitalism.gif'> | ||||||
| 	</div> | 	</div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  | @ -5,10 +5,11 @@ | ||||||
|       <th>Name</th> |       <th>Name</th> | ||||||
|       <th>Content</th> |       <th>Content</th> | ||||||
|       <th>TTL</th> |       <th>TTL</th> | ||||||
|  |       <th>Delete</th> | ||||||
|     </tr> |     </tr> | ||||||
|     {{ if (eq (len .DNSRecords) 0) }} |     {{ if (eq (len .DNSRecords) 0) }} | ||||||
|     <tr> |     <tr> | ||||||
|       <td colspan="4"><span class="blinky">No DNS records found</span></td> |       <td colspan="5"><span class="blinky">No DNS records found</span></td> | ||||||
|     </tr> |     </tr> | ||||||
|     {{ end }} |     {{ end }} | ||||||
|     {{ range $record := .DNSRecords }} |     {{ range $record := .DNSRecords }} | ||||||
|  | @ -17,21 +18,60 @@ | ||||||
| 	<td>{{ $record.Name }}</td> | 	<td>{{ $record.Name }}</td> | ||||||
| 	<td>{{ $record.Content }}</td> | 	<td>{{ $record.Content }}</td> | ||||||
| 	<td>{{ $record.TTL }}</td> | 	<td>{{ $record.TTL }}</td> | ||||||
|  | 	<td> | ||||||
|  | 	  <form method="POST" action="/dns/delete"> | ||||||
|  | 	    <input type="hidden" name="id" value="{{ $record.ID }}" /> | ||||||
|  | 	    <input type="submit" value="Delete" /> | ||||||
|  | 	  </form> | ||||||
|  | 	</td> | ||||||
|       </tr> |       </tr> | ||||||
|     {{ end }} |     {{ end }} | ||||||
|   </table> |   </table> | ||||||
|   <br> |   <br> | ||||||
|   <form method="POST" action="/dns"> |   <form method="POST" action="/dns" class="form"> | ||||||
|     <h2>Add DNS Records</h2> |     <h2>Add DNS Records</h2> | ||||||
|  |     <p>note that the name <em>must</em> be a subdomain of <em>{{ .User.Username }}</em></p> | ||||||
|     <hr> |     <hr> | ||||||
|     <label for="type">Type</label> |     <label for="type">Type</label> | ||||||
|     <input type="text" name="type" placeholder="CNAME" required /> |     <input type="text" name="type" placeholder="CNAME" | ||||||
|  | 	   {{ if not .RecordForm }} | ||||||
|  | 	   placeholder="CNAME" | ||||||
|  | 	   {{ else }} | ||||||
|  | 	   value="{{ .RecordForm.Type }}" | ||||||
|  | 	   {{ end }} | ||||||
|  | 	   required /> | ||||||
|     <label for="name">Name</label> |     <label for="name">Name</label> | ||||||
|     <input type="text" name="name" placeholder="{{ .User.Username }}" required /> |     <input type="text" name="name" | ||||||
|  | 	   {{ if not .RecordForm }} | ||||||
|  | 	   placeholder="{{ .User.Username }} || endpoint.{{ .User.Username }}..." | ||||||
|  | 	   {{ else }} | ||||||
|  | 	   value="{{ .RecordForm.Name }}" | ||||||
|  | 	   {{ end }} | ||||||
|  | 	   required/> | ||||||
|     <label for="content">Content</label> |     <label for="content">Content</label> | ||||||
|     <input type="text" name="content" placeholder="{{ .User.Username }}.dev" required /> |     <input type="text" name="content" | ||||||
|  | 	   {{ if not .RecordForm }} | ||||||
|  | 	   placeholder="{{ .User.Username }}.dev" | ||||||
|  | 	   {{ else }} | ||||||
|  | 	   value="{{ .RecordForm.Content }}" | ||||||
|  | 	   {{ end }} | ||||||
|  | 	   required /> | ||||||
|     <label for="ttl">TTL</label> |     <label for="ttl">TTL</label> | ||||||
|     <input type="text" name="ttl" placeholder="43200" required /> |     <input type="text" name="ttl" | ||||||
|  | 	   {{ if not .RecordForm }} | ||||||
|  | 	   placeholder="43200" | ||||||
|  | 	   {{ else }} | ||||||
|  | 	   value="{{ .RecordForm.TTL }}" | ||||||
|  | 	   {{ end }} | ||||||
|  | 	   required /> | ||||||
|     <input type="submit" value="Add" /> |     <input type="submit" value="Add" /> | ||||||
|   </form> |   </form> | ||||||
|  | 
 | ||||||
|  |   {{ if .FormError }} | ||||||
|  |     {{ if (len .FormError.Errors) }} | ||||||
|  |       {{ range $error := .FormError.Errors }} | ||||||
|  |         <div class="error">{{ $error }}</div> | ||||||
|  |       {{ end }} | ||||||
|  |     {{ end }} | ||||||
|  |   {{ end }} | ||||||
| {{ end }} | {{ end }} | ||||||
|  |  | ||||||
|  | @ -6,14 +6,11 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func RandomId() string { | func RandomId() string { | ||||||
| 	uuid := make([]byte, 16) | 	id := make([]byte, 16) | ||||||
| 	_, err := rand.Read(uuid) | 	_, err := rand.Read(id) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		panic(err) | 		panic(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	uuid[8] = uuid[8]&^0xc0 | 0x80 | 	return fmt.Sprintf("%x", id) | ||||||
| 	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