package main import ( "errors" "time" "crypto/ed25519" "database/sql" "encoding/json" library "git.ailur.dev/ailur/fg-library/v3" authLibrary "git.ailur.dev/ailur/fg-nucleus-library" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "html/template" "io/fs" "net/http" ) var ServiceInformation = library.Service{ Name: "datatracker", Permissions: library.Permissions{ Authenticate: true, // This service does require authentication Database: true, // This service does require database access Router: true, // This service does require a router BlobStorage: false, // This service does not require blob storage InterServiceCommunication: true, // This service does require inter-service communication Resources: true, // This service does require its HTTP templates and static files }, ServiceID: uuid.MustParse("322dc186-04d2-4f69-89b5-403ab643cc1d"), } var ( loggerService = uuid.MustParse("00000000-0000-0000-0000-000000000002") ) func logFunc(message string, messageType library.MessageCode, information *library.ServiceInitializationInformation) { // Log the message to the logger service information.SendISMessage(loggerService, messageType, message) } func renderTemplate(statusCode int, w http.ResponseWriter, data map[string]interface{}, templatePath string, information *library.ServiceInitializationInformation) { var err error var requestedTemplate *template.Template // Output ls of the resource directory requestedTemplate, err = template.ParseFS(information.ResourceDir, "templates/"+templatePath) if err != nil { logFunc(err.Error(), 2, information) renderString(500, w, "Sorry, something went wrong on our end. Error code: 01. Please report to the administrator.", information) } else { w.WriteHeader(statusCode) err = requestedTemplate.Execute(w, data) if err != nil { logFunc(err.Error(), 2, information) renderString(500, w, "Sorry, something went wrong on our end. Error code: 02. Please report to the administrator.", information) } } } func renderString(statusCode int, w http.ResponseWriter, data string, information *library.ServiceInitializationInformation) { w.WriteHeader(statusCode) _, err := w.Write([]byte(data)) if err != nil { logFunc(err.Error(), 2, information) } } func renderJSON(statusCode int, w http.ResponseWriter, data map[string]interface{}, information *library.ServiceInitializationInformation) { w.WriteHeader(statusCode) err := json.NewEncoder(w).Encode(data) if err != nil { logFunc(err.Error(), 2, information) } } func getUsername(token string, oauthHostName string, publicKey ed25519.PublicKey) (string, string, error) { parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { return publicKey, nil }) if err != nil { return "", "", err } if !parsedToken.Valid { return "", "", errors.New("invalid token") } claims, ok := parsedToken.Claims.(jwt.MapClaims) if !ok { return "", "", errors.New("invalid token") } // Check if the token expired date, err := claims.GetExpirationTime() if err != nil || date.Before(time.Now()) || claims["sub"] == nil || claims["isOpenID"] == nil || claims["isOpenID"].(bool) { return "", "", errors.New("invalid token") } // Get the user's information var responseData struct { Username string `json:"username"` Sub string `json:"sub"` } request, err := http.NewRequest("GET", oauthHostName+"/api/oauth/userinfo", nil) if err != nil { return "", "", err } request.Header.Set("Authorization", "Bearer "+token) response, err := http.DefaultClient.Do(request) if err != nil { return "", "", err } if response.StatusCode != 200 || response.Body == nil || response.Body == http.NoBody { return "", "", errors.New("invalid response") } err = json.NewDecoder(response.Body).Decode(&responseData) if err != nil { return "", "", err } return responseData.Sub, responseData.Username, nil } func verifyJwt(token string, publicKey ed25519.PublicKey, conn library.Database) (jwt.MapClaims, bool) { parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { return publicKey, nil }) if err != nil { return nil, false } if !parsedToken.Valid { return nil, false } claims, ok := parsedToken.Claims.(jwt.MapClaims) if !ok { return nil, false } // Check if the token expired date, err := claims.GetExpirationTime() if err != nil || date.Before(time.Now()) || claims["sub"] == nil || claims["isOpenID"] == nil || claims["isOpenID"].(bool) { return claims, false } // Check if the token is in users userUuid, err := uuid.MustParse(claims["sub"].(string)).MarshalBinary() if err != nil { return claims, false } var idCheck []byte err = conn.DB.QueryRow("SELECT id FROM users WHERE id = $1", userUuid).Scan(&idCheck) if err != nil || claims["sub"] != uuid.Must(uuid.FromBytes(idCheck)).String() { return claims, false } return claims, true } func Main(information *library.ServiceInitializationInformation) { var conn library.Database hostName := information.Configuration["hostName"].(string) go information.StartISProcessor() // Initiate a connection to the database conn, err := information.GetDatabase() if err != nil { logFunc(err.Error(), 3, information) return } if conn.DBType == library.Sqlite { // Create the RFCs table _, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS rfc (id INTEGER NOT NULL, year INTEGER NOT NULL, name TEXT NOT NULL, content TEXT NOT NULL, version TEXT NOT NULL, creator BLOB NOT NULL, UNIQUE(id, year))") if err != nil { logFunc(err.Error(), 3, information) } // Create the users table _, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB NOT NULL)") if err != nil { logFunc(err.Error(), 3, information) } // Create the comments table _, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS comments (id BLOB NOT NULL, rfcId INTEGER NOT NULL, rfcYear INTEGER NOT NULL, content TEXT NOT NULL, creator BLOB NOT NULL, creatorName TEXT NOT NULL, created INTEGER NOT NULL)") if err != nil { logFunc(err.Error(), 3, information) } } else { // Create the RFCs table _, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS rfc (id SERIAL PRIMARY KEY, year INTEGER NOT NULL, name TEXT NOT NULL, content TEXT NOT NULL, version TEXT NOT NULL, creator BYTEA NOT NULL, UNIQUE(id, year))") if err != nil { logFunc(err.Error(), 3, information) } // Create the users table _, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BYTEA NOT NULL)") if err != nil { logFunc(err.Error(), 3, information) } // Create the comments table _, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS comments (id BYTEA NOT NULL, rfcId INTEGER NOT NULL, rfcYear INTEGER NOT NULL, content TEXT NOT NULL, creator BYTEA NOT NULL, creatorName TEXT NOT NULL, created INTEGER NOT NULL)") if err != nil { logFunc(err.Error(), 3, information) } } // Initialize the OAuth oauthResponse, publicKey, oauthHostName, err := authLibrary.InitializeOAuth(authLibrary.OAuthInformation{ Name: "datatracker", RedirectUri: hostName + "/oauth", Scopes: []string{"openid"}, }, information) if err != nil { logFunc(err.Error(), 3, information) return } // Set up the router router := information.Router // Set up the static routes staticDir, err := fs.Sub(information.ResourceDir, "static") if err != nil { logFunc(err.Error(), 3, information) } else { router.Handle("/dt-static/*", http.StripPrefix("/dt-static/", http.FileServerFS(staticDir))) } // Set up the API routes router.Post("/api/comment/add", func(w http.ResponseWriter, r *http.Request) { var commentData struct { RfcId int `json:"rfcId"` RfcYear int `json:"rfcYear"` Content string `json:"content"` JwtToken string `json:"token"` } err := json.NewDecoder(r.Body).Decode(&commentData) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information) return } // Get the username sub, username, err := getUsername(commentData.JwtToken, oauthHostName, publicKey) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } subBytes, err := uuid.MustParse(sub).MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "10"}, information) return } // Create the comment UUID commentId, err := uuid.NewRandom() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "11"}, information) return } commentIdBytes, err := commentId.MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "12"}, information) return } // Add the comment to the database _, err = conn.DB.Exec("INSERT INTO comments (id, rfcId, rfcYear, content, creator, creatorName, created) VALUES ($1, $2, $3, $4, $5, $6, $7)", commentIdBytes, commentData.RfcId, commentData.RfcYear, commentData.Content, subBytes, username, time.Now().Unix()) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "13"}, information) return } renderJSON(200, w, map[string]interface{}{"success": true, "author": username}, information) }) router.Post("/api/comment/list", func(w http.ResponseWriter, r *http.Request) { var commentData struct { RfcId int `json:"rfcId"` RfcYear int `json:"rfcYear"` } err := json.NewDecoder(r.Body).Decode(&commentData) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information) return } // Get the list of comments rows, err := conn.DB.Query("SELECT id, content, creatorName, created FROM comments WHERE rfcId = $1 AND rfcYear = $2", commentData.RfcId, commentData.RfcYear) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "05"}, information) return } var comments []map[string]interface{} for rows.Next() { var created int var id []byte var content, creatorName string err = rows.Scan(&id, &content, &creatorName, &created) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "06"}, information) return } comments = append(comments, map[string]interface{}{ "id": uuid.Must(uuid.FromBytes(id)).String(), "content": content, "author": creatorName, }) } renderJSON(200, w, map[string]interface{}{ "comments": comments, }, information) }) router.Post("/api/comment/remove", func(w http.ResponseWriter, r *http.Request) { var commentData struct { Id string `json:"id"` JwtToken string `json:"token"` } err := json.NewDecoder(r.Body).Decode(&commentData) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information) return } // Get the username _, username, err := getUsername(commentData.JwtToken, oauthHostName, publicKey) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } // Parse the UUID commentDataId, err := uuid.Parse(commentData.Id) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid UUID"}, information) return } commentDataIdBytes, err := commentDataId.MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "12"}, information) return } // Remove the comment from the database affected, err := conn.DB.Exec("DELETE FROM comments WHERE id = $1 AND creatorName = $2", commentDataIdBytes, username) if err != nil { if errors.Is(err, sql.ErrNoRows) { renderJSON(404, w, map[string]interface{}{"error": "Comment not found"}, information) return } else { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "13"}, information) return } } rowsAffected, err := affected.RowsAffected() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "14"}, information) return } if rowsAffected == 0 { renderJSON(404, w, map[string]interface{}{"error": "Comment not found"}, information) return } renderJSON(200, w, map[string]interface{}{"success": true}, information) }) router.Get("/api/rfc/list", func(w http.ResponseWriter, r *http.Request) { // Get the list of RFCs rows, err := conn.DB.Query("SELECT name, id, year, version FROM rfc ORDER BY year DESC, id DESC") if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "01"}, information) return } var rfcs []map[string]interface{} for rows.Next() { var name, version string var id, year int err = rows.Scan(&name, &id, &year, &version) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "02"}, information) return } rfcs = append(rfcs, map[string]interface{}{ "name": name, "id": id, "year": year, "version": version, }) } renderJSON(200, w, map[string]interface{}{ "rfcs": rfcs, }, information) }) router.Post("/api/rfc/add", func(w http.ResponseWriter, r *http.Request) { var rfcData struct { Name string `json:"name"` Content string `json:"content"` Version string `json:"version"` Year int `json:"year"` Id int `json:"id"` JwtToken string `json:"token"` } err := json.NewDecoder(r.Body).Decode(&rfcData) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information) return } var claims jwt.MapClaims // Verify the JWT token var ok bool claims, ok = verifyJwt(rfcData.JwtToken, publicKey, conn) if !ok { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } // Add the rfc to the database userid, err := uuid.MustParse(claims["sub"].(string)).MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information) } _, err = conn.DB.Exec("INSERT INTO rfc (id, year, name, content, version, creator) VALUES ($1, $2, $3, $4, $5, $6)", rfcData.Id, rfcData.Year, rfcData.Name, rfcData.Content, rfcData.Version, userid) if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information) return } renderJSON(200, w, map[string]interface{}{"success": true}, information) }) router.Post("/api/rfc/remove", func(w http.ResponseWriter, r *http.Request) { var rfcData struct { Id int `json:"id"` Year int `json:"year"` JwtToken string `json:"token"` } err := json.NewDecoder(r.Body).Decode(&rfcData) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information) return } // Verify the JWT token claims, ok := verifyJwt(rfcData.JwtToken, publicKey, conn) if !ok { renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information) return } // Remove the rfc from the database userid, err := uuid.MustParse(claims["sub"].(string)).MarshalBinary() if err != nil { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "08"}, information) } _, err = conn.DB.Exec("DELETE FROM rfc WHERE creator = $1 AND id = $2 AND year = $3", userid, rfcData.Id, rfcData.Year) if err != nil { if errors.Is(err, sql.ErrNoRows) { renderJSON(404, w, map[string]interface{}{"error": "RFC not found"}, information) return } else { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "09"}, information) return } } renderJSON(200, w, map[string]interface{}{"success": true}, information) }) router.Post("/api/rfc/get", func(w http.ResponseWriter, r *http.Request) { var rfcData struct { Id int `json:"id"` Year int `json:"year"` } err := json.NewDecoder(r.Body).Decode(&rfcData) if err != nil { renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information) return } var content, name, version string err = conn.DB.QueryRow("SELECT content, name, version FROM rfc WHERE id = $1 AND year = $2", rfcData.Id, rfcData.Year).Scan(&content, &name, &version) if err != nil { if err != nil && errors.Is(err, sql.ErrNoRows) { renderJSON(404, w, map[string]interface{}{"error": "RFC not found"}, information) return } else { renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "07"}, information) return } } renderJSON(200, w, map[string]interface{}{ "content": content, "name": name, "version": version, }, information) }) // Set up the template routes router.Get("/", func(w http.ResponseWriter, r *http.Request) { renderTemplate(200, w, map[string]interface{}{}, "index.html", information) }) router.Get("/rfc", func(w http.ResponseWriter, r *http.Request) { renderTemplate(200, w, map[string]interface{}{}, "rfc.html", information) }) router.Get("/admin", func(w http.ResponseWriter, r *http.Request) { renderTemplate(200, w, map[string]interface{}{}, "admin.html", information) }) router.Get("/oauth", func(w http.ResponseWriter, r *http.Request) { renderTemplate(200, w, map[string]interface{}{ "ClientId": oauthResponse.AppID, "AuthorizationUri": oauthHostName, }, "oauth.html", information) }) }