2024-10-15 17:27:54 +01:00
package main
import (
"errors"
2025-01-08 18:21:32 +00:00
"fmt"
2024-10-15 17:27:54 +01:00
"time"
"crypto/ed25519"
"database/sql"
"encoding/json"
2024-10-15 19:05:49 +01:00
2025-01-08 14:40:38 +00:00
library "git.ailur.dev/ailur/fg-library/v3"
2024-10-15 17:27:54 +01:00
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
2025-01-08 14:40:38 +00:00
Router : true , // This service does require a router
2024-10-15 17:27:54 +01:00
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" ) ,
}
2025-01-08 14:40:38 +00:00
var (
loggerService = uuid . MustParse ( "00000000-0000-0000-0000-000000000002" )
)
2025-01-08 18:21:32 +00:00
func logFunc ( message string , messageType library . MessageCode , information * library . ServiceInitializationInformation ) {
2024-10-15 17:27:54 +01:00
// Log the message to the logger service
2025-01-08 18:21:32 +00:00
fmt . Println ( "here i am, sending a message to the logger service" )
2025-01-08 14:40:38 +00:00
information . SendISMessage ( loggerService , messageType , message )
2024-10-15 17:27:54 +01:00
}
2025-01-08 18:21:32 +00:00
func renderTemplate ( statusCode int , w http . ResponseWriter , data map [ string ] interface { } , templatePath string , information * library . ServiceInitializationInformation ) {
2024-10-15 17:27:54 +01:00
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 )
}
}
}
2025-01-08 18:21:32 +00:00
func renderString ( statusCode int , w http . ResponseWriter , data string , information * library . ServiceInitializationInformation ) {
2024-10-15 17:27:54 +01:00
w . WriteHeader ( statusCode )
_ , err := w . Write ( [ ] byte ( data ) )
if err != nil {
logFunc ( err . Error ( ) , 2 , information )
}
}
2025-01-08 18:21:32 +00:00
func renderJSON ( statusCode int , w http . ResponseWriter , data map [ string ] interface { } , information * library . ServiceInitializationInformation ) {
2024-10-15 17:27:54 +01:00
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 )
2025-01-08 14:40:38 +00:00
if err != nil {
return "" , "" , err
}
2024-10-15 17:27:54 +01:00
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
}
2025-01-08 18:21:32 +00:00
func Main ( information * library . ServiceInitializationInformation ) {
2024-10-15 17:27:54 +01:00
var conn library . Database
hostName := information . Configuration [ "hostName" ] . ( string )
2025-01-08 18:21:32 +00:00
go information . StartISProcessor ( )
2024-10-15 17:27:54 +01:00
// Initiate a connection to the database
2025-01-08 18:21:32 +00:00
fmt . Println ( "Connecting to the database" )
2025-01-08 14:40:38 +00:00
conn , err := information . GetDatabase ( )
if err != nil {
logFunc ( err . Error ( ) , 3 , information )
return
2024-10-15 17:27:54 +01:00
}
2025-01-08 14:40:38 +00:00
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 )
2024-10-15 17:27:54 +01:00
}
2025-01-08 14:40:38 +00:00
// 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 )
2024-10-15 17:27:54 +01:00
}
2025-01-08 14:40:38 +00:00
// 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 )
2024-10-15 19:28:54 +01:00
}
} else {
2025-01-08 14:40:38 +00:00
// 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 )
}
2024-10-15 19:28:54 +01:00
}
2025-01-08 14:40:38 +00:00
// Initialize the OAuth
oauthResponse , publicKey , oauthHostName , err := authLibrary . InitializeOAuth ( authLibrary . OAuthInformation {
Name : "datatracker" ,
RedirectUri : hostName + "/oauth" ,
Scopes : [ ] string { "openid" } ,
} , information )
2024-10-15 17:27:54 +01:00
if err != nil {
logFunc ( err . Error ( ) , 3 , information )
2025-01-08 14:40:38 +00:00
return
2024-10-15 17:27:54 +01:00
}
// Set up the router
2024-10-20 19:54:08 +01:00
router := information . Router
2024-10-15 17:27:54 +01:00
// 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
2024-10-15 19:28:54 +01:00
sub , username , err := getUsername ( commentData . JwtToken , oauthHostName , publicKey )
2024-10-15 17:27:54 +01:00
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
2024-10-15 19:28:54 +01:00
_ , username , err := getUsername ( commentData . JwtToken , oauthHostName , publicKey )
2024-10-15 17:27:54 +01:00
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
2024-10-15 20:03:34 +01:00
rows , err := conn . DB . Query ( "SELECT name, id, year, version FROM rfc ORDER BY year DESC, id DESC" )
2024-10-15 17:27:54 +01:00
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 { } {
2024-10-15 19:28:54 +01:00
"ClientId" : oauthResponse . AppID ,
"AuthorizationUri" : oauthHostName ,
2024-10-15 17:27:54 +01:00
} , "oauth.html" , information )
} )
}