// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. This file is part of cc-backend.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

// Package auth implements various authentication methods
package auth

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/gob"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"os"
	"sync"
	"time"

	"golang.org/x/time/rate"

	"github.com/ClusterCockpit/cc-backend/internal/config"
	"github.com/ClusterCockpit/cc-backend/internal/repository"
	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
	"github.com/ClusterCockpit/cc-lib/v2/schema"
	"github.com/ClusterCockpit/cc-lib/v2/util"
	"github.com/alexedwards/scs/sqlite3store"
	"github.com/alexedwards/scs/v2"
)

// Authenticator is the interface for all authentication methods.
// Each authenticator determines if it can handle a login request (CanLogin)
// and performs the actual authentication (Login).
type Authenticator interface {
	// CanLogin determines if this authenticator can handle the login request.
	// It returns the user object if available and a boolean indicating if this
	// authenticator should attempt the login. This method should not perform
	// expensive operations or actual authentication.
	CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) (*schema.User, bool)

	// Login performs the actually authentication for the user.
	// It returns the authenticated user or an error if authentication fails.
	// The user parameter may be nil if the user doesn't exist in the database yet.
	Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error)
}

var (
	initOnce     sync.Once
	authInstance *Authentication
)

// rateLimiterEntry tracks a rate limiter and its last use time for cleanup
type rateLimiterEntry struct {
	limiter  *rate.Limiter
	lastUsed time.Time
}

var ipUserLimiters sync.Map

// getIPUserLimiter returns a rate limiter for the given IP and username combination.
// Rate limiters are created on demand and track 5 attempts per 15 minutes.
func getIPUserLimiter(ip, username string) *rate.Limiter {
	key := ip + ":" + username
	now := time.Now()

	if entry, ok := ipUserLimiters.Load(key); ok {
		rle := entry.(*rateLimiterEntry)
		rle.lastUsed = now
		return rle.limiter
	}

	// More aggressive rate limiting: 5 attempts per 15 minutes
	newLimiter := rate.NewLimiter(rate.Every(15*time.Minute/5), 5)
	ipUserLimiters.Store(key, &rateLimiterEntry{
		limiter:  newLimiter,
		lastUsed: now,
	})
	return newLimiter
}

// cleanupOldRateLimiters removes rate limiters that haven't been used recently
func cleanupOldRateLimiters(olderThan time.Time) {
	ipUserLimiters.Range(func(key, value any) bool {
		entry := value.(*rateLimiterEntry)
		if entry.lastUsed.Before(olderThan) {
			ipUserLimiters.Delete(key)
			cclog.Debugf("Cleaned up rate limiter for %v", key)
		}
		return true
	})
}

// startRateLimiterCleanup starts a background goroutine to clean up old rate limiters
func startRateLimiterCleanup() {
	go func() {
		ticker := time.NewTicker(1 * time.Hour)
		defer ticker.Stop()
		for range ticker.C {
			// Clean up limiters not used in the last 24 hours
			cleanupOldRateLimiters(time.Now().Add(-24 * time.Hour))
		}
	}()
}

// AuthConfig contains configuration for all authentication methods
type AuthConfig struct {
	LdapConfig   *LdapConfig    `json:"ldap"`
	JwtConfig    *JWTAuthConfig `json:"jwts"`
	OpenIDConfig *OpenIDConfig  `json:"oidc"`
}

// Keys holds the global authentication configuration
var Keys AuthConfig

// secretFromEnv resolves a secret from the environment or config. The
// environment variable takes precedence when set and non-empty; otherwise the
// value configured in config.json is used. This lets deployments inject secrets
// via the environment (or a secret manager) while keeping config.json
// self-contained for simple setups.
func secretFromEnv(envVar, configValue string) string {
	if v := os.Getenv(envVar); v != "" {
		return v
	}
	return configValue
}

// Authentication manages all authentication methods and session handling
type Authentication struct {
	sessionManager *scs.SessionManager
	LdapAuth       *LdapAuthenticator
	JwtAuth        *JWTAuthenticator
	LocalAuth      *LocalAuthenticator
	authenticators []Authenticator
	SessionMaxAge  time.Duration
}

// SessionManager exposes the scs session manager so the HTTP router can install
// the session middleware (LoadAndSave on write paths, LoadSession on read paths).
func (auth *Authentication) SessionManager() *scs.SessionManager {
	return auth.sessionManager
}

// LoadSession is a non-buffering, read-only session middleware. It loads any
// existing session into the request context so AuthViaSession can read it, but
// (unlike scs.LoadAndSave) it never wraps the ResponseWriter and never commits,
// so large responses (e.g. the GraphQL /query endpoint) stream without being
// buffered in memory. Use it on routes that only read the session to
// authenticate; use scs.LoadAndSave on the login/logout routes that mutate it.
func (auth *Authentication) LoadSession(next http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		var token string
		if c, err := r.Cookie(auth.sessionManager.Cookie.Name); err == nil {
			token = c.Value
		}
		ctx, err := auth.sessionManager.Load(r.Context(), token)
		if err != nil {
			cclog.Errorf("session load failed: %s", err.Error())
			http.Error(rw, "internal server error", http.StatusInternalServerError)
			return
		}
		next.ServeHTTP(rw, r.WithContext(ctx))
	})
}

// expireSessionCookie clears a (corrupted) session cookie on the client. Used on
// read paths where there is no commit to drive the deletion.
func expireSessionCookie(rw http.ResponseWriter) {
	http.SetCookie(rw, &http.Cookie{
		Name:     "session",
		Path:     "/",
		MaxAge:   -1,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	})
}

func (auth *Authentication) AuthViaSession(
	rw http.ResponseWriter,
	r *http.Request,
) (*schema.User, error) {
	// The session data was loaded into the request context by the LoadSession
	// middleware. No active session cookie => not logged in (mirrors session.IsNew).
	ctx := r.Context()
	if !auth.sessionManager.Exists(ctx, "username") {
		return nil, nil
	}

	// Validate session data with proper type checking
	username := auth.sessionManager.GetString(ctx, "username")
	if username == "" {
		cclog.Warn("Invalid session: missing or invalid username")
		expireSessionCookie(rw)
		return nil, errors.New("invalid session data")
	}

	projects, ok := auth.sessionManager.Get(ctx, "projects").([]string)
	if !ok {
		cclog.Warn("Invalid session: projects not found or invalid type, using empty list")
		projects = []string{}
	}

	roles, ok := auth.sessionManager.Get(ctx, "roles").([]string)
	if !ok || len(roles) == 0 {
		cclog.Warn("Invalid session: missing or invalid roles")
		expireSessionCookie(rw)
		return nil, errors.New("invalid session data")
	}

	// GetInt returns 0 (== schema.AuthViaLocalPassword) when the key is absent.
	authSourceInt := auth.sessionManager.GetInt(ctx, "authSource")

	return &schema.User{
		Username:   username,
		Projects:   projects,
		Roles:      roles,
		AuthType:   schema.AuthSession,
		AuthSource: schema.AuthSource(authSourceInt),
	}, nil
}

func Init(authCfg *json.RawMessage) {
	initOnce.Do(func() {
		authInstance = &Authentication{}

		// Start background cleanup of rate limiters
		startRateLimiterCleanup()

		// Server-side sessions via scs, persisted in the existing SQLite DB so
		// sessions survive restarts. Only an opaque random token is stored in the
		// cookie, so no secret signing key (the former SESSION_KEY) is required.
		gob.Register([]string{}) // user.Projects / user.Roles are stored as []string
		sm := scs.New()
		sm.Store = sqlite3store.New(repository.GetConnection().DB.DB)
		sm.Cookie.Name = "session"
		sm.Cookie.Path = "/"
		sm.Cookie.HttpOnly = true
		sm.Cookie.SameSite = http.SameSiteLaxMode
		// scs sets Secure globally (no per-request option). Enable it when this
		// process terminates TLS itself. Deployments terminating TLS at a reverse
		// proxy can set this via a future config flag if needed.
		sm.Cookie.Secure = config.Keys.HTTPSCertFile != ""

		if d, err := time.ParseDuration(config.Keys.SessionMaxAge); err == nil && d != 0 {
			sm.Lifetime = d
			authInstance.SessionMaxAge = d
		} else {
			// SessionMaxAge of 0/empty means "do not expire": approximate with a
			// long absolute lifetime (the cookie remains persistent).
			sm.Lifetime = 10 * 365 * 24 * time.Hour
		}
		authInstance.sessionManager = sm

		// When authentication is disabled no authenticators are required; the
		// session store created above is enough for the server to run with a
		// valid (non-nil) auth instance.
		if config.Keys.DisableAuthentication {
			return
		}

		if authCfg == nil {
			return
		}

		config.Validate(configSchema, *authCfg)
		dec := json.NewDecoder(bytes.NewReader(*authCfg))
		dec.DisallowUnknownFields()
		if err := dec.Decode(&Keys); err != nil {
			cclog.Errorf("error while decoding ldap config: %v", err)
		}

		if Keys.LdapConfig != nil {
			ldapAuth := &LdapAuthenticator{}
			if err := ldapAuth.Init(); err != nil {
				cclog.Warn("Error while initializing authentication -> ldapAuth init failed")
			} else {
				authInstance.LdapAuth = ldapAuth
				authInstance.authenticators = append(authInstance.authenticators, authInstance.LdapAuth)
			}
		} else {
			cclog.Info("Missing LDAP configuration: No LDAP support!")
		}

		if Keys.JwtConfig != nil {
			authInstance.JwtAuth = &JWTAuthenticator{}
			if err := authInstance.JwtAuth.Init(); err != nil {
				cclog.Fatal("Error while initializing authentication -> jwtAuth init failed")
			}

			jwtSessionAuth := &JWTSessionAuthenticator{}
			if err := jwtSessionAuth.Init(); err != nil {
				cclog.Info("jwtSessionAuth init failed: No JWT login support!")
			} else {
				authInstance.authenticators = append(authInstance.authenticators, jwtSessionAuth)
			}

			jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
			if err := jwtCookieSessionAuth.Init(); err != nil {
				cclog.Info("jwtCookieSessionAuth init failed: No JWT cookie login support!")
			} else {
				authInstance.authenticators = append(authInstance.authenticators, jwtCookieSessionAuth)
			}
		} else {
			cclog.Info("Missing JWT configuration: No JWT token support!")
		}

		authInstance.LocalAuth = &LocalAuthenticator{}
		if err := authInstance.LocalAuth.Init(); err != nil {
			cclog.Fatal("Error while initializing authentication -> localAuth init failed")
		}
		authInstance.authenticators = append(authInstance.authenticators, authInstance.LocalAuth)
	})
}

func GetAuthInstance() *Authentication {
	if authInstance == nil {
		cclog.Fatal("Authentication module not initialized!")
	}

	return authInstance
}

// handleUserSync syncs or updates a user in the database based on configuration.
// This is used for LDAP, JWT and OIDC authentications when syncUserOnLogin or updateUserOnLogin is enabled.
func handleUserSync(user *schema.User, syncUserOnLogin, updateUserOnLogin bool) {
	r := repository.GetUserRepository()
	dbUser, err := r.GetUser(user.Username)

	if err != nil && err != sql.ErrNoRows {
		cclog.Errorf("Error while loading user '%s': %v", user.Username, err)
		return
	}

	if err == sql.ErrNoRows && syncUserOnLogin { // Add new user
		if err := r.AddUser(user); err != nil {
			cclog.Errorf("Error while adding user '%s' to DB: %v", user.Username, err)
		}
	} else if err == nil && updateUserOnLogin { // Update existing user
		if err := r.UpdateUser(dbUser, user); err != nil {
			cclog.Errorf("Error while updating user '%s' in DB: %v", dbUser.Username, err)
		}
	}
}

// handleTokenUser syncs JWT token user with database
func handleTokenUser(tokenUser *schema.User) {
	handleUserSync(tokenUser, Keys.JwtConfig.SyncUserOnLogin, Keys.JwtConfig.UpdateUserOnLogin)
}

// handleOIDCUser syncs OIDC user with database
func handleOIDCUser(OIDCUser *schema.User) {
	handleUserSync(OIDCUser, Keys.OpenIDConfig.SyncUserOnLogin, Keys.OpenIDConfig.UpdateUserOnLogin)
}

// handleLdapUser syncs LDAP user with database
func handleLdapUser(ldapUser *schema.User) {
	handleUserSync(ldapUser, Keys.LdapConfig.SyncUserOnLogin, Keys.LdapConfig.UpdateUserOnLogin)
}

func (auth *Authentication) SaveSession(rw http.ResponseWriter, r *http.Request, user *schema.User) error {
	// The login routes are wrapped by scs.LoadAndSave, which loaded the session
	// into the request context and will commit it (persist to the store and write
	// the Set-Cookie header) after the handler returns.
	ctx := r.Context()

	// Generate a new session token to prevent session fixation.
	if err := auth.sessionManager.RenewToken(ctx); err != nil {
		cclog.Errorf("session renew failed: %s", err.Error())
		http.Error(rw, err.Error(), http.StatusInternalServerError)
		return err
	}

	auth.sessionManager.Put(ctx, "username", user.Username)
	auth.sessionManager.Put(ctx, "projects", user.Projects)
	auth.sessionManager.Put(ctx, "roles", user.Roles)
	auth.sessionManager.Put(ctx, "authSource", int(user.AuthSource))

	return nil
}

func (auth *Authentication) Login(
	onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		ip, _, err := net.SplitHostPort(r.RemoteAddr)
		if err != nil {
			ip = r.RemoteAddr
		}

		username := r.FormValue("username")

		limiter := getIPUserLimiter(ip, username)
		if !limiter.Allow() {
			cclog.Warnf("AUTH/RATE > Too many login attempts for combination IP: %s, Username: %s", ip, username)
			onfailure(rw, r, errors.New("too many login attempts, try again in a few minutes"))
			return
		}

		var dbUser *schema.User
		if username != "" {
			var err error
			dbUser, err = repository.GetUserRepository().GetUser(username)
			if err != nil && err != sql.ErrNoRows {
				cclog.Errorf("Error while loading user '%v'", username)
			}
		}

		for _, authenticator := range auth.authenticators {
			var ok bool
			var user *schema.User
			if user, ok = authenticator.CanLogin(dbUser, username, rw, r); !ok {
				continue
			} else {
				cclog.Debugf("Can login with user %v", user)
			}

			user, err := authenticator.Login(user, rw, r)
			if err != nil {
				cclog.Warnf("user login failed: %s", err.Error())
				onfailure(rw, r, err)
				return
			}

			if err := auth.SaveSession(rw, r, user); err != nil {
				return
			}

			cclog.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
			ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)

			if redirect := r.FormValue("redirect"); redirect != "" {
				if u, perr := url.Parse(redirect); perr == nil && u.Scheme == "" && u.Host == "" {
					http.RedirectHandler(redirect, http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
					return
				}
				cclog.Warnf("login redirect rejected (not a relative path): %q", redirect)
			}

			http.RedirectHandler("/", http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
			return
		}

		cclog.Debugf("login failed: no authenticator applied")
		onfailure(rw, r, errors.New("no authenticator applied"))
	})
}

func (auth *Authentication) Auth(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.JwtAuth.AuthViaJWT(rw, r)
		if err != nil {
			cclog.Infof("auth -> authentication failed: %s", err.Error())
			http.Error(rw, err.Error(), http.StatusUnauthorized)
			return
		}
		if user == nil {
			user, err = auth.AuthViaSession(rw, r)
			if err != nil {
				cclog.Infof("auth -> authentication failed: %s", err.Error())
				http.Error(rw, err.Error(), http.StatusUnauthorized)
				return
			}
		}
		if user != nil {
			ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
			return
		}

		cclog.Infof("auth -> authentication failed: no valid session or JWT for %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
		onfailure(rw, r, errors.New("unauthorized (please login first)"))
	})
}

func (auth *Authentication) AuthAPI(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.JwtAuth.AuthViaJWT(rw, r)
		if err != nil {
			cclog.Infof("auth api -> authentication failed: %s", err.Error())
			onfailure(rw, r, err)
			return
		}

		ipErr := securedCheck(user, r)
		if ipErr != nil {
			cclog.Infof("auth api -> secured check failed: %s", ipErr.Error())
			onfailure(rw, r, ipErr)
			return
		}

		if user != nil {
			switch {
			case len(user.Roles) == 1:
				if user.HasRole(schema.RoleAPI) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			case len(user.Roles) >= 2:
				if user.HasAllRoles([]schema.Role{schema.RoleAdmin, schema.RoleAPI}) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			default:
				cclog.Info("auth api -> authentication failed: missing role")
				onfailure(rw, r, errors.New("unauthorized"))
			}
		}
		cclog.Info("auth api -> authentication failed: no auth")
		onfailure(rw, r, errors.New("unauthorized"))
	})
}

func (auth *Authentication) AuthUserAPI(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.JwtAuth.AuthViaJWT(rw, r)
		if err != nil {
			cclog.Infof("auth user api -> authentication failed: %s", err.Error())
			onfailure(rw, r, err)
			return
		}

		if user != nil {
			switch {
			case len(user.Roles) == 1:
				if user.HasRole(schema.RoleAPI) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			case len(user.Roles) >= 2:
				if user.HasRole(schema.RoleAPI) && user.HasAnyRole([]schema.Role{schema.RoleUser, schema.RoleManager, schema.RoleSupport, schema.RoleAdmin}) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			default:
				cclog.Info("auth user api -> authentication failed: missing role")
				onfailure(rw, r, errors.New("unauthorized"))
			}
		}
		cclog.Info("auth user api -> authentication failed: no auth")
		onfailure(rw, r, errors.New("unauthorized"))
	})
}

func (auth *Authentication) AuthMetricStoreAPI(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.JwtAuth.AuthViaJWT(rw, r)
		if err != nil {
			cclog.Infof("auth metricstore api -> authentication failed: %s", err.Error())
			onfailure(rw, r, err)
			return
		}

		if user != nil {
			switch {
			case len(user.Roles) == 1:
				if user.HasRole(schema.RoleAPI) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			case len(user.Roles) >= 2:
				if user.HasRole(schema.RoleAPI) && user.HasAnyRole([]schema.Role{schema.RoleUser, schema.RoleManager, schema.RoleAdmin}) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			default:
				cclog.Info("auth metricstore api -> authentication failed: missing role")
				onfailure(rw, r, errors.New("unauthorized"))
			}
		}
		cclog.Info("auth metricstore api -> authentication failed: no auth")
		onfailure(rw, r, errors.New("unauthorized"))
	})
}

func (auth *Authentication) AuthConfigAPI(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.AuthViaSession(rw, r)
		if err != nil {
			cclog.Infof("auth config api -> authentication failed: %s", err.Error())
			onfailure(rw, r, err)
			return
		}
		if user != nil && user.HasRole(schema.RoleAdmin) {
			ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
			return
		}
		cclog.Info("auth config api -> authentication failed: no auth")
		onfailure(rw, r, errors.New("unauthorized"))
	})
}

func (auth *Authentication) AuthFrontendAPI(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.AuthViaSession(rw, r)
		if err != nil {
			cclog.Infof("auth frontend api -> authentication failed: %s", err.Error())
			onfailure(rw, r, err)
			return
		}
		if user != nil {
			ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
			return
		}
		cclog.Info("auth frontend api -> authentication failed: no auth")
		onfailure(rw, r, errors.New("unauthorized"))
	})
}

func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		// The logout route is wrapped by scs.LoadAndSave: Destroy removes the
		// session from the store and the middleware clears the cookie on the way out.
		if err := auth.sessionManager.Destroy(r.Context()); err != nil {
			http.Error(rw, err.Error(), http.StatusInternalServerError)
			return
		}

		onsuccess.ServeHTTP(rw, r)
	})
}

// Helper Moved To MiddleWare Auth Handlers
func securedCheck(user *schema.User, r *http.Request) error {
	if user == nil {
		return fmt.Errorf("no user for secured check")
	}

	// extract IP address for checking
	IPAddress := r.Header.Get("X-Real-Ip")
	if IPAddress == "" {
		IPAddress = r.Header.Get("X-Forwarded-For")
	}
	if IPAddress == "" {
		IPAddress = r.RemoteAddr
	}

	// Handle both IPv4 and IPv6 addresses properly
	// For IPv6, this will strip the port and brackets
	// For IPv4, this will strip the port
	if host, _, err := net.SplitHostPort(IPAddress); err == nil {
		IPAddress = host
	}
	// If SplitHostPort fails, IPAddress is already just a host (no port)

	// If nothing declared in config: Continue // FIXME: Allow All If Not Declared?
	if len(config.Keys.APIAllowedIPs) == 0 {
		return nil
	}
	// If wildcard declared in config: Continue
	if config.Keys.APIAllowedIPs[0] == "*" {
		return nil
	}
	// check if IP is allowed
	if !util.Contains(config.Keys.APIAllowedIPs, IPAddress) {
		return fmt.Errorf("unknown ip: %v", IPAddress)
	}

	return nil
}
