Index: binary-improvements2/WebServer/ModInfo.xml
===================================================================
--- binary-improvements2/WebServer/ModInfo.xml	(revision 425)
+++ binary-improvements2/WebServer/ModInfo.xml	(revision 426)
@@ -5,5 +5,5 @@
 	<Description value="Integrated Webserver for the Web Dashboard and server APIs" />
 	<Author value="The Fun Pimps LLC" />
-	<Version value="21.0.258.0" />
+	<Version value="21.0.270.0" />
 	<Website value="" />
 </xml>
Index: binary-improvements2/WebServer/WebServer.csproj
===================================================================
--- binary-improvements2/WebServer/WebServer.csproj	(revision 425)
+++ binary-improvements2/WebServer/WebServer.csproj	(revision 426)
@@ -101,4 +101,5 @@
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="src\ERequestMethod.cs" />
     <Compile Include="src\FileCache\AbstractCache.cs" />
     <Compile Include="src\FileCache\DirectAccess.cs" />
@@ -133,5 +134,5 @@
     <Compile Include="src\WebAPI\APIs\ServerInfo.cs" />
     <Compile Include="src\WebAPI\APIs\ServerStats.cs" />
-    <Compile Include="src\WebAPI\APIs\WebMods.cs" />
+    <Compile Include="src\WebAPI\APIs\Mods.cs" />
     <Compile Include="src\WebAPI\APIs\WebUiUpdates.cs" />
     <Compile Include="src\WebAPI\JsonCommons.cs" />
Index: binary-improvements2/WebServer/src/Commands/WebPermissionsCmd.cs
===================================================================
--- binary-improvements2/WebServer/src/Commands/WebPermissionsCmd.cs	(revision 425)
+++ binary-improvements2/WebServer/src/Commands/WebPermissionsCmd.cs	(revision 426)
@@ -15,10 +15,17 @@
 
 		protected override string getHelp () {
-			return "Set/get permission levels required to access a given web functionality. Default\n" +
-			       "level required for functions that are not explicitly specified is 0.\n" +
-			       "Usage:\n" +
-			       "   webpermission add <webfunction> <level>\n" +
-			       "   webpermission remove <webfunction>\n" +
-			       "   webpermission list [includedefaults]";
+			return @"
+				|Set/get permission levels required to access a given web functionality. Default
+			    |level required for functions that are not explicitly specified is 0.
+			    |Usage:
+				|   1. webpermission add <webfunction> <method> <level>
+			    |   2. webpermission remove <webfunction>
+			    |   3. webpermission list [includedefaults]
+				|1. Add a new override (or replace the existing one) for the given function. Method must be a HTTP method (like 'GET', 'POST')
+						supported by the function or the keyword 'global' for a per-API permission level. Use the permission level keyword
+						'inherit' to use the per-API permission level for the specified method instead of a custom one for just the single method.
+				|2. Removes any custom overrides for the specified function.
+				|3. List all permissions. Pass in 'true' for the includedefaults argument to also show functions that do not have a custom override defined.
+				".Unindent ();
 		}
 
@@ -40,21 +47,65 @@
 
 		private void ExecuteAdd (List<string> _params) {
-			if (_params.Count != 3) {
-				SdtdConsole.Instance.Output ($"Wrong number of arguments, expected 3, found {_params.Count}.");
+			if (_params.Count != 4) {
+				SdtdConsole.Instance.Output ($"Wrong number of arguments, expected 4, found {_params.Count}.");
 				return;
 			}
 
-			if (!AdminWebModules.Instance.IsKnownModule (_params [1])) {
-				SdtdConsole.Instance.Output ($"\"{_params [1]}\" is not a valid web function.");
+			string moduleString = _params [1];
+			string methodString = _params [2];
+			string permissionLevelString = _params [3];
+
+			ERequestMethod method = ERequestMethod.Count;
+			bool isGlobal = false;
+			bool isInherit = false;
+			int level;
+			
+			if (!AdminWebModules.Instance.IsKnownModule (moduleString)) {
+				SdtdConsole.Instance.Output ($"\"{moduleString}\" is not a valid web function.");
 				return;
 			}
 
-			if (!int.TryParse (_params [2], out int level)) {
-				SdtdConsole.Instance.Output ($"\"{_params [2]}\" is not a valid integer.");
-				return;
+			AdminWebModules.WebModule module = AdminWebModules.Instance.GetModule (moduleString);
+
+			if (methodString.EqualsCaseInsensitive ("global")) {
+				isGlobal = true;
+			} else {
+				if (!EnumUtils.TryParse (methodString, out method, true)) {
+					SdtdConsole.Instance.Output ($"\"{methodString}\" is neither a valid HTTP method nor the 'global' keyword.");
+					return;
+				}
+
+				if (module.LevelPerMethod == null || module.LevelPerMethod [(int)method] == AdminWebModules.MethodLevelNotSupported) {
+					SdtdConsole.Instance.Output ($"\"{methodString}\" is not a method supported by the \"{moduleString}\" function.");
+					return;
+				}
 			}
 
-			AdminWebModules.Instance.AddModule (_params [1], level);
-			SdtdConsole.Instance.Output ($"{_params [1]} added with permission level of {level}.");
+			if (permissionLevelString.EqualsCaseInsensitive ("inherit")) {
+				if (isGlobal) {
+					SdtdConsole.Instance.Output ($"Permission level can not use the 'inherit' keyword with the 'global' method keyword.");
+					return;
+				}
+				
+				isInherit = true;
+				level = AdminWebModules.MethodLevelInheritGlobal;
+			} else {
+				if (!int.TryParse (permissionLevelString, out level)) {
+					SdtdConsole.Instance.Output ($"\"{permissionLevelString}\" is neither a valid integer nor the 'inherit' keyword.");
+					return;
+				}
+			}
+
+			module.IsDefault = false;
+			if (isGlobal) {
+				module.LevelGlobal = level;
+			} else {
+				module.LevelPerMethod [(int)method] = level;
+			}
+			
+			AdminWebModules.Instance.AddModule (module);
+			
+			SdtdConsole.Instance.Output (
+				$"{moduleString}, method {methodString} added {(isInherit ? ", inheriting the APIs global permission level" : "with permission level " + level)}.");
 		}
 
@@ -78,9 +129,8 @@
 			
 			SdtdConsole.Instance.Output ("Defined web function permissions:");
-			SdtdConsole.Instance.Output ("  Level: Web function");
 			
 			List<AdminWebModules.WebModule> wmps = AdminWebModules.Instance.GetModules ();
-			for (int i = 0; i < wmps.Count; i++) {
-				AdminWebModules.WebModule wmp = wmps [i];
+			for (int iModule = 0; iModule < wmps.Count; iModule++) {
+				AdminWebModules.WebModule wmp = wmps [iModule];
 
 				if (!includeDefaults && wmp.IsDefault) {
@@ -88,12 +138,20 @@
 				}
 
-				if (wmp.IsDefault) {
-					if (wmp.PermissionLevel == int.MinValue) {
-						SdtdConsole.Instance.Output ($"    -  : {wmp.Name} (default permission)");
-					} else {
-						SdtdConsole.Instance.Output ($"  {wmp.PermissionLevel,5}: {wmp.Name} (default permission)");
+				SdtdConsole.Instance.Output ($"  {wmp.Name,-25}: {wmp.LevelGlobal,4}{(wmp.IsDefault ? " (default permissions)" : "")}");
+				if (wmp.LevelPerMethod != null) {
+					for (int iMethod = 0; iMethod < wmp.LevelPerMethod.Length; iMethod++) {
+						int methodLevel = wmp.LevelPerMethod [iMethod];
+						ERequestMethod method = (ERequestMethod)iMethod;
+						
+						if (methodLevel == AdminWebModules.MethodLevelNotSupported) {
+							continue;
+						}
+						
+						if (methodLevel == AdminWebModules.MethodLevelInheritGlobal) {
+							SdtdConsole.Instance.Output ($"  {method.ToStringCached (),25}: {wmp.LevelGlobal,4} (Using API level)");
+						} else {
+							SdtdConsole.Instance.Output ($"  {method.ToStringCached (),25}: {methodLevel,4}");
+						}
 					}
-				} else {
-					SdtdConsole.Instance.Output ($"  {wmp.PermissionLevel,5}: {wmp.Name}");
 				}
 			}
Index: binary-improvements2/WebServer/src/ERequestMethod.cs
===================================================================
--- binary-improvements2/WebServer/src/ERequestMethod.cs	(revision 426)
+++ binary-improvements2/WebServer/src/ERequestMethod.cs	(revision 426)
@@ -0,0 +1,14 @@
+namespace Webserver {
+	public enum ERequestMethod {
+		Other,
+		// ReSharper disable InconsistentNaming
+		GET,
+		POST,
+		PUT,
+		DELETE,
+		HEAD,
+		OPTIONS,
+		// ReSharper restore InconsistentNaming
+		Count
+	}
+}
Index: binary-improvements2/WebServer/src/Permissions/AdminWebModules.cs
===================================================================
--- binary-improvements2/WebServer/src/Permissions/AdminWebModules.cs	(revision 425)
+++ binary-improvements2/WebServer/src/Permissions/AdminWebModules.cs	(revision 426)
@@ -1,4 +1,5 @@
 using System.Collections.Generic;
 using System.Xml;
+using UnityEngine;
 
 namespace Webserver.Permissions {
@@ -40,10 +41,9 @@
 
 
-		public void AddModule (string _module, int _permissionLevel) {
-			WebModule p = new WebModule (_module, _permissionLevel, false);
+		public void AddModule (WebModule _module) {
 			lock (this) {
 				allModulesList.Clear ();
 				
-				modules [_module] = p;
+				modules [_module.Name] = _module;
 				Parent.Save ();
 			}
@@ -80,20 +80,60 @@
 
 #endregion
-
-		public readonly struct WebModule {
-			public readonly string Name;
-			public readonly int PermissionLevel;
-			public readonly bool IsDefault;
-
-			public WebModule (string _name, int _permissionLevel, bool _isDefault) {
+		
+		public struct WebModule {
+			public string Name;
+			public int LevelGlobal;
+			public int[] LevelPerMethod;
+			public bool IsDefault;
+
+			public WebModule (string _name, int _level, bool _isDefault = false) {
+				LevelPerMethod = null;
+				
 				Name = _name;
-				PermissionLevel = _permissionLevel;
+				LevelGlobal = _level;
+				IsDefault = _isDefault;
+			}
+
+			public WebModule (string _name, int _levelGlobal, int[] _levelPerMethod, bool _isDefault = false) {
+				if (_levelPerMethod == null || _levelPerMethod.Length != (int)ERequestMethod.Count) {
+					LevelPerMethod = createDefaultPerMethodArray ();
+
+					for (int i = 0; i < (int)ERequestMethod.Count; i++) {
+						if (_levelPerMethod != null && i < _levelPerMethod.Length) {
+							LevelPerMethod [i] = _levelPerMethod [i];
+						}
+					}
+				} else {
+					LevelPerMethod = _levelPerMethod;
+				}
+
+				Name = _name;
+				LevelGlobal = _levelGlobal;
 				IsDefault = _isDefault;
 			}
 			
 			public void ToXml (XmlElement _parent) {
-				_parent.AddXmlElement ("module")
-						.SetAttrib ("name", Name)
-						.SetAttrib ("permission_level", PermissionLevel.ToString ());
+				bool hasPerMethodLevels = LevelPerMethod != null;
+
+				XmlElement permissionElement = _parent.AddXmlElement ("module")
+					.SetAttrib ("name", Name)
+					.SetAttrib ("permission_level", LevelGlobal.ToString ());
+				
+				if (!hasPerMethodLevels) {
+					return;
+				}
+
+				for (int i = 0; i < LevelPerMethod.Length; i++) {
+					ERequestMethod method = (ERequestMethod)i;
+					int level = LevelPerMethod [i];
+					
+					if (level == MethodLevelNotSupported) {
+						continue;
+					}
+
+					permissionElement.AddXmlElement ("method")
+						.SetAttrib ("name", method.ToStringCached ())
+						.SetAttrib ("permission_level", level.ToString ());
+				}
 			}
 
@@ -117,8 +157,64 @@
 					return false;
 				}
-				
-				_result = new WebModule (name, permissionLevel, false);
+
+				int[] perMethodLevels = null;
+				
+				foreach (XmlNode child in _element.ChildNodes) {
+					if (child.NodeType != XmlNodeType.Element) {
+						continue;
+					}
+
+					XmlElement childElem = (XmlElement)child;
+					if (childElem.Name != "method") {
+						Log.Warning ($"[Web] [Perms] Ignoring module child element, invalid element name: {childElem.OuterXml}");
+						continue;
+					}
+
+					if (!childElem.TryGetAttribute ("name", out string methodName)) {
+						Log.Warning ($"[Web] [Perms] Ignoring module child element, missing 'name' attribute: {childElem.OuterXml}");
+						continue;
+					}
+
+					if (!EnumUtils.TryParse (methodName, out ERequestMethod method, true)) {
+						Log.Warning (
+							$"[Web] [Perms] Ignoring module child element, unknown method name in 'name' attribute: {childElem.OuterXml}");
+						continue;
+					}
+
+					if (method >= ERequestMethod.Count) {
+						Log.Warning (
+							$"[Web] [Perms] Ignoring module child element, invalid method name in 'name' attribute: {childElem.OuterXml}");
+						continue;
+					}
+					
+					if (!childElem.TryGetAttribute ("permission_level", out permissionLevelString)) {
+						Log.Warning ($"[Web] [Perms] Ignoring module child element, missing 'permission_level' attribute: {childElem.OuterXml}");
+						continue;
+					}
+
+					if (!int.TryParse (permissionLevelString, out int methodPermissionLevel)) {
+						Log.Warning (
+							$"[Web] [Perms] Ignoring module child element, invalid (non-numeric) value for 'permission_level' attribute: {childElem.OuterXml}");
+						continue;
+					}
+
+					perMethodLevels ??= createDefaultPerMethodArray ();
+					perMethodLevels [(int)method] = methodPermissionLevel;
+				}
+
+				_result = perMethodLevels != null ? new WebModule (name, permissionLevel, perMethodLevels) : new WebModule (name, permissionLevel);
+
 				return true;
 			}
+
+			private static int[] createDefaultPerMethodArray () {
+				int[] result = new int[(int)ERequestMethod.Count];
+
+				for (int i = 0; i < (int)ERequestMethod.Count; i++) {
+					result [i] = MethodLevelNotSupported;
+				}
+
+				return result;
+			}
 		}
 
@@ -127,4 +223,17 @@
 
 		/// <summary>
+		/// Use global (API based) permission level for the method
+		/// </summary>
+		public const int MethodLevelInheritGlobal = int.MinValue;
+		
+		/// <summary>
+		/// Method not supported
+		/// </summary>
+		public const int MethodLevelNotSupported = int.MinValue + 1;
+
+		public const int PermissionLevelUser = 1000;
+		public const int PermissionLevelGuest = 2000;
+
+		/// <summary>
 		/// Contains all registered modules and their default permission
 		/// </summary>
@@ -136,14 +245,14 @@
 		private readonly List<WebModule> allModulesList = new List<WebModule> ();
 
-		public void AddKnownModule (string _module, int _defaultPermission) {
-			if (string.IsNullOrEmpty (_module)) {
+		public void AddKnownModule (WebModule _module) {
+			if (string.IsNullOrEmpty (_module.Name)) {
 				return;
 			}
 
-			WebModule p = new WebModule (_module, _defaultPermission, true);
-
+			_module.IsDefault = true;
+			
 			lock (this) {
 				allModulesList.Clear ();
-				knownModules [_module] = p;
+				knownModules [_module.Name] = _module;
 			}
 		}
@@ -160,15 +269,10 @@
 
 		public bool ModuleAllowedWithLevel (string _module, int _level) {
-			WebModule permInfo = GetModule (_module)!.Value;
-			return permInfo.PermissionLevel >= _level;
-		}
-
-		public WebModule? GetModule (string _module, bool _returnDefaults = true) {
+			return GetModule (_module).LevelGlobal >= _level;
+		}
+
+		public WebModule GetModule (string _module) {
 			if (modules.TryGetValue (_module, out WebModule result)) {
 				return result;
-			}
-
-			if (!_returnDefaults) {
-				return null;
 			}
 
Index: binary-improvements2/WebServer/src/RequestContext.cs
===================================================================
--- binary-improvements2/WebServer/src/RequestContext.cs	(revision 425)
+++ binary-improvements2/WebServer/src/RequestContext.cs	(revision 426)
@@ -2,17 +2,4 @@
 
 namespace Webserver {
-	public enum ERequestMethod {
-		Other,
-		// ReSharper disable InconsistentNaming
-		GET,
-		PUT,
-		POST,
-		DELETE,
-		HEAD,
-		OPTIONS,
-		// ReSharper restore InconsistentNaming
-		Count
-	}
-	
 	public class RequestContext {
 		public string RequestPath;
Index: binary-improvements2/WebServer/src/UrlHandlers/AbsHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/AbsHandler.cs	(revision 425)
+++ binary-improvements2/WebServer/src/UrlHandlers/AbsHandler.cs	(revision 426)
@@ -12,5 +12,5 @@
 		protected AbsHandler (string _moduleName, int _defaultPermissionLevel = 0) {
 			moduleName = _moduleName;
-			AdminWebModules.Instance.AddKnownModule (_moduleName, _defaultPermissionLevel);
+			AdminWebModules.Instance.AddKnownModule (new AdminWebModules.WebModule(_moduleName, _defaultPermissionLevel, true));
 		}
 
Index: binary-improvements2/WebServer/src/UrlHandlers/SseHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/SseHandler.cs	(revision 425)
+++ binary-improvements2/WebServer/src/UrlHandlers/SseHandler.cs	(revision 426)
@@ -53,5 +53,5 @@
 		public void AddEvent (string _eventName, AbsEvent _eventInstance) {
 			events.Add (_eventName, _eventInstance);
-			AdminWebModules.Instance.AddKnownModule ($"webevent.{_eventName}", _eventInstance.DefaultPermissionLevel ());
+			AdminWebModules.Instance.AddKnownModule (new AdminWebModules.WebModule($"webevent.{_eventName}", _eventInstance.DefaultPermissionLevel (), true));
 		}
 
Index: binary-improvements2/WebServer/src/UrlHandlers/UserStatusHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/UserStatusHandler.cs	(revision 425)
+++ binary-improvements2/WebServer/src/UrlHandlers/UserStatusHandler.cs	(revision 426)
@@ -16,4 +16,14 @@
 		private static readonly byte[] jsonAllowedKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("allowed");
 
+		private static readonly byte[][] jsonMethodNameKeys;
+
+		static UserStatusHandler () {
+			jsonMethodNameKeys = new byte[(int)ERequestMethod.Count][];
+			for (int i = 0; i < jsonMethodNameKeys.Length; i++) {
+				ERequestMethod method = (ERequestMethod)i;
+				jsonMethodNameKeys [i] = JsonWriter.GetEncodedPropertyName (method.ToStringCached ());
+			}
+		}
+
 		public override void HandleRequest (RequestContext _context) {
 			WebUtils.PrepareEnvelopedResult (out JsonWriter writer);
@@ -32,8 +42,8 @@
 
 			List<AdminWebModules.WebModule> list = AdminWebModules.Instance.GetModules ();
-			for (int i = 0; i < list.Count; i++) {
-				AdminWebModules.WebModule perm = list [i];
+			for (int iModule = 0; iModule < list.Count; iModule++) {
+				AdminWebModules.WebModule perm = list [iModule];
 				
-				if (i > 0) {
+				if (iModule > 0) {
 					writer.WriteValueSeparator ();
 				}
@@ -43,5 +53,36 @@
 
 				writer.WriteRaw (jsonAllowedKey);
-				writer.WriteBoolean (perm.PermissionLevel >= _context.PermissionLevel);
+				
+				writer.WriteBeginObject ();
+
+				if (perm.LevelPerMethod == null) {
+					writer.WriteRaw (jsonMethodNameKeys [(int)ERequestMethod.GET]);
+					writer.WriteBoolean (perm.LevelGlobal >= _context.PermissionLevel);
+				} else {
+					bool first = true;
+					for (int iMethod = 0; iMethod < perm.LevelPerMethod.Length; iMethod++) {
+						int methodLevel = perm.LevelPerMethod [iMethod];
+						
+						if (methodLevel == AdminWebModules.MethodLevelNotSupported) {
+							continue;
+						}
+
+						if (methodLevel == AdminWebModules.MethodLevelInheritGlobal) {
+							methodLevel = perm.LevelGlobal;
+						}
+
+						if (!first) {
+							writer.WriteValueSeparator ();
+						}
+
+						first = false;
+						
+						writer.WriteRaw (jsonMethodNameKeys [iMethod]);
+						writer.WriteBoolean (methodLevel >= _context.PermissionLevel);
+					}
+				}
+
+				writer.WriteEndObject ();
+				
 
 				writer.WriteEndObject ();
Index: binary-improvements2/WebServer/src/Web.cs
===================================================================
--- binary-improvements2/WebServer/src/Web.cs	(revision 425)
+++ binary-improvements2/WebServer/src/Web.cs	(revision 426)
@@ -16,5 +16,4 @@
 		public static event Action<Web> ServerInitialized;
 		
-		private const int guestPermissionLevel = 2000;
 		private const string indexPageUrl = "/app";
 		
@@ -51,29 +50,7 @@
 				}
 
-				// TODO: Read from config
-				bool useCacheForStatic = StringParsers.ParseBool ("false");
-
-				string webfilesFolder = DetectWebserverFolder (_modInstancePath);
-
 				ConnectionHandler = new ConnectionHandler ();
 				
-				RegisterPathHandler ("/", new RewriteHandler ("/files/"));
-
-				// React virtual routing
-				RegisterPathHandler ("/app", new RewriteHandler ("/files/index.html", true));
-				
-				// Do mods relatively early as they should not be requested a lot, unlike the later registrations, especially for API and map tiles
-				RegisterWebMods (useCacheForStatic);
-
-				RegisterPathHandler ("/session/", new SessionHandler (ConnectionHandler));
-				RegisterPathHandler ("/userstatus", new UserStatusHandler ());
-				RegisterPathHandler ("/sse/", new SseHandler ());
-				RegisterPathHandler ("/files/", new StaticHandler (
-					webfilesFolder,
-					useCacheForStatic ? new SimpleCache () : new DirectAccess (),
-					false)
-				);
-				RegisterPathHandler ("/itemicons/", new ItemIconHandler (true));
-				RegisterPathHandler ("/api/", new ApiHandler ());
+				RegisterDefaultHandlers (_modInstancePath);
 				
 				// Allow other code to add their stuff
@@ -94,4 +71,30 @@
 		}
 
+		private void RegisterDefaultHandlers (string _modInstancePath) {
+			// TODO: Read from config
+			bool useCacheForStatic = StringParsers.ParseBool ("false");
+
+			string webfilesFolder = DetectWebserverFolder (_modInstancePath);
+
+			RegisterPathHandler ("/", new RewriteHandler ("/files/"));
+
+			// React virtual routing
+			RegisterPathHandler ("/app", new RewriteHandler ("/files/index.html", true));
+				
+			// Do mods relatively early as they should not be requested a lot, unlike the later registrations, especially for API and map tiles
+			RegisterWebMods (useCacheForStatic);
+
+			RegisterPathHandler ("/session/", new SessionHandler (ConnectionHandler));
+			RegisterPathHandler ("/userstatus", new UserStatusHandler ());
+			RegisterPathHandler ("/sse/", new SseHandler ());
+			RegisterPathHandler ("/files/", new StaticHandler (
+				webfilesFolder,
+				useCacheForStatic ? new SimpleCache () : new DirectAccess (),
+				false)
+			);
+			RegisterPathHandler ("/itemicons/", new ItemIconHandler (true));
+			RegisterPathHandler ("/api/", new ApiHandler ());
+		}
+
 		private static string DetectWebserverFolder (string _modInstancePath) {
 			string webserverFolder = $"{_modInstancePath}/webserver";
@@ -127,9 +130,4 @@
 			foreach (Mod mod in ModManager.GetLoadedMods ()) {
 				try {
-					string webModPath = $"{mod.Path}/WebMod";
-					if (!Directory.Exists (webModPath)) {
-						continue;
-					}
-
 					try {
 						WebMod webMod = new WebMod (this, mod, _useStaticCache);
@@ -298,5 +296,5 @@
 			if (reqRemoteEndPoint == null) {
 				Log.Warning ("[Web] No RemoteEndPoint on web request");
-				return guestPermissionLevel;
+				return AdminWebModules.PermissionLevelGuest;
 			}
 			
@@ -316,5 +314,5 @@
 			if (!_req.Headers.TryGetValue ("X-SDTD-API-TOKENNAME", out string apiTokenName) ||
 			    !_req.Headers.TryGetValue ("X-SDTD-API-SECRET", out string apiTokenSecret)) {
-				return guestPermissionLevel;
+				return AdminWebModules.PermissionLevelGuest;
 			}
 
@@ -326,5 +324,5 @@
 			Log.Warning ($"[Web] Invalid Admintoken used from {reqRemoteEndPoint}");
 
-			return guestPermissionLevel;
+			return AdminWebModules.PermissionLevelGuest;
 		}
 	}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Command.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Command.cs	(revision 425)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Command.cs	(revision 426)
@@ -3,4 +3,5 @@
 using JetBrains.Annotations;
 using Utf8Json;
+using Webserver.Permissions;
 
 namespace Webserver.WebAPI.APIs {
@@ -130,5 +131,5 @@
 		}
 
-		public override int DefaultPermissionLevel () => 2000;
+		public override int DefaultPermissionLevel () => AdminWebModules.PermissionLevelGuest;
 	}
 }
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Mods.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Mods.cs	(revision 426)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Mods.cs	(revision 426)
@@ -0,0 +1,91 @@
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.Permissions;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	public class Mods : AbsRestApi {
+		private readonly byte[] loadedWebMods;
+
+		public Mods (Web _parent) {
+			JsonWriter writer = new JsonWriter ();
+			writer.WriteBeginArray ();
+
+			for (int i = 0; i < _parent.webMods.Count; i++) {
+				WebMod webMod = _parent.webMods [i];
+
+				if (i > 0) {
+					writer.WriteValueSeparator ();
+				}
+				
+				writer.WriteBeginObject ();
+
+				writeModJson (ref writer, webMod);
+
+				if (webMod.ReactBundle != null || webMod.CssPath != null) {
+					writer.WriteValueSeparator ();
+
+					writer.WritePropertyName ("web");
+					writer.WriteBeginObject ();
+					
+					string webModReactBundle = webMod.ReactBundle;
+					if (webModReactBundle != null) {
+						writer.WritePropertyName ("bundle");
+						writer.WriteString (webModReactBundle);
+					}
+
+					string webModCssFile = webMod.CssPath;
+					if (webModCssFile != null) {
+						if (webModReactBundle != null) {
+							writer.WriteValueSeparator ();
+						}
+
+						writer.WritePropertyName ("css");
+						writer.WriteString (webModCssFile);
+					}
+					
+					writer.WriteEndObject ();
+				}
+
+				writer.WriteEndObject ();
+			}
+
+			writer.WriteEndArray ();
+
+			loadedWebMods = writer.ToUtf8ByteArray ();
+		}
+
+		protected override void HandleRestGet (RequestContext _context) {
+			PrepareEnvelopedResult (out JsonWriter writer);
+			writer.WriteRaw (loadedWebMods);
+			SendEnvelopedResult (_context, ref writer);
+		}
+
+		public override int DefaultPermissionLevel () => AdminWebModules.PermissionLevelGuest;
+
+		private void writeModJson (ref JsonWriter _writer, WebMod _webMod) {
+			_writer.WritePropertyName ("name");
+			_writer.WriteString (_webMod.ParentMod.Name);
+			
+			_writer.WriteValueSeparator ();
+			_writer.WritePropertyName ("displayName");
+			JsonCommons.WriteStringOrNull (ref _writer, _webMod.ParentMod.DisplayName);
+
+			_writer.WriteValueSeparator ();
+			_writer.WritePropertyName ("description");
+			JsonCommons.WriteStringOrNull (ref _writer, _webMod.ParentMod.Description);
+
+			_writer.WriteValueSeparator ();
+			_writer.WritePropertyName ("author");
+			JsonCommons.WriteStringOrNull (ref _writer, _webMod.ParentMod.Author);
+
+			_writer.WriteValueSeparator ();
+			_writer.WritePropertyName ("version");
+			JsonCommons.WriteStringOrNull (ref _writer, _webMod.ParentMod.VersionString);
+
+			_writer.WriteValueSeparator ();
+			_writer.WritePropertyName ("website");
+			JsonCommons.WriteStringOrNull (ref _writer, _webMod.ParentMod.Website);
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Player.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Player.cs	(revision 425)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Player.cs	(revision 426)
@@ -256,5 +256,5 @@
 		}
 
-		public override int DefaultPermissionLevel () => 2000;
+		public override int DefaultPermissionLevel () => AdminWebModules.PermissionLevelGuest;
 	}
 }
Index: binary-improvements2/WebServer/src/WebAPI/APIs/RegisterUser.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/RegisterUser.cs	(revision 425)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/RegisterUser.cs	(revision 426)
@@ -80,5 +80,6 @@
 			}
 			
-			// TODO: Check if username is already used!
+			// TODO: Check if username is already used by someone else!
+			// TODO: Remove existing username if player already had one!
 
 			AdminWebUsers.Instance.AddUser (username, password, regData.PlatformUserId, regData.CrossPlatformUserId);
@@ -95,5 +96,5 @@
 		}
 
-		public override int DefaultPermissionLevel () => 2000;
+		public override int DefaultPermissionLevel () => AdminWebModules.PermissionLevelGuest;
 	}
 }
Index: binary-improvements2/WebServer/src/WebAPI/APIs/ServerStats.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/ServerStats.cs	(revision 425)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/ServerStats.cs	(revision 426)
@@ -2,4 +2,5 @@
 using Utf8Json;
 using Webserver.LiveData;
+using Webserver.Permissions;
 
 namespace Webserver.WebAPI.APIs {
@@ -47,5 +48,5 @@
 		}
 
-		public override int DefaultPermissionLevel () => 2000;
+		public override int DefaultPermissionLevel () => AdminWebModules.PermissionLevelGuest;
 	}
 }
Index: binary-improvements2/WebServer/src/WebAPI/APIs/WebMods.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/WebMods.cs	(revision 425)
+++ 	(revision )
@@ -1,58 +1,0 @@
-using JetBrains.Annotations;
-using Utf8Json;
-
-namespace Webserver.WebAPI.APIs {
-	[UsedImplicitly]
-	public class WebMods : AbsRestApi {
-		private readonly byte[] loadedWebMods;
-
-		public WebMods (Web _parent) {
-			JsonWriter writer = new JsonWriter ();
-			writer.WriteBeginArray ();
-
-			for (int i = 0; i < _parent.webMods.Count; i++) {
-				WebMod webMod = _parent.webMods [i];
-				
-				if (i > 0) {
-					writer.WriteValueSeparator ();
-				}
-
-				writer.WriteBeginObject ();
-
-				writer.WriteString ("name");
-				writer.WriteNameSeparator ();
-				writer.WriteString (webMod.ParentMod.Name);
-
-				string webModReactBundle = webMod.ReactBundle;
-				if (webModReactBundle != null) {
-					writer.WriteValueSeparator ();
-					writer.WriteString ("bundle");
-					writer.WriteNameSeparator ();
-					writer.WriteString (webModReactBundle);
-				}
-
-				string webModCssFile = webMod.CssPath;
-				if (webModCssFile != null) {
-					writer.WriteValueSeparator ();
-					writer.WriteString ("css");
-					writer.WriteNameSeparator ();
-					writer.WriteString (webModCssFile);
-				}
-
-				writer.WriteEndObject ();
-			}
-
-			writer.WriteEndArray ();
-
-			loadedWebMods = writer.ToUtf8ByteArray ();
-		}
-
-		protected override void HandleRestGet (RequestContext _context) {
-			PrepareEnvelopedResult (out JsonWriter writer);
-			writer.WriteRaw (loadedWebMods);
-			SendEnvelopedResult (_context, ref writer);
-		}
-
-		public override int DefaultPermissionLevel () => 2000;
-	}
-}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/WebUiUpdates.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/WebUiUpdates.cs	(revision 425)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/WebUiUpdates.cs	(revision 426)
@@ -2,4 +2,5 @@
 using Utf8Json;
 using Webserver.LiveData;
+using Webserver.Permissions;
 
 namespace Webserver.WebAPI.APIs {
@@ -57,5 +58,5 @@
 		}
 
-		public override int DefaultPermissionLevel () => 2000;
+		public override int DefaultPermissionLevel () => AdminWebModules.PermissionLevelGuest;
 	}
 }
Index: binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs	(revision 425)
+++ binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs	(revision 426)
@@ -10,6 +10,4 @@
 		private static readonly UnityEngine.Profiling.CustomSampler jsonDeserializeSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Deserialize");
 
-		protected readonly string[] CachedPerMethodModuleNames = new string[(int)ERequestMethod.Count];
-
 		protected AbsRestApi (string _name = null) : this(null, _name) {
 		}
@@ -19,16 +17,6 @@
 
 		protected override void RegisterPermissions () {
-			base.RegisterPermissions ();
-			
-			for (int i = 0; i < (int)ERequestMethod.Count; i++) {
-				ERequestMethod method = (ERequestMethod)i;
-
-				if (method is not (ERequestMethod.GET or ERequestMethod.PUT or ERequestMethod.POST or ERequestMethod.DELETE)) {
-					continue;
-				}
-
-				CachedPerMethodModuleNames [i] = $"webapi.{Name}:{method.ToStringCached ()}";
-				AdminWebModules.Instance.AddKnownModule (CachedPerMethodModuleNames [i], DefaultMethodPermissionLevel (method));
-			}
+			AdminWebModules.Instance.AddKnownModule (new AdminWebModules.WebModule (CachedApiModuleName, DefaultPermissionLevel (),
+				DefaultMethodPermissionLevels (), true));
 		}
 
@@ -136,39 +124,31 @@
 
 		public override bool Authorized (RequestContext _context) {
-			return ActiveMethodPermissionLevel (_context.Method) >= _context.PermissionLevel;
+			AdminWebModules.WebModule module = AdminWebModules.Instance.GetModule (CachedApiModuleName);
+
+			if (module.LevelPerMethod != null) {
+				int perMethodLevel = module.LevelPerMethod [(int)_context.Method];
+				if (perMethodLevel == AdminWebModules.MethodLevelNotSupported) {
+					return false;
+				}
+
+				if (perMethodLevel != AdminWebModules.MethodLevelInheritGlobal) {
+					return perMethodLevel >= _context.PermissionLevel;
+				}
+			}
+
+			return module.LevelGlobal >= _context.PermissionLevel;
 		}
 
 		/// <summary>
-		/// Define default permission levels per HTTP method
+		/// Define default permission levels per HTTP method as an array of levels. See <see cref="ERequestMethod"/> for the order of entries.
 		/// </summary>
-		/// <param name="_method">HTTP method to return the default value for</param>
-		/// <returns>Default permission level for the given HTTP method. A value of int.MinValue means no per-method default, use per-API default</returns>
-		public virtual int DefaultMethodPermissionLevel (ERequestMethod _method) => int.MinValue;
-
-		public virtual int ActiveMethodPermissionLevel (ERequestMethod _method) {
-			string methodApiModuleName = CachedPerMethodModuleNames [(int)_method];
-
-			if (methodApiModuleName == null) {
-				return 0;
-			}
-
-			AdminWebModules.WebModule? overrideModule = AdminWebModules.Instance.GetModule (methodApiModuleName, false);
-			if (overrideModule.HasValue) {
-				return overrideModule.Value.PermissionLevel;
-			}
-
-			overrideModule = AdminWebModules.Instance.GetModule (CachedApiModuleName, false);
-			if (overrideModule.HasValue) {
-				return overrideModule.Value.PermissionLevel;
-			}
-
-			int defaultMethodPermissionLevel = DefaultMethodPermissionLevel (_method);
-			// ReSharper disable once ConvertIfStatementToReturnStatement
-			if (defaultMethodPermissionLevel != int.MinValue) {
-				return defaultMethodPermissionLevel;
-			}
-
-			return DefaultPermissionLevel ();
-		}
+		/// <returns>Default permission levels for supported methods. See <see cref="AdminWebModules.MethodLevelNotSupported"/> and <see cref="AdminWebModules.MethodLevelInheritGlobal"/>.</returns>
+		public virtual int[] DefaultMethodPermissionLevels () => new[] {
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelInheritGlobal
+		};
 
 #region Helpers
Index: binary-improvements2/WebServer/src/WebAPI/AbsWebAPI.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/AbsWebAPI.cs	(revision 425)
+++ binary-improvements2/WebServer/src/WebAPI/AbsWebAPI.cs	(revision 426)
@@ -19,5 +19,5 @@
 
 		protected virtual void RegisterPermissions () {
-			AdminWebModules.Instance.AddKnownModule ($"webapi.{Name}", DefaultPermissionLevel ());
+			AdminWebModules.Instance.AddKnownModule (new AdminWebModules.WebModule(CachedApiModuleName, DefaultPermissionLevel (), true));
 		}
 
@@ -25,5 +25,5 @@
 
 		public virtual bool Authorized (RequestContext _context) {
-			return AdminWebModules.Instance.ModuleAllowedWithLevel (CachedApiModuleName, _context.PermissionLevel);
+			return AdminWebModules.Instance.GetModule (CachedApiModuleName).LevelGlobal >= _context.PermissionLevel;
 		}
 
Index: binary-improvements2/WebServer/src/WebAPI/JsonCommons.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/JsonCommons.cs	(revision 425)
+++ binary-improvements2/WebServer/src/WebAPI/JsonCommons.cs	(revision 426)
@@ -60,4 +60,12 @@
 			_writer.WriteString (_dateTime.ToString ("o"));
 		}
+
+		public static void WriteStringOrNull (ref JsonWriter _writer, string _string) {
+			if (_string == null) {
+				_writer.WriteNull ();
+			} else {
+				_writer.WriteString (_string);
+			}
+		}
 	}
 }
Index: binary-improvements2/WebServer/src/WebMod.cs
===================================================================
--- binary-improvements2/WebServer/src/WebMod.cs	(revision 425)
+++ binary-improvements2/WebServer/src/WebMod.cs	(revision 426)
@@ -12,30 +12,27 @@
 		public readonly string ReactBundle; // Absolute web path to the React bundle if the mod has one, e.g. "/webmods/myMod/bundle.js"
 		public readonly string CssPath; // Absolute web path to a CSS if the mod has one, e.g. "/webmods/myMod/styling.css";
+		public readonly bool IsWebMod;
 
 		public WebMod (Web _parentWeb, Mod _parentMod, bool _useStaticCache) {
-			string folder = $"{_parentMod.Path}/WebMod";
-			if (!Directory.Exists (folder)) {
-				throw new InvalidDataException("No WebMod folder in mod");
-			}
-
-			string urlWebModBase = $"{modsBaseUrl}{_parentMod.FolderName}/";
-
-			ReactBundle = $"{folder}/{reactBundleName}";
-			ReactBundle = File.Exists (ReactBundle) ? $"{urlWebModBase}{reactBundleName}" : null;
-
-			CssPath = $"{folder}/{stylingFileName}";
-			CssPath = File.Exists (CssPath) ? $"{urlWebModBase}{stylingFileName}" : null;
-
-			if (ReactBundle == null && CssPath == null) {
-				throw new InvalidDataException($"WebMod folder has neither a {reactBundleName} nor a {stylingFileName}");
-			}
-
 			ParentMod = _parentMod;
 
-			_parentWeb.RegisterPathHandler (urlWebModBase, new StaticHandler (
-				folder,
-				_useStaticCache ? new SimpleCache () : new DirectAccess (),
-				false)
-			);
+			string folder = $"{_parentMod.Path}/WebMod";
+			IsWebMod = Directory.Exists (folder);
+
+			if (IsWebMod) {
+				string urlWebModBase = $"{modsBaseUrl}{_parentMod.FolderName}/";
+
+				ReactBundle = $"{folder}/{reactBundleName}";
+				ReactBundle = File.Exists (ReactBundle) ? $"{urlWebModBase}{reactBundleName}" : null;
+
+				CssPath = $"{folder}/{stylingFileName}";
+				CssPath = File.Exists (CssPath) ? $"{urlWebModBase}{stylingFileName}" : null;
+
+				_parentWeb.RegisterPathHandler (urlWebModBase, new StaticHandler (
+					folder,
+					_useStaticCache ? new SimpleCache () : new DirectAccess (),
+					false)
+				);
+			}
 		}
 	}
