diff --git a/OwnChar/Api/Endpoint/CharactersApi.cs b/OwnChar/Api/Endpoint/CharactersApi.cs new file mode 100644 index 0000000..526b20d --- /dev/null +++ b/OwnChar/Api/Endpoint/CharactersApi.cs @@ -0,0 +1,29 @@ +namespace OwnChar.Api.Endpoint; + +internal class CharactersApi(IOwnCharApiServer server) +{ + private IResult GetCharacter(long characterId) + { + + } + + private IResult CreateGroupCharacter(string name, long groupId) + { + + } + + private IResult CreateUserCharacter(string name, long userId) + { + + } + + private IResult UpdateCharacter(long characterId) + { + + } + + private IResult DeleteCharacter(long characterId) + { + + } +} diff --git a/OwnChar/Api/Endpoint/GroupsApi.cs b/OwnChar/Api/Endpoint/GroupsApi.cs new file mode 100644 index 0000000..b345bb1 --- /dev/null +++ b/OwnChar/Api/Endpoint/GroupsApi.cs @@ -0,0 +1,184 @@ +using OwnChar.Api.Packets.General; +using OwnChar.Api.Packets.Groups; +using OwnChar.Api.Updates; +using OwnChar.Data; +using OwnChar.Data.Model.Client; +using OwnChar.Data.Model.Server; +using OwnChar.Server.Extensions; +using Pilz.Net.Api; + +namespace OwnChar.Api.Endpoint; + +internal class GroupsApi(IOwnCharApiServer server) +{ + [MessageHandler("/group/get/byid")] + public ApiResult GetById(GetSinlgeObjectRequest request, ApiRequestInfo info) + { + if (!server.CheckLogin(info, UserType.Guest, out UserAccountDb? user)) + return ApiResult.Unauthorized(); + + if (server.Data?.Set().FirstOrDefault(n => n.Id == request.ObjectId && n.Members.Any(m => m.User != null && m.User.Id == user.Id)) is not GroupDb group) + return ApiResult.NotFound(); + + return ApiResult.Ok(new GetSingleObjectResponse(group.ToClient())); + } + + [MessageHandler("/group/get", RequiesAuth = true)] + public ApiResult Get(GetGroupsRequest request, ApiRequestInfo info) + { + if (!server.CheckLogin(info, UserType.Guest, out UserAccountDb? user)) + return ApiResult.Unauthorized(); + + IQueryable groups; + + if (request.UseProfileId && server.Data!.Set().FirstOrDefault(p => p.Id == request.ProfileId) is UserProfileDb profile) + groups = server.Data!.Set().Where(group => group.Members.Any(m => m.User != null && m.User.Id == profile.Id)); + else if (request.IncludeNonPublic && user.Is(UserType.Admin)) + groups = server.Data!.Set(); + else + groups = Array.Empty().AsQueryable(); // Currently not supported. + + return ApiResult.Ok(new GetGroupsResponse([.. groups.Select(g => g.ToClient())])); + } + + [MessageHandler("/group/create", RequiesAuth = true)] + public ApiResult Create(CreateGroupRequest request, ApiRequestInfo info) + { + if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user)) + return ApiResult.Unauthorized(); + + var group = new GroupDb(); + + if (!string.IsNullOrWhiteSpace(request.Name)) + group.Name = request.Name; + + group.Members.Add(new() + { + User = user.Profile, + Level = MemberLevel.Owner, + }); + + server.Data!.Update(group); + server.Data.SaveChanges(); + + return ApiResult.Ok(new CreateGroupResponse(group.ToClient())); + } + + [MessageHandler("/group/update", RequiesAuth = true)] + public ApiResult Update(UpdateRequest request, ApiRequestInfo info) + { + if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user) + || server.Data?.Set().FirstOrDefault(n => n.Id == request.Update.Id) is not GroupDb group + || !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Admin + || user.IsNot(UserType.Admin))) + return ApiResult.Unauthorized(); + + if (request.Update is not GroupUpdate update) + return ApiResult.NotFound(); + + group.Name = update.Name; + group.Fandom = update.Fandom; + + server.Data.Update(group); + server.Data.SaveChanges(); + + return ApiResult.Ok(); + } + + [MessageHandler("/group/delete", RequiesAuth = true)] + public ApiResult Delete(DeleteObjectRequest request, ApiRequestInfo info) + { + if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user) + || server.Data?.Set().FirstOrDefault(n => n.Id == request.ObjectId) is not GroupDb group + || !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Owner) + || user.IsNot(UserType.Admin)) + return ApiResult.Unauthorized(); + + server.Data.Remove(group); + server.Data.SaveChanges(); + + return ApiResult.Ok(); + } + + [MessageHandler("/group/members/get", RequiesAuth = true)] + public ApiResult GetMembers(GetGroupMembersRequest request, ApiRequestInfo info) + { + if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user) + || server.Data?.Set().FirstOrDefault(n => n.Id == request.GroupId) is not GroupDb group + || !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Member) + || user.IsNot(UserType.Admin)) + return ApiResult.Unauthorized(); + + var members = group.Members.Select(n => n.ToClient()); + return ApiResult.Ok(new GetGroupMembersResponse(members.ToList())); + } + + [MessageHandler("/group/members/add", RequiesAuth = true)] + public ApiResult AddMembers(GroupMemberAddRequest request, ApiRequestInfo info) + { + if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user) + || server.Data?.Set().FirstOrDefault(n => n.Id == request.GroupId) is not GroupDb group + || !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Admin) + || user.IsNot(UserType.Admin)) + return ApiResult.Unauthorized(); + + var addedMembers = new List(); + + foreach (var kvp in request.Members) + { + if (group.Members.FirstOrDefault(m => m.User != null && m.User.Id == kvp.Key) is not MemberEntryDb member + && server.Data.Set().FirstOrDefault(u => u.Id == kvp.Key) is UserProfileDb mu) + { + member = new() + { + User = mu, + Level = kvp.Value, + }; + server.Data.Update(member); + server.Data.Update(group); + server.Data.SaveChanges(); + } + } + + return ApiResult.Ok(new GroupMemberAddResponse(addedMembers.Select(m => m.ToClient()).ToList())); + } + + [MessageHandler("/group/members/update", RequiesAuth = true)] + public ApiResult UpdateMember(UpdateRequest request, ApiRequestInfo info) + { + if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user) + || server.Data?.Set().FirstOrDefault(n => n.Id == request.Update.Id) is not GroupDb group + || group.Members.FirstOrDefault(m => m.Id == request.Update.Id) is not MemberEntryDb member + || !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Admin + || user.IsNot(UserType.Admin))) + return ApiResult.Unauthorized(); + + if (request.Update is not MemberUpdate update) + return ApiResult.NotFound(); + + member.Level = update.Level; + + server.Data.Update(member); + server.Data.SaveChanges(); + + return ApiResult.Ok(); + } + + [MessageHandler("/group/members/remove", RequiesAuth = true)] + public ApiResult RemoveMembers(DeleteObjectRequest request, ApiRequestInfo info) + { + if (!server.CheckLogin(info, UserType.User, out UserAccountDb? user) + || server.Data?.Set().FirstOrDefault(m => m.Id == request.ObjectId) is not MemberEntryDb member + || server.Data?.Set().FirstOrDefault(n => n.Members.Contains(member)) is not GroupDb group + || !group.Members.Any(m => m.Id == user.Profile!.Id && m.Level >= MemberLevel.Admin + || user.IsNot(UserType.Admin))) + return ApiResult.Unauthorized(); + + group.Members.Remove(member); + server.Data.Remove(member); + server.Data.Update(group); + server.Data.SaveChanges(); + + return ApiResult.Ok(); + } +} diff --git a/OwnChar/Api/Endpoint/LoginApi.cs b/OwnChar/Api/Endpoint/LoginApi.cs new file mode 100644 index 0000000..ff40050 --- /dev/null +++ b/OwnChar/Api/Endpoint/LoginApi.cs @@ -0,0 +1,30 @@ +using OwnChar.Api.Packets; +using OwnChar.Api.Packets.General; +using OwnChar.Data.Model.Server; +using OwnChar.Server.Extensions; +using Pilz.Net.Api; + +namespace OwnChar.Api.Endpoint; + +internal class LoginApi(OwnCharApiServer server) +{ + [MessageHandler("/auth/login")] + public ApiResult Login(LoginRequest request, ApiRequestInfo info) + { + if (server.Data?.Set()?.FirstOrDefault(n => n.Username == request.Username && n.Password == request.Password) is UserAccountDb acc && acc.Profile != null) + { + if (info.IsAuthenticated) + server.Logout(info.AuthKey); + return ApiResult.Ok(new LoginResponse(acc.ToClient(), acc.Profile.ToClient(), server.Login(acc))); + } + return ApiResult.NotFound(); + } + + [MessageHandler("/auth/logout")] + public ApiResult Logout(ApiRequestInfo info) + { + if (info.IsAuthenticated) + server.Logout(info.AuthKey); + return ApiResult.Ok(); + } +} diff --git a/OwnChar/Api/Endpoint/UsersApi.cs b/OwnChar/Api/Endpoint/UsersApi.cs new file mode 100644 index 0000000..ebd9494 --- /dev/null +++ b/OwnChar/Api/Endpoint/UsersApi.cs @@ -0,0 +1,39 @@ +namespace OwnChar.Api.Endpoint; + +internal class UsersApi(IOwnCharApiServer server) +{ + private IResult GetUsers() + { + + } + + private IResult GetUser(long userId) + { + + } + + private IResult GetUserProfile(long userId) + { + + } + + private IResult CreateUser(string username, string password) + { + + } + + private IResult DeleteUser(long userId) + { + + } + + private IResult UpdateUserPassword(long userId, string username, string password) + { + + } + + private IResult UpdateUserProfile(long profileId, string displayName) + { + + } +} diff --git a/OwnChar/Api/IOwnCharApiServer.cs b/OwnChar/Api/IOwnCharApiServer.cs new file mode 100644 index 0000000..59c1511 --- /dev/null +++ b/OwnChar/Api/IOwnCharApiServer.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using OwnChar.Data.Model.Base; +using Pilz.Net.Api; +using System.Diagnostics.CodeAnalysis; + +namespace OwnChar.Api; + +public interface IOwnCharApiServer : IApiServer +{ + DbContext? Data { get; } + + [MemberNotNull(nameof(Data))] + void CheckLogin(string secret); + + [MemberNotNullWhen(true, nameof(Data))] + bool IsLoggedIn(string secret); + + [MemberNotNullWhen(true, nameof(Data))] + UserAccountBase? GetUser(string secret); +} diff --git a/OwnChar/Api/OwnCharApiServer.cs b/OwnChar/Api/OwnCharApiServer.cs new file mode 100644 index 0000000..2bcf050 --- /dev/null +++ b/OwnChar/Api/OwnCharApiServer.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore; +using OwnChar.Api; +using OwnChar.Api.Endpoint; +using OwnChar.Data.Model.Base; +using OwnChar.Server.Data; +using Pilz.Cryptography; +using Pilz.Extensions.Collections; +using Pilz.Net.Api; + +namespace OwnChar.Api; + +internal class OwnCharApiServer(string apiUrl, string dbServer, string dbDatabase, string dbUser, SecureString dbPassword) : ApiServer(apiUrl), IOwnCharApiServer +{ + private readonly Dictionary users = []; + + public DbContext? Data { get; private set; } + + public override void Start() + { + Log.Info("Prepairing server"); + + // Load database + Log.Debug("Loading database"); + Data = new DatabaseContext(dbServer, dbDatabase, dbUser, dbPassword); + + // Built-in endpoints + Log.Debug("Loading internal api endpoints"); + RegisterHandler(new LoginApi(this)); + RegisterHandler(new UsersApi(this)); + RegisterHandler(new GroupsApi(this)); + RegisterHandler(new CharactersApi(this)); + + // Run server + Log.Info("Starting webserver"); + base.Start(); + } + + protected override string? DecodeAuthKey(string authKey) + { + if (!string.IsNullOrWhiteSpace(authKey)) + return new SecureString(authKey, true).Value; + return authKey; + } + + protected override bool CheckAuthentication(string authKey) + { + return string.IsNullOrWhiteSpace(authKey) + || authKey.Split(":") is not string[] authKeyParts + || authKey.Length != 2 + || string.IsNullOrWhiteSpace(authKeyParts[0]) + || string.IsNullOrWhiteSpace(authKeyParts[1]) + || !IsLoggedIn(authKeyParts[1]); + } + + public string Login(UserAccountBase account) + { + var secret = new UniquieID(UniquieIDGenerationMode.GenerateOnInit).ID; + users.Add(secret, account); + Log.DebugFormat("Logged-in out user with secret {0}", secret); + return secret; + } + + public void Logout(string secret) + { + users.Remove(secret); + Log.DebugFormat("Logged-out user with secret {0}", secret); + } + + public bool IsLoggedIn(string secret) + { + Log.DebugFormat("Deleting user with secret {0}", secret); + return users.ContainsKey(secret); + } + + public void CheckLogin(string secret) + { + Log.DebugFormat("Checking login for user with secret {0}", secret); + if (!IsLoggedIn(secret)) + throw new UnauthorizedAccessException(); + } + + public UserAccountBase? GetUser(string secret) + { + Log.DebugFormat("Getting user with secret {0}", secret); + if (users.TryGetValue(secret, out UserAccountBase? value)) + return value; + return null; + } +} diff --git a/OwnChar/Data/Model.Server/CharacterDb.cs b/OwnChar/Data/Model.Server/CharacterDb.cs new file mode 100644 index 0000000..9af9449 --- /dev/null +++ b/OwnChar/Data/Model.Server/CharacterDb.cs @@ -0,0 +1,10 @@ +using OwnChar.Data.Model.Base; + +namespace OwnChar.Data.Model.Server; + +public class CharacterDb : CharacterBase +{ + public UserProfileDb? Owner { get; set; } + public List PropCats { get; } = []; + public List Props { get; } = []; +} diff --git a/OwnChar/Data/Model.Server/DatabaseContext.cs b/OwnChar/Data/Model.Server/DatabaseContext.cs new file mode 100644 index 0000000..fd42d9f --- /dev/null +++ b/OwnChar/Data/Model.Server/DatabaseContext.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Pilz.Cryptography; + +namespace OwnChar.Server.Data; + +public class DatabaseContext(string dbHost, string dbDatabase, string dbUser, SecureString dbPassword) : DbContext +{ + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + } +} diff --git a/OwnChar/Data/Model.Server/GroupDb.cs b/OwnChar/Data/Model.Server/GroupDb.cs new file mode 100644 index 0000000..9d9d49f --- /dev/null +++ b/OwnChar/Data/Model.Server/GroupDb.cs @@ -0,0 +1,9 @@ +using OwnChar.Data.Model.Base; + +namespace OwnChar.Data.Model.Server; + +public class GroupDb : GroupBase +{ + public List Characters { get; } = []; + public List Members { get; } = []; +} diff --git a/OwnChar/Data/Model.Server/MemberEntryDb.cs b/OwnChar/Data/Model.Server/MemberEntryDb.cs new file mode 100644 index 0000000..0daa0a2 --- /dev/null +++ b/OwnChar/Data/Model.Server/MemberEntryDb.cs @@ -0,0 +1,8 @@ +using OwnChar.Data.Model.Base; + +namespace OwnChar.Data.Model.Server; + +public class MemberEntryDb() : MemberEntryBase +{ + public virtual UserProfileDb? User { get; set; } +} diff --git a/OwnChar/Data/Model.Server/PropertyCategoryDb.cs b/OwnChar/Data/Model.Server/PropertyCategoryDb.cs new file mode 100644 index 0000000..3e239a3 --- /dev/null +++ b/OwnChar/Data/Model.Server/PropertyCategoryDb.cs @@ -0,0 +1,7 @@ +using OwnChar.Data.Model.Base; + +namespace OwnChar.Data.Model.Server; + +public class PropertyCategoryDb : PropertyCategoryBase +{ +} diff --git a/OwnChar/Data/Model.Server/PropertyDb.cs b/OwnChar/Data/Model.Server/PropertyDb.cs new file mode 100644 index 0000000..ac766e1 --- /dev/null +++ b/OwnChar/Data/Model.Server/PropertyDb.cs @@ -0,0 +1,7 @@ +using OwnChar.Data.Model.Base; + +namespace OwnChar.Data.Model.Server; + +public class PropertyDb : PropertyBase +{ +} diff --git a/OwnChar/Data/Model.Server/UserAccountDb.cs b/OwnChar/Data/Model.Server/UserAccountDb.cs new file mode 100644 index 0000000..f107bbf --- /dev/null +++ b/OwnChar/Data/Model.Server/UserAccountDb.cs @@ -0,0 +1,9 @@ +using OwnChar.Data.Model.Base; + +namespace OwnChar.Data.Model.Server; + +public class UserAccountDb : UserAccountBase +{ + public virtual string? Password { get; set; } + public UserProfileDb? Profile { get; set; } +} diff --git a/OwnChar/Data/Model.Server/UserProfileDb.cs b/OwnChar/Data/Model.Server/UserProfileDb.cs new file mode 100644 index 0000000..46c5ae5 --- /dev/null +++ b/OwnChar/Data/Model.Server/UserProfileDb.cs @@ -0,0 +1,8 @@ +using OwnChar.Data.Model.Base; + +namespace OwnChar.Data.Model.Server; + +public class UserProfileDb : UserProfileBase +{ + public List Characters { get; } = []; +} diff --git a/OwnChar/Extensions/ModelExtensions.cs b/OwnChar/Extensions/ModelExtensions.cs new file mode 100644 index 0000000..8e9908e --- /dev/null +++ b/OwnChar/Extensions/ModelExtensions.cs @@ -0,0 +1,48 @@ +using OwnChar.Data.Model.Client; +using OwnChar.Data.Model.Server; + +namespace OwnChar.Server.Extensions; + +public static class ModelExtensions +{ + public static Group ToClient(this GroupDb @this) + { + return new() + { + Id = @this.Id, + Name = @this.Name, + Fandom = @this.Fandom, + IsInternal = @this.IsInternal, + }; + } + + public static UserAccount ToClient(this UserAccountDb @this) + { + return new() + { + Id = @this.Id, + Email = @this.Email, + Type = @this.Type, + Username = @this.Username, + }; + } + + public static UserProfile ToClient(this UserProfileDb @this) + { + return new() + { + Id = @this.Id, + Name = @this.Name, + }; + } + + public static MemberEntry ToClient(this MemberEntryDb @this) + { + return new() + { + Id = @this.Id, + UserProfileId = @this.User!.Id, + Level = @this.Level, + }; + } +} diff --git a/OwnChar/Extensions/OwnCharApiServerExtensions.cs b/OwnChar/Extensions/OwnCharApiServerExtensions.cs new file mode 100644 index 0000000..93f8864 --- /dev/null +++ b/OwnChar/Extensions/OwnCharApiServerExtensions.cs @@ -0,0 +1,47 @@ +using OwnChar.Api; +using OwnChar.Data; +using OwnChar.Data.Model.Base; +using OwnChar.Data.Model.Server; +using Pilz.Net.Api; +using System.Diagnostics.CodeAnalysis; + +namespace OwnChar.Server.Extensions; + +public static class OwnCharApiServerExtensions +{ + public static bool CheckLogin(this IOwnCharApiServer server, ApiRequestInfo request, UserType userType) + { + if (server.Data is null + || !request.IsAuthenticated + || request.AuthKey.Split(":") is not string[] authKey + || authKey.ElementAtOrDefault(0) is not string username + || authKey.ElementAtOrDefault(1) is not string secret + || string.IsNullOrWhiteSpace(username) + || string.IsNullOrWhiteSpace(secret) + || !server.IsLoggedIn(secret) + || server.GetUser(secret) is not UserAccountBase usr + || usr.Type < userType) + return false; + return true; + } + + public static bool CheckLogin(this IOwnCharApiServer server, ApiRequestInfo request, UserType userType, [NotNullWhen(true)] out UserAccountDb? user) + { + if (server.Data is null + || !request.IsAuthenticated + || request.AuthKey.Split(":") is not string[] authKey + || authKey.ElementAtOrDefault(0) is not string username + || authKey.ElementAtOrDefault(1) is not string secret + || string.IsNullOrWhiteSpace(username) + || string.IsNullOrWhiteSpace(secret) + || !server.IsLoggedIn(secret) + || server.GetUser(secret) is not UserAccountDb usr + || usr.Type < userType) + { + user = null; + return false; + } + user = usr; + return true; + } +}