480 lines
14 KiB
Go
480 lines
14 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
authdiscord "example.com/auth/discord"
|
|
configserver "example.com/config/server"
|
|
character "example.com/database/character"
|
|
customization "example.com/database/customization"
|
|
function "example.com/database/function"
|
|
functionset "example.com/database/functionset"
|
|
functiontag "example.com/database/functiontag"
|
|
group "example.com/database/group"
|
|
inventoryslot "example.com/database/inventoryslot"
|
|
item "example.com/database/item"
|
|
itemtag "example.com/database/itemtag"
|
|
person "example.com/database/person"
|
|
role "example.com/database/role"
|
|
schematic "example.com/database/schematic"
|
|
tier "example.com/database/tier"
|
|
user "example.com/database/user"
|
|
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/xyproto/randomstring"
|
|
"golang.org/x/oauth2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var GlobalDatabase *gorm.DB
|
|
var GlobalConfig configserver.AppConfig
|
|
var GlobalOAuth *oauth2.Config
|
|
var filteredModelNames []string
|
|
|
|
// Private Functions
|
|
|
|
func updateUser(context *gin.Context, discordUser authdiscord.DiscordUser, oauthTokenJSON string) {
|
|
err := user.Update(
|
|
GlobalDatabase,
|
|
discordUser.Id,
|
|
discordUser.Global_Name,
|
|
discordUser.Username,
|
|
discordUser.Avatar,
|
|
discordUser.Avatar_Decoration_Data.Asset,
|
|
oauthTokenJSON,
|
|
true,
|
|
)
|
|
if err != nil {
|
|
log.Println(err)
|
|
context.AbortWithStatus(http.StatusInternalServerError)
|
|
}
|
|
|
|
}
|
|
|
|
func createUser(context *gin.Context, discordUser authdiscord.DiscordUser, oauthTokenJSON string) {
|
|
err := user.Create(
|
|
GlobalDatabase,
|
|
discordUser.Id,
|
|
discordUser.Global_Name,
|
|
discordUser.Username,
|
|
discordUser.Avatar,
|
|
discordUser.Avatar_Decoration_Data.Asset,
|
|
oauthTokenJSON,
|
|
true,
|
|
)
|
|
if err != nil {
|
|
log.Println(err)
|
|
context.AbortWithStatus(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func getPublicKey(token *jwt.Token) (any, error) {
|
|
userId, err := token.Claims.GetIssuer()
|
|
if err != nil {
|
|
return []byte(""), nil
|
|
}
|
|
apiKeyName, err := token.Claims.GetSubject()
|
|
if err != nil {
|
|
return []byte(""), nil
|
|
}
|
|
var user user.User
|
|
user.Get(GlobalDatabase, userId)
|
|
key := user.GetAPIKeySecret(GlobalDatabase, apiKeyName)
|
|
keyBlock, _ := pem.Decode([]byte(key))
|
|
privateKey, _ := x509.ParseECPrivateKey(keyBlock.Bytes)
|
|
return &privateKey.PublicKey, nil
|
|
}
|
|
|
|
func checkAuthentication(context *gin.Context) *oauth2.Token {
|
|
oauthTokenJSON, err := context.Cookie("discord-oauthtoken")
|
|
if err != nil {
|
|
authHeader := context.Request.Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
return nil
|
|
}
|
|
signedString := strings.Split(authHeader, " ")[1]
|
|
token, err := jwt.ParseWithClaims(signedString, &jwt.RegisteredClaims{}, getPublicKey)
|
|
if err != nil || !token.Valid {
|
|
log.Println(err)
|
|
context.AbortWithStatus(http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
var oauthToken *oauth2.Token
|
|
userId, _ := token.Claims.GetIssuer()
|
|
json.Unmarshal([]byte((*user.Get(GlobalDatabase, []string{userId}))[0].LoginToken), &oauthToken)
|
|
return oauthToken
|
|
}
|
|
var oauthToken *oauth2.Token
|
|
err = json.Unmarshal([]byte(oauthTokenJSON), &oauthToken)
|
|
if err == nil {
|
|
if !oauthToken.Valid() || !(*user.Get(GlobalDatabase, []string{oauthTokenJSON}))[0].LoggedIn {
|
|
return nil
|
|
}
|
|
return oauthToken
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateBasicParams(context *gin.Context) (*[]uint, *[]byte) {
|
|
IDArray, IDOk := context.GetQueryArray("id")
|
|
body := []byte{}
|
|
var err error
|
|
if slices.Contains([]string{"POST", "PUT"}, context.Request.Method) {
|
|
if context.Request.Method == "PUT" && IDOk && len(IDArray) != 1 {
|
|
err = errors.New("invalid number of IDs were included")
|
|
log.Println(err)
|
|
context.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, nil
|
|
}
|
|
body, err = io.ReadAll(context.Request.Body)
|
|
if err != nil {
|
|
log.Println(err)
|
|
context.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, nil
|
|
}
|
|
if !json.Valid(body) {
|
|
err := errors.New("invalid JSON Body")
|
|
log.Println(err)
|
|
context.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, nil
|
|
}
|
|
}
|
|
var IDUintArray []uint
|
|
for _, ID := range IDArray {
|
|
IDUint, err := strconv.Atoi(ID)
|
|
if err != nil {
|
|
log.Println(err)
|
|
context.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, nil
|
|
}
|
|
IDUintArray = append(IDUintArray, uint(IDUint))
|
|
}
|
|
return &IDUintArray, &body
|
|
}
|
|
|
|
func validateRequest(context *gin.Context) (string, *[]uint, *[]byte) {
|
|
// Check that the method is valid
|
|
if !slices.Contains([]string{"GET", "POST", "PUT", "DELETE"}, context.Request.Method) {
|
|
err := errors.New("request must be GET, POST, PUT, or DELETE")
|
|
log.Println(err)
|
|
context.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return "", nil, nil
|
|
}
|
|
// Check authentication if not a GET request
|
|
if (context.Request.Method != "GET") && (checkAuthentication(context) == nil) {
|
|
err := errors.New("must be authenticated to make this kind of request")
|
|
log.Println(err)
|
|
context.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return "", nil, nil
|
|
}
|
|
// Get the object type from the request parameters
|
|
objectType := context.Param("object")
|
|
// Check that the object type is valid
|
|
if !slices.Contains(filteredModelNames, objectType) {
|
|
err := errors.New("requested object type does not exist")
|
|
log.Println(err)
|
|
context.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return "", nil, nil
|
|
}
|
|
// Validate and parse out required parameters from request
|
|
IDUintArray, body := validateBasicParams(context)
|
|
return objectType, IDUintArray, body
|
|
}
|
|
|
|
// Authentication Workflow
|
|
|
|
func AuthCallback(context *gin.Context) {
|
|
// Discord OAuth library requires this.
|
|
oauthState := randomstring.CookieFriendlyString(32)
|
|
context.Set("state", oauthState)
|
|
// Get the OAuth token.
|
|
oauthToken, err := GlobalOAuth.Exchange(context.Request.Context(), context.Query("code"))
|
|
if err != nil {
|
|
context.Redirect(http.StatusInternalServerError, GlobalConfig.GetFrontendRootDomain())
|
|
return
|
|
}
|
|
// Get the current discord user info.
|
|
var currentDiscordUser authdiscord.DiscordUser
|
|
result := currentDiscordUser.Get(context, oauthToken, GlobalOAuth)
|
|
if result != "" {
|
|
context.Redirect(http.StatusInternalServerError, GlobalConfig.GetFrontendRootDomain())
|
|
return
|
|
} else {
|
|
var currentUser user.User
|
|
// Parse object to string so we can save as a cookie.
|
|
oauthTokenJSON, _ := json.Marshal(oauthToken)
|
|
// Either create or update the user in the database
|
|
if currentUser.Get(GlobalDatabase, currentDiscordUser.Id) == nil {
|
|
if currentUser.LoggedIn {
|
|
oauthTokenJSON = []byte(currentUser.LoginToken)
|
|
}
|
|
updateUser(context, currentDiscordUser, string(oauthTokenJSON))
|
|
} else {
|
|
createUser(context, currentDiscordUser, string(oauthTokenJSON))
|
|
}
|
|
// Save the resulting oauthTokenJSON as a cookie.
|
|
context.SetCookie("discord-oauthtoken", string(oauthTokenJSON), 0, "", GlobalConfig.API.Domain, false, false)
|
|
|
|
}
|
|
// Redirect to the dashboard once logged in.
|
|
context.Redirect(http.StatusTemporaryRedirect, GlobalConfig.GetFrontendRootDomain()+"/dashboard")
|
|
}
|
|
|
|
func AuthLoginRedirect(context *gin.Context) {
|
|
context.Redirect(http.StatusTemporaryRedirect, GlobalOAuth.AuthCodeURL(context.GetString("state")))
|
|
}
|
|
|
|
func AuthLogoutRedirect(context *gin.Context) {
|
|
oauthTokenJSON, err := context.Cookie("discord-oauthtoken")
|
|
if err == nil {
|
|
user.Logout(GlobalDatabase, oauthTokenJSON)
|
|
context.SetCookie("discord-oauthtoken", "", -1, "", GlobalConfig.API.Domain, false, true)
|
|
} else {
|
|
log.Println(err)
|
|
}
|
|
context.Redirect(http.StatusTemporaryRedirect, GlobalConfig.GetFrontendRootDomain())
|
|
}
|
|
|
|
// Public Functions
|
|
|
|
func SetFilteredModels() {
|
|
var modelNames []string
|
|
filteredModelNames = []string{}
|
|
result := GlobalDatabase.Table("sqlite_master").Where("type = ?", "table").Pluck("name", &modelNames)
|
|
if result.Error != nil {
|
|
log.Fatal(errors.New("unable to access model names"))
|
|
}
|
|
for _, model := range modelNames {
|
|
if slices.Contains([]string{"api_keys", "sqlite_sequence"}, model) || slices.Contains(strings.Split(model, "_"), "associations") {
|
|
continue
|
|
}
|
|
model = strings.Replace(model, "people", "persons", 1)
|
|
model = strings.Replace(model, "_", "-", -1)
|
|
filteredModelNames = append(filteredModelNames, model[:len(model)-1])
|
|
}
|
|
}
|
|
|
|
func ObjectRequest(context *gin.Context) {
|
|
// Make sure request has valid parameters
|
|
objectType, IDUintArray, body := validateRequest(context)
|
|
// This shouldn't happen, but just in case...
|
|
if objectType == "" {
|
|
return
|
|
}
|
|
// Determine how to handle the request
|
|
var err error
|
|
method := context.Request.Method
|
|
resultJSON := gin.H{}
|
|
switch objectType {
|
|
case "user":
|
|
user.GetAll(GlobalDatabase)
|
|
case "person":
|
|
result, resultError := person.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "group":
|
|
result, resultError := group.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "character":
|
|
result, resultError := character.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "role":
|
|
result, resultError := role.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "tier":
|
|
result, resultError := tier.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "function-set":
|
|
result, resultError := functionset.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "function":
|
|
result, resultError := function.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "function-tag":
|
|
result, resultError := functiontag.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "inventory-slot":
|
|
result, resultError := inventoryslot.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "item":
|
|
result, resultError := item.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "item-tag":
|
|
result, resultError := itemtag.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "customization":
|
|
result, resultError := customization.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
case "schematic":
|
|
result, resultError := schematic.HandleRequest(method, GlobalDatabase, IDUintArray, body)
|
|
err = resultError
|
|
resultJSON = gin.H{
|
|
"result": result,
|
|
}
|
|
default:
|
|
err = errors.New("request made for object that exists, but is not implemented")
|
|
context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.Println(err)
|
|
context.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
context.JSON(http.StatusOK, resultJSON)
|
|
}
|
|
|
|
func CreateAPIToken(context *gin.Context) {
|
|
name, nameOK := context.GetQuery("name")
|
|
if nameOK {
|
|
oauthToken := checkAuthentication(context)
|
|
if oauthToken != nil {
|
|
oauthTokenJSON, err := json.Marshal(&oauthToken)
|
|
if err != nil {
|
|
log.Println("This should never happen, how did this happen???\n", err)
|
|
context.AbortWithStatus(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
var userId string
|
|
GlobalDatabase.Model(&user.User{}).Select("id").Where("login_token = ?", string(oauthTokenJSON)).Take(&userId)
|
|
currentUser := (*user.Get(GlobalDatabase, []string{userId}))[0]
|
|
result := currentUser.GenerateAPIKey(GlobalDatabase, name)
|
|
if result != nil {
|
|
log.Println("This should also never happen, how did this happen???\n", err)
|
|
context.AbortWithStatus(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
|
|
"iss": currentUser.Id,
|
|
"sub": name,
|
|
})
|
|
context.JSON(http.StatusOK, gin.H{
|
|
"token": token,
|
|
"secret": currentUser.GetAPIKeySecret(GlobalDatabase, name),
|
|
"claims": gin.H{
|
|
"iss": currentUser.Id,
|
|
"sub": name,
|
|
},
|
|
})
|
|
}
|
|
} else {
|
|
context.AbortWithStatus(http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func GetDiscordUser(context *gin.Context) {
|
|
oauthToken := checkAuthentication(context)
|
|
if oauthToken != nil {
|
|
var discordUser authdiscord.DiscordUser
|
|
result := discordUser.Get(context, oauthToken, GlobalOAuth)
|
|
if result != "" {
|
|
log.Println("Assuming the Discord OAuth Key has expired.")
|
|
context.Redirect(http.StatusUnauthorized, GlobalConfig.GetFrontendRootDomain()+"/logout")
|
|
} else {
|
|
if (*user.Get(GlobalDatabase, []string{discordUser.Id}))[0].LoggedIn {
|
|
context.JSON(http.StatusOK, discordUser)
|
|
} else {
|
|
context.Redirect(http.StatusTemporaryRedirect, GlobalConfig.GetFrontendRootDomain()+"/logout")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func GetUserLoggedIn(context *gin.Context) {
|
|
context.JSON(http.StatusOK, gin.H{
|
|
"message": (checkAuthentication(context) != nil),
|
|
})
|
|
}
|
|
|
|
func InitializeRouter() *gin.Engine {
|
|
router := gin.Default()
|
|
router.Use(cors.New(cors.Config{
|
|
AllowOrigins: []string{GlobalConfig.GetFrontendRootDomain()},
|
|
AllowMethods: []string{"PUT", "POST", "DELETE", "GET"},
|
|
AllowHeaders: []string{"Origin"},
|
|
ExposeHeaders: []string{"Content-Length"},
|
|
AllowCredentials: true,
|
|
}))
|
|
// Testing
|
|
router.GET("/ping", func(context *gin.Context) {
|
|
context.Status(http.StatusOK)
|
|
})
|
|
// Authentication Workflow
|
|
router.GET("/auth/callback", AuthCallback)
|
|
router.GET("/auth/login", AuthLoginRedirect)
|
|
router.GET("/auth/logout", AuthLogoutRedirect)
|
|
// User methods
|
|
router.GET("/user/token/generate", CreateAPIToken)
|
|
router.GET("/user/info", GetDiscordUser)
|
|
router.GET("/user/authorized", GetUserLoggedIn)
|
|
// Object Requests
|
|
router.POST("/:object", ObjectRequest)
|
|
router.PUT("/:object", ObjectRequest)
|
|
router.GET("/:object", ObjectRequest)
|
|
router.DELETE("/:object", ObjectRequest)
|
|
return router
|
|
}
|