cpularp-manager-api/src/lib/api/api.go

315 lines
9.4 KiB
Go
Raw Normal View History

2025-04-24 13:58:05 -05:00
package api
import (
"crypto/x509"
2025-04-24 13:58:05 -05:00
"encoding/json"
"encoding/pem"
2025-04-24 13:58:05 -05:00
"log"
"net/http"
"slices"
"strings"
2025-04-24 13:58:05 -05:00
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"
2025-04-24 13:58:05 -05:00
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
2025-04-24 13:58:05 -05:00
"github.com/xyproto/randomstring"
"golang.org/x/oauth2"
"gorm.io/gorm"
)
var GlobalDatabase *gorm.DB
var GlobalConfig configserver.AppConfig
var GlobalOAuth *oauth2.Config
// 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 checkAuthentication(context *gin.Context) *oauth2.Token {
oauthTokenJSON, err := context.Cookie("discord-oauthtoken")
if err == nil {
var oauthToken *oauth2.Token
err := json.Unmarshal([]byte(oauthTokenJSON), &oauthToken)
if err == nil {
if oauthToken.Valid() {
if (*user.Get(GlobalDatabase, []string{oauthTokenJSON}))[0].LoggedIn {
return oauthToken
}
}
}
} else {
if authHeader := context.Request.Header.Get("Authorization"); authHeader != "" {
signedString := strings.Split(authHeader, " ")[1]
token, err := jwt.ParseWithClaims(signedString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, 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
})
if err != nil {
log.Println(err)
context.AbortWithStatus(http.StatusBadRequest)
return nil
}
if token.Valid {
var oauthToken *oauth2.Token
userId, _ := token.Claims.GetIssuer()
json.Unmarshal([]byte((*user.Get(GlobalDatabase, []string{userId}))[0].LoginToken), &oauthToken)
return oauthToken
}
}
}
return nil
}
2025-04-24 17:08:09 -05:00
// Authentication Workflow
func AuthCallback(context *gin.Context) {
// Discord OAuth library requires this.
2025-04-24 17:08:09 -05:00
oauthState := randomstring.CookieFriendlyString(32)
context.Set("state", oauthState)
// Get the OAuth token.
2025-04-24 17:08:09 -05:00
oauthToken, err := GlobalOAuth.Exchange(context.Request.Context(), context.Query("code"))
if err != nil {
context.Redirect(http.StatusInternalServerError, GlobalConfig.GetFrontendRootDomain())
return
2025-04-24 17:08:09 -05:00
}
// 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")
2025-04-24 17:08:09 -05:00
}
func AuthLoginRedirect(context *gin.Context) {
context.Redirect(http.StatusTemporaryRedirect, GlobalOAuth.AuthCodeURL(context.GetString("state")))
2025-04-24 13:58:05 -05:00
}
2025-04-24 17:08:09 -05:00
func AuthLogoutRedirect(context *gin.Context) {
2025-04-24 13:58:05 -05:00
oauthTokenJSON, err := context.Cookie("discord-oauthtoken")
if err == nil {
2025-04-29 00:42:57 -05:00
user.Logout(GlobalDatabase, oauthTokenJSON)
2025-04-24 13:58:05 -05:00
context.SetCookie("discord-oauthtoken", "", -1, "", GlobalConfig.API.Domain, false, true)
} else {
log.Println(err)
}
context.Redirect(http.StatusTemporaryRedirect, GlobalConfig.GetFrontendRootDomain())
2025-04-24 13:58:05 -05:00
}
// Public Functions
func ObjectRequest(context *gin.Context) {
if !slices.Contains([]string{"GET", "POST", "PUT", "DELETE"}, context.Request.Method) {
context.AbortWithStatus(http.StatusBadRequest)
return
}
if (context.Request.Method != "GET") && (checkAuthentication(context) == nil) {
context.AbortWithStatus(http.StatusUnauthorized)
return
}
var modelNames []string
result := GlobalDatabase.Table("sqlite_master").Where("type = ?", "table").Pluck("name", &modelNames)
if result.Error != nil {
context.AbortWithStatus(http.StatusInternalServerError)
return
}
var filteredModelNames []string
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])
}
objectType := context.Param("object")
if !slices.Contains(filteredModelNames, objectType) {
context.AbortWithStatus(http.StatusBadRequest)
return
}
var err error
switch objectType {
case "user":
user.GetAll(GlobalDatabase)
case "person":
person.GetAll(GlobalDatabase)
case "group":
err = group.HandleRequest(GlobalDatabase, context)
case "character":
character.GetAll(GlobalDatabase)
case "role":
role.GetAll(GlobalDatabase)
case "tier":
tier.GetAll(GlobalDatabase)
case "function-set":
functionset.GetAll(GlobalDatabase)
case "function":
// result = function.Create(GlobalDatabase, context)
function.GetAll(GlobalDatabase)
case "function-tag":
// result = functiontag.Create(GlobalDatabase, context)
functiontag.GetAll(GlobalDatabase)
case "inventory-slot":
inventoryslot.GetAll(GlobalDatabase)
case "item":
item.GetAll(GlobalDatabase)
case "item-tag":
itemtag.GetAll(GlobalDatabase)
case "customization":
customization.GetAll(GlobalDatabase)
case "schematic":
schematic.GetAll(GlobalDatabase)
default:
context.AbortWithStatus(http.StatusBadRequest)
return
}
if err != nil {
context.Status(http.StatusInternalServerError)
return
}
context.Status(http.StatusOK)
}
2025-04-29 00:42:57 -05:00
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)
}
}
2025-04-29 00:42:57 -05:00
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(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)
2025-04-24 17:08:09 -05:00
} else {
context.Redirect(http.StatusTemporaryRedirect, GlobalConfig.GetFrontendRootDomain()+"/logout")
2025-04-24 13:58:05 -05:00
}
}
}
}
2025-04-29 00:42:57 -05:00
func GetUserLoggedIn(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"message": (checkAuthentication(context) != nil),
2025-04-24 13:58:05 -05:00
})
}