split plan and role, fix lots of small issues, move registration logic to code

This commit is contained in:
Jetsparrow 2022-05-13 18:52:52 +03:00
parent 95e8d12751
commit 4cdeb8a7c7
17 changed files with 283 additions and 140 deletions

View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "6.0.5",
"commands": [
"dotnet-ef"
]
}
}
}

View File

@ -45,6 +45,7 @@ public class User
public byte[] PasswordSalt { get; set; } public byte[] PasswordSalt { get; set; }
public int HashType { get; set; } public int HashType { get; set; }
public uint PlanId { get; set; } public uint PlanId { get; set; }
public uint RoleId { get; set; }
public string Allow { get; set; } public string Allow { get; set; }
@ -58,6 +59,7 @@ public class UserInvite
public uint UserInviteId { get; set; } public uint UserInviteId { get; set; }
public string InviteCode { get; set; } public string InviteCode { get; set; }
public uint PlanId { get; set; } public uint PlanId { get; set; }
public uint RoleId { get; set; }
public uint RedeemedBy { get; set; } public uint RedeemedBy { get; set; }
} }
@ -74,5 +76,11 @@ public class Plan
public string Name { get; set; } public string Name { get; set; }
public int MaxTopics { get; set; } public int MaxTopics { get; set; }
public double TimeoutMultiplier { get; set; } public double TimeoutMultiplier { get; set; }
}
public class Role
{
public uint RoleId { get; set; }
public string Name { get; set; }
public string Allow { get; set; } public string Allow { get; set; }
} }

View File

@ -76,9 +76,9 @@ public class HeartbeatController : ControllerBase
if (Timeouts.IsTimedOut(t.TopicId)) if (Timeouts.IsTimedOut(t.TopicId))
return StatusCode(StatusCodes.Status429TooManyRequests); return StatusCode(StatusCodes.Status429TooManyRequests);
var affected = await Db.ReportHeartbeat(t.TopicId, heart, args.ExpiryTimeout); var wasBeating = await Db.ReportHeartbeat(t.TopicId, heart, args.ExpiryTimeout);
if (affected == 0) if (wasBeating == 0)
await Herald.BroadcastMessageRaw(t.TopicId, $"!{t.Description}!:\nHeart \"{heart}\" has started beating at {DateTime.UtcNow:O}"); await Herald.BroadcastMessageRaw(t.TopicId, $"!{t.Description}!:\nHeart \"{heart}\" has started beating at {DateTime.UtcNow:O}");
Timeouts.ApplyCost(t.TopicId, Config.HeartbeatCost); Timeouts.ApplyCost(t.TopicId, Config.HeartbeatCost);

View File

@ -33,10 +33,12 @@ public class AdminToolsController : Controller
{ {
var invites = await Db.GetInvites(); var invites = await Db.GetInvites();
var plans = await Db.GetPlans(); var plans = await Db.GetPlans();
var roles = await Db.GetRoles();
return View(new ViewInvitesModel return View(new ViewInvitesModel
{ {
Invites = invites.ToArray(), Invites = invites.ToArray(),
Plans = plans.ToDictionary(p => p.PlanId) Plans = plans.ToDictionary(p => p.PlanId),
Roles = roles.ToDictionary(r => r.RoleId)
}); });
} }
@ -44,16 +46,20 @@ public class AdminToolsController : Controller
{ {
[BindProperty(Name = "planId"), BindRequired] [BindProperty(Name = "planId"), BindRequired]
public uint PlanId { get; set; } public uint PlanId { get; set; }
[BindProperty(Name = "roleId"), BindRequired]
public uint RoleId { get; set; }
} }
[HttpPost, Route("ui/admintools/invites/create")] [HttpPost, Route("ui/admintools/invites/create")]
public async Task<IActionResult> CreateInvite(CreateInviteRequest req) public async Task<IActionResult> CreateInvite(CreateInviteRequest req)
{ {
await Db.CreateUserInvite(req.PlanId, TokenHelper.GetToken(AuthCfg.InviteCodeLength)); await Db.CreateUserInvite(req.PlanId, req.RoleId, TokenHelper.GetToken(AuthCfg.InviteCodeLength));
return RedirectToAction(nameof(ViewInvites)); return RedirectToAction(nameof(ViewInvites));
} }
} }
public class ViewInvitesModel public class ViewInvitesModel
{ {
public UserInvite[] Invites { get; set; } public UserInvite[] Invites { get; set; }
public Dictionary<uint, Plan> Plans { get; set; } public Dictionary<uint, Plan> Plans { get; set; }
public Dictionary<uint, Role> Roles { get; set; }
} }

View File

@ -73,7 +73,7 @@ public class LoginController : Controller
} }
catch (ArgumentException) catch (ArgumentException)
{ {
return RedirectToAction("Dashboard", "Dashboard"); return RedirectToAction("Index", "Dashboard");
} }
} }

View File

@ -1,25 +1,26 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace JetHerald.Controllers.Ui; namespace JetHerald.Controllers.Ui;
[Route("ui")]
public class MainController : Controller public class MainController : Controller
{ {
[Route("/error/")] [Route("error")]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error() public IActionResult Error()
{ {
return View(); return View();
} }
[Route("/403")] [Route("403")]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error403() public IActionResult Error403()
{ {
return View(); return View();
} }
[Route("/404")] [Route("404")]
public IActionResult Error404() public IActionResult Error404()
{ {
return View(); return View();
} }
[Route("/400")] [Route("400")]
public IActionResult Error400() public IActionResult Error400()
{ {
return View(); return View();

View File

@ -27,7 +27,7 @@ public class RegistrationController : Controller
} }
public RegistrationController( public RegistrationController(
ILogger<LoginController> log, ILogger<RegistrationController> log,
Db db, Db db,
IOptionsSnapshot<AuthConfig> authConfig) IOptionsSnapshot<AuthConfig> authConfig)
{ {
@ -83,14 +83,17 @@ public class RegistrationController : Controller
} }
var user = new User() var user = new User()
{ {
RoleId = invite.RoleId,
PlanId = invite.PlanId, PlanId = invite.PlanId,
Login = req.Login, Login = req.Login,
Name = req.Name, Name = req.Name,
HashType = Cfg.HashType,
PasswordSalt = RandomNumberGenerator.GetBytes(64) PasswordSalt = RandomNumberGenerator.GetBytes(64)
}; };
user.PasswordHash = AuthUtils.GetHashFor(req.Password, user.PasswordSalt, Cfg.HashType); user.PasswordHash = AuthUtils.GetHashFor(req.Password, user.PasswordSalt, user.HashType);
var newUser = await Db.RegisterUserFromInvite(user, invite.UserInviteId); user = await Db.RegisterUser(user);
var userIdentity = AuthUtils.CreateIdentity(newUser.UserId, newUser.Login, newUser.Name, newUser.Allow); await Db.RedeemInvite(invite.UserInviteId, user.UserId);
var userIdentity = AuthUtils.CreateIdentity(user.UserId, user.Login, user.Name, user.Allow);
var principal = new ClaimsPrincipal(userIdentity); var principal = new ClaimsPrincipal(userIdentity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal); await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
@ -100,7 +103,7 @@ public class RegistrationController : Controller
} }
catch (ArgumentException) catch (ArgumentException)
{ {
return RedirectToAction("News", "Issue"); return RedirectToAction("Index", "Dashboard");
} }
} }
} }

View File

@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper.Transaction" Version="2.0.35.2" />
<PackageReference Include="DSharpPlus" Version="4.1.0" /> <PackageReference Include="DSharpPlus" Version="4.1.0" />
<PackageReference Include="DSharpPlus.CommandsNext" Version="4.1.0" /> <PackageReference Include="DSharpPlus.CommandsNext" Version="4.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.1" />
@ -15,10 +15,4 @@
<PackageReference Include="Telegram.Bot.Extensions.Polling" Version="1.0.2" /> <PackageReference Include="Telegram.Bot.Extensions.Polling" Version="1.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project> </Project>

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true">
<!-- enable asp.net core layout renderers -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<targets>
<target xsi:type="File" name="allfile" fileName="herald.log"
layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
<target name="logconsole" xsi:type="Console"
layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=Message,StackTrace,Data:maxInnerExceptionLevel=10}"/>
</targets>
<!-- rules to map from logger name to target -->
<rules>
<!--All logs, including from Microsoft-->
<logger name="*" minlevel="Trace" writeTo="allfile" />
<logger name="*" minlevel="Trace" writeTo="logconsole" />
<!--Skip non-critical Microsoft logs and so log only own logs-->
<logger name="Microsoft.*" maxlevel="Info" final="true" />
</rules>
</nlog>

View File

@ -35,7 +35,6 @@ try
builder.WebHost.ConfigureAppConfiguration((hostingContext, config) => builder.WebHost.ConfigureAppConfiguration((hostingContext, config) =>
{ {
//config.SetBasePath(Directory.GetCurrentDirectory());
config.AddIniFile("secrets.ini", config.AddIniFile("secrets.ini",
optional: true, reloadOnChange: true); optional: true, reloadOnChange: true);
config.AddIniFile($"secrets.{hostingContext.HostingEnvironment.EnvironmentName}.ini", config.AddIniFile($"secrets.{hostingContext.HostingEnvironment.EnvironmentName}.ini",
@ -78,10 +77,10 @@ try
options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = true; options.Cookie.HttpOnly = true;
// options.SessionStore = new JetHeraldTicketStore(); // options.SessionStore = new JetHeraldTicketStore();
options.LoginPath = "/login"; options.LoginPath = "/ui/login";
options.LogoutPath = "/logout"; options.LogoutPath = "/ui/logout";
options.ReturnUrlParameter = "redirect"; options.ReturnUrlParameter = "redirect";
options.AccessDeniedPath = "/403"; options.AccessDeniedPath = "/ui/403";
options.ClaimsIssuer = "JetHerald"; options.ClaimsIssuer = "JetHerald";
}); });
services.AddPermissions(); services.AddPermissions();
@ -96,6 +95,9 @@ try
var adminUser = await db.GetUser("admin"); var adminUser = await db.GetUser("admin");
if (adminUser == null) if (adminUser == null)
{ {
var adminRole = (await db.GetRoles()).First(r => r.Name == "admin");
var unlimitedPlan = (await db.GetPlans()).First(p => p.Name == "unlimited");
var authCfg = app.Services.GetService<IOptions<AuthConfig>>().Value; var authCfg = app.Services.GetService<IOptions<AuthConfig>>().Value;
var password = Convert.ToBase64String(RandomNumberGenerator.GetBytes(48)); var password = Convert.ToBase64String(RandomNumberGenerator.GetBytes(48));
adminUser = new JetHerald.Contracts.User() adminUser = new JetHerald.Contracts.User()
@ -103,10 +105,12 @@ try
Login = "admin", Login = "admin",
Name = "Administrator", Name = "Administrator",
PasswordSalt = RandomNumberGenerator.GetBytes(64), PasswordSalt = RandomNumberGenerator.GetBytes(64),
HashType = authCfg.HashType HashType = authCfg.HashType,
RoleId = adminRole.RoleId,
PlanId = unlimitedPlan.PlanId
}; };
adminUser.PasswordHash = AuthUtils.GetHashFor(password, adminUser.PasswordSalt, adminUser.HashType); adminUser.PasswordHash = AuthUtils.GetHashFor(password, adminUser.PasswordSalt, adminUser.HashType);
var newUser = await db.RegisterUser(adminUser, "admin"); var newUser = await db.RegisterUser(adminUser);
log.Warn($"Created administrative account {adminUser.Login}:{password}. Be sure to save these credentials somewhere!"); log.Warn($"Created administrative account {adminUser.Login}:{password}. Be sure to save these credentials somewhere!");
} }
} }

View File

@ -19,6 +19,11 @@ public class Db
return await c.QueryAsync<Plan>("SELECT * FROM plan"); return await c.QueryAsync<Plan>("SELECT * FROM plan");
} }
public async Task<IEnumerable<Role>> GetRoles()
{
using var c = GetConnection();
return await c.QueryAsync<Role>("SELECT * FROM role");
}
public async Task<IEnumerable<UserInvite>> GetInvites() public async Task<IEnumerable<UserInvite>> GetInvites()
{ {
using var c = GetConnection(); using var c = GetConnection();
@ -33,12 +38,15 @@ public class Db
new { userId }); new { userId });
} }
public async Task CreateUserInvite(uint planId, string inviteCode) public async Task CreateUserInvite(uint planId, uint roleId, string inviteCode)
{ {
using var c = GetConnection(); using var c = GetConnection();
await c.ExecuteAsync( await c.ExecuteAsync(@"
"INSERT INTO userinvite (PlanId, InviteCode) VALUES (@planId, @inviteCode)", INSERT INTO userinvite
new { planId, inviteCode }); ( PlanId, RoleId, InviteCode)
VALUES
(@planId, @roleId, @inviteCode)",
new { planId, roleId, inviteCode });
} }
public async Task<Topic> GetTopic(string name) public async Task<Topic> GetTopic(string name)
@ -79,11 +87,12 @@ public class Db
public async Task<User> GetUser(string login) public async Task<User> GetUser(string login)
{ {
using var c = GetConnection(); using var c = GetConnection();
return await c.QuerySingleOrDefaultAsync<User>( return await c.QuerySingleOrDefaultAsync<User>(@"
" SELECT u.*, p.* " + SELECT u.*, up.*, ur.*
" FROM user u " + FROM user u
" LEFT JOIN plan p ON p.PlanId = u.PlanId " + JOIN plan up ON u.PlanId = up.PlanId
" WHERE u.Login = @login", JOIN role ur ON u.RoleId = ur.RoleId
WHERE u.Login = @login;",
new { login }); new { login });
} }
@ -135,24 +144,24 @@ public class Db
return topic; return topic;
} }
public async Task<User> RegisterUserFromInvite(User user, uint inviteId) public async Task<User> RegisterUser(User user)
{ {
using var c = GetConnection(); using var c = GetConnection();
uint userId = await c.QuerySingleOrDefaultAsync<uint>( uint userId = await c.QuerySingleOrDefaultAsync<uint>(@"
"CALL register_user_from_invite(@inviteId, @Login, @Name, @PasswordHash, @PasswordSalt, @HashType);", INSERT INTO user
new { inviteId, user.Login, user.Name, user.PasswordHash, user.PasswordSalt, user.HashType }); ( Login, Name, PasswordHash, PasswordSalt, HashType, PlanId, RoleId)
VALUES
(@Login, @Name, @PasswordHash, @PasswordSalt, @HashType, @PlanId, @RoleId);",
param:user);
return await GetUser(user.Login); return await GetUser(user.Login);
} }
public async Task<User> RegisterUser(User user, string plan) public async Task RedeemInvite(uint inviteId, uint userId)
{ {
using var c = GetConnection(); using var c = GetConnection();
uint userId = await c.QuerySingleOrDefaultAsync<uint>( await c.ExecuteAsync(
"CALL register_user(@plan, @Login, @Name, @PasswordHash, @PasswordSalt, @HashType);", @"UPDATE userinvite SET RedeemedBy = @userId WHERE UserInviteId = @inviteId",
new { plan, user.Login, user.Name, user.PasswordHash, user.PasswordSalt, user.HashType }); new { inviteId, userId });
return await GetUser(user.Login);
} }
public async Task<UserInvite> GetInviteByCode(string inviteCode) public async Task<UserInvite> GetInviteByCode(string inviteCode)
@ -274,6 +283,6 @@ public class Db
Config = cfg; Config = cfg;
} }
IOptionsMonitor<ConnectionStrings> Config { get; } IOptionsMonitor<ConnectionStrings> Config { get; }
MySqlConnection GetConnection() => new(Config.CurrentValue.DefaultConnection); public MySqlConnection GetConnection() => new(Config.CurrentValue.DefaultConnection);
} }

View File

@ -1,6 +1,6 @@
<ul class="issues-list"> <ul class="issues-list">
<li><a asp-action="CreateProject">Create new project code</a></li>
<li><a asp-action="ViewUsers">View all registered users</a></li> <li><a asp-action="ViewUsers">View all registered users</a></li>
<li><a asp-action="ViewInvites">View unredeemed invites</a></li> <!-- <li><a asp-action="ViewInvites">View unredeemed invites</a></li>
<li><a asp-action="ViewRoles">View all roles</a></li> <li><a asp-action="ViewRoles">View all roles</a></li>
-->
</ul> </ul>

View File

@ -1,13 +1,21 @@
@model ViewInvitesModel @model ViewInvitesModel
<form asp-controller="AdminTools" asp-action="CreateInvite" method="POST" enctype="application/x-www-form-urlencoded"> <form asp-controller="AdminTools" asp-action="CreateInvite" method="POST" enctype="application/x-www-form-urlencoded">
<label for="planselector">Plan: </label> <label for="roleselector">Role:</label>
<select name="roleId" required id="roleselector">
@foreach (var role in Model.Roles.Values)
{
<option value="@role.RoleId">@role.Name</option>
}
</select>
<label for="planselector">Plan:</label>
<select name="planId" required id="planselector"> <select name="planId" required id="planselector">
@foreach (var plan in Model.Plans.Values) @foreach (var plan in Model.Plans.Values)
{ {
<option value="@plan.PlanId">@plan.Name</option> <option value="@plan.PlanId">@plan.Name</option>
} }
</select> </select>
<input type="submit" value="Create invite" class="h2 submitpost" style="margin-top:10px; width:initial"> <input type="submit" value="Create invite" class="h2 submitpost" style="margin-top:10px; width:initial">
</form> </form>
<br> <br>
@ -17,7 +25,7 @@
@foreach (var invite in Model.Invites) @foreach (var invite in Model.Invites)
{ {
<li> <li>
<span style="font-family:monospace">@invite.InviteCode.Substring(0, 8)... (@Model.Plans[invite.PlanId].Name)</span> <span style="font-family:monospace">@invite.InviteCode.Substring(0, 8)... r:(@Model.Roles[invite.RoleId].Name) p:(@Model.Plans[invite.PlanId].Name)</span>
<form asp-controller="Admin" asp-action="DeleteInvite" asp-route-inviteId="@invite.UserInviteId" method="POST" style="display:inline"> <form asp-controller="Admin" asp-action="DeleteInvite" asp-route-inviteId="@invite.UserInviteId" method="POST" style="display:inline">
<input type="submit" value="❌" style="display:inline; color:red;" class="buttonlink"> <input type="submit" value="❌" style="display:inline; color:red;" class="buttonlink">
</form> </form>

View File

@ -5,5 +5,19 @@
"System": "Information", "System": "Information",
"Microsoft": "Information" "Microsoft": "Information"
} }
},
"Telegram": {
"UseProxy": "true"
},
"Timeout": {
"DebtLimitSeconds": 600,
"HeartbeatCost": 60,
"ReportCost": 60
},
"PathBase": "/",
"AuthConfig": {
"InviteCodeLength": 128,
"TicketIdLengthBytes": 64,
"HashType": 1
} }
} }

View File

@ -1,21 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"Telegram": {
"UseProxy": "true"
},
"Timeout": {
"DebtLimitSeconds": 600,
"HeartbeatCost": 60,
"ReportCost": 60
},
"PathBase": "/",
"AuthConfig": {
"InviteCodeLength" : 128,
"TicketIdLengthBytes": 64,
"HashType": 1
}
}

View File

@ -12,7 +12,7 @@
<target xsi:type="File" name="allfile" fileName="herald.log" <target xsi:type="File" name="allfile" fileName="herald.log"
layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" /> layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
<target name="logconsole" xsi:type="Console" <target name="logconsole" xsi:type="Console"
layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=Message,StackTrace,Data:maxInnerExceptionLevel=10}"/> layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=Message,StackTrace,Data:maxInnerExceptionLevel=10}"/>
</targets> </targets>

View File

@ -1,6 +1,4 @@
CREATE DATABASE IF NOT EXISTS `herald` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; -- MySQL dump 10.13 Distrib 8.0.26, for Linux (x86_64)
USE `herald`;
-- MySQL dump 10.13 Distrib 8.0.28, for Win64 (x86_64)
-- --
-- Host: localhost Database: herald -- Host: localhost Database: herald
-- ------------------------------------------------------ -- ------------------------------------------------------
@ -9,7 +7,7 @@ USE `herald`;
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8 */; /*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */; /*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
@ -28,17 +26,19 @@ CREATE TABLE `heart` (
`HeartId` bigint unsigned NOT NULL AUTO_INCREMENT, `HeartId` bigint unsigned NOT NULL AUTO_INCREMENT,
`TopicId` int unsigned NOT NULL, `TopicId` int unsigned NOT NULL,
`Heart` varchar(32) NOT NULL, `Heart` varchar(32) NOT NULL,
`Status` enum('beating','stopped','deleted') NOT NULL DEFAULT 'beating', `Name` varchar(45) GENERATED ALWAYS AS (`Heart`) VIRTUAL,
`Status` varchar(16) NOT NULL DEFAULT 'beating',
`LastBeatTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `LastBeatTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ExpiryTs` timestamp NOT NULL, `ExpiryTs` timestamp NOT NULL,
`CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`UpdateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `UpdateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`HeartId`), PRIMARY KEY (`HeartId`),
UNIQUE KEY `idx_TopicId_Heart_UNIQUE` (`TopicId`,`Heart`), UNIQUE KEY `idx_TopicId_Heart_UNIQUE` (`TopicId`,`Heart`),
CONSTRAINT `fk_topic` FOREIGN KEY (`TopicId`) REFERENCES `topic` (`TopicId`) CONSTRAINT `fk_heart_TopicId` FOREIGN KEY (`TopicId`) REFERENCES `topic` (`TopicId`)
) ENGINE=InnoDB AUTO_INCREMENT=115140 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ) ENGINE=InnoDB AUTO_INCREMENT=227102 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Table structure for table `heartevent` -- Table structure for table `heartevent`
-- --
@ -50,15 +50,68 @@ CREATE TABLE `heartevent` (
`HeartEventId` bigint unsigned NOT NULL AUTO_INCREMENT, `HeartEventId` bigint unsigned NOT NULL AUTO_INCREMENT,
`TopicId` int unsigned NOT NULL, `TopicId` int unsigned NOT NULL,
`Heart` varchar(32) NOT NULL, `Heart` varchar(32) NOT NULL,
`Status` enum('created','processing','reported') NOT NULL DEFAULT 'created', `Status` varchar(16) NOT NULL DEFAULT 'created',
`Event` enum('created','started','stopped','deleted') NOT NULL COMMENT 'ENUM(''created'',''started'',''stopped'',''deleted'')', `Event` varchar(16) NOT NULL COMMENT 'ENUM(''created'',''started'',''stopped'',''deleted'')',
`CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`HeartEventId`), PRIMARY KEY (`HeartEventId`),
KEY `idx_topic` (`TopicId`,`CreateTs`), KEY `idx_topic` (`TopicId`,`CreateTs`),
KEY `idx_reported` (`Status`) KEY `idx_reported` (`Status`)
) ENGINE=InnoDB AUTO_INCREMENT=201 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ) ENGINE=InnoDB AUTO_INCREMENT=372 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `plan`
--
DROP TABLE IF EXISTS `plan`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `plan` (
`PlanId` int unsigned NOT NULL AUTO_INCREMENT,
`Name` varchar(45) NOT NULL,
`MaxTopics` int unsigned NOT NULL,
`TimeoutMultiplier` double NOT NULL,
PRIMARY KEY (`PlanId`),
UNIQUE KEY `Name_UNIQUE` (`Name`)
) ENGINE=InnoDB AUTO_INCREMENT=4099 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `plan`
--
LOCK TABLES `plan` WRITE;
/*!40000 ALTER TABLE `plan` DISABLE KEYS */;
INSERT INTO `plan` VALUES (4096,'none',0,1),(4097,'unlimited',1000000,0);
/*!40000 ALTER TABLE `plan` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `role`
--
DROP TABLE IF EXISTS `role`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role` (
`RoleId` int unsigned NOT NULL AUTO_INCREMENT,
`Name` varchar(45) NOT NULL,
`Allow` text NOT NULL,
PRIMARY KEY (`RoleId`)
) ENGINE=InnoDB AUTO_INCREMENT=4099 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `role`
--
LOCK TABLES `role` WRITE;
/*!40000 ALTER TABLE `role` DISABLE KEYS */;
INSERT INTO `role` VALUES (4096,'anonymous','login;register;'),(4097,'admin','**;'),(4098,'client','dashboard;profile;topic;');
/*!40000 ALTER TABLE `role` ENABLE KEYS */;
UNLOCK TABLES;
-- --
-- Table structure for table `topic` -- Table structure for table `topic`
-- --
@ -69,11 +122,11 @@ DROP TABLE IF EXISTS `topic`;
CREATE TABLE `topic` ( CREATE TABLE `topic` (
`TopicId` int unsigned NOT NULL AUTO_INCREMENT, `TopicId` int unsigned NOT NULL AUTO_INCREMENT,
`Creator` varchar(45) NOT NULL, `Creator` varchar(45) NOT NULL,
`CreatorId` int unsigned NOT NULL,
`Name` varchar(45) NOT NULL, `Name` varchar(45) NOT NULL,
`Description` varchar(45) NOT NULL, `Description` varchar(45) NOT NULL,
`ReadToken` char(32) NOT NULL, `ReadToken` char(32) NOT NULL,
`WriteToken` char(32) NOT NULL, `WriteToken` char(32) NOT NULL,
`AdminToken` char(32) NOT NULL,
`CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`UpdateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `UpdateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`TopicId`), PRIMARY KEY (`TopicId`),
@ -95,14 +148,87 @@ CREATE TABLE `topic_sub` (
`Sub` varchar(45) NOT NULL, `Sub` varchar(45) NOT NULL,
`CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`TopicId`,`Sub`), PRIMARY KEY (`TopicId`,`Sub`),
CONSTRAINT `FK_TOPICID` FOREIGN KEY (`TopicId`) REFERENCES `topic` (`TopicId`) ON DELETE CASCADE CONSTRAINT `fk_topic_sub_TopicId` FOREIGN KEY (`TopicId`) REFERENCES `topic` (`TopicId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Dumping events for database 'herald' -- Table structure for table `user`
-- --
DROP TABLE IF EXISTS `user`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user` (
`UserId` int unsigned NOT NULL AUTO_INCREMENT,
`PlanId` int unsigned NOT NULL,
`RoleId` int unsigned NOT NULL,
`Login` varchar(45) NOT NULL,
`Name` varchar(255) NOT NULL,
`PasswordHash` varbinary(255) NOT NULL,
`PasswordSalt` varbinary(255) NOT NULL,
`HashType` tinyint NOT NULL DEFAULT '1',
`CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`UpdateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`UserId`),
KEY `idx_user_PlanId` (`PlanId`),
KEY `fk_user_RoleId_idx` (`RoleId`),
CONSTRAINT `fk_user_PlanId` FOREIGN KEY (`PlanId`) REFERENCES `plan` (`PlanId`),
CONSTRAINT `fk_user_RoleId` FOREIGN KEY (`RoleId`) REFERENCES `role` (`RoleId`)
) ENGINE=InnoDB AUTO_INCREMENT=4111 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `user`
--
LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;
INSERT INTO `user` VALUES (4096,4096,4096,'Anonymous','Anonymous',0xBADC0D35,0xBADC0D35,0,NOW(),NOW());
/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `userinvite`
--
DROP TABLE IF EXISTS `userinvite`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `userinvite` (
`UserInviteId` int NOT NULL AUTO_INCREMENT,
`PlanId` int unsigned NOT NULL,
`RoleId` int unsigned NOT NULL DEFAULT '4096',
`InviteCode` varchar(255) NOT NULL,
`RedeemedBy` int unsigned DEFAULT NULL,
`CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`UpdateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`UserInviteId`),
UNIQUE KEY `InviteCode_UNIQUE` (`InviteCode`),
KEY `fk_PlanId_idx` (`PlanId`),
KEY `fk_userinvite_RoleId_idx` (`RoleId`),
CONSTRAINT `fk_userinvite_PlanId` FOREIGN KEY (`PlanId`) REFERENCES `plan` (`PlanId`),
CONSTRAINT `fk_userinvite_RoleId` FOREIGN KEY (`RoleId`) REFERENCES `role` (`RoleId`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `usersession`
--
DROP TABLE IF EXISTS `usersession`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `usersession` (
`SessionId` varchar(255) NOT NULL,
`SessionData` blob NOT NULL,
`ExpiryTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`CreateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`UpdateTs` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`SessionId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Dumping routines for database 'herald' -- Dumping routines for database 'herald'
-- --
@ -116,33 +242,31 @@ CREATE TABLE `topic_sub` (
/*!50003 SET @saved_sql_mode = @@sql_mode */ ; /*!50003 SET @saved_sql_mode = @@sql_mode */ ;
/*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' */ ; /*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' */ ;
DELIMITER ;; DELIMITER ;;
CREATE PROCEDURE `process_hearts`() CREATE DEFINER=`herald`@`%` PROCEDURE `process_hearts`()
BEGIN BEGIN
START TRANSACTION;
SET @ts = NOW();
START TRANSACTION; INSERT INTO heartevent
(TopicId, Heart, Event)
SELECT
TopicID, Heart, 'stopped'
FROM heart
WHERE ExpiryTs < @ts AND Status = 'beating';
SET @ts = NOW(); UPDATE heart set Status = 'stopped' WHERE ExpiryTs < @ts AND Status = 'beating' AND HeartId > 0;
SET @id = (SELECT HeartEventId FROM heartevent WHERE `Status` = 'created' ORDER BY HeartEventId DESC LIMIT 1);
INSERT INTO heartevent
(TopicId, Heart, `Event`)
SELECT
TopicId, Heart, 'stopped'
FROM heart
WHERE ExpiryTs < @ts AND `Status` = 'beating';
UPDATE heart SET `Status` = 'stopped' WHERE ExpiryTs < @ts AND `Status` = 'beating' AND HeartId > 0;
SET @id = (SELECT HeartEventId FROM heartevent WHERE `Status` = 'created' ORDER BY HeartEventId DESC LIMIT 1);
UPDATE heartevent SET `Status` = 'processing' WHERE HeartEventId = @id;
SELECT ha.*, t.`Description`
FROM heartevent ha
JOIN topic t USING (TopicId)
WHERE HeartEventId = @id;
COMMIT; UPDATE heartevent SET `Status` = 'processing' WHERE HeartEventId = @id;
SELECT ha.*, t.`Description`
FROM heartevent ha
JOIN topic t USING (TopicId)
WHERE HeartEventId = @id;
COMMIT;
END ;; END ;;
DELIMITER ; DELIMITER ;
/*!50003 SET sql_mode = @saved_sql_mode */ ; /*!50003 SET sql_mode = @saved_sql_mode */ ;
/*!50003 SET character_set_client = @saved_cs_client */ ; /*!50003 SET character_set_client = @saved_cs_client */ ;
/*!50003 SET character_set_results = @saved_cs_results */ ; /*!50003 SET character_set_results = @saved_cs_results */ ;
@ -157,19 +281,28 @@ DELIMITER ;
/*!50003 SET @saved_sql_mode = @@sql_mode */ ; /*!50003 SET @saved_sql_mode = @@sql_mode */ ;
/*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' */ ; /*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' */ ;
DELIMITER ;; DELIMITER ;;
CREATE PROCEDURE `report_heartbeat`(_topicId INT UNSIGNED, _heart VARCHAR(45), _timeoutSeconds INTEGER UNSIGNED) CREATE DEFINER=`herald`@`%` PROCEDURE `report_heartbeat`(_topicId INT UNSIGNED, _heart VARCHAR(45), _timeoutSeconds INTEGER UNSIGNED)
BEGIN BEGIN
DECLARE beating INTEGER DEFAULT 0; DECLARE beating INTEGER DEFAULT 0;
START TRANSACTION; START TRANSACTION;
SET beating = EXISTS(SELECT * FROM heart WHERE TopicId = _topicId AND Heart = _heart AND `Status` = `beating`); SET beating = EXISTS(SELECT * FROM heart WHERE TopicId = _topicId AND Heart = _heart AND `Status` = 'beating');
INSERT INTO heart INSERT INTO heart
(TopicId, Heart, `Status`, ExpiryTs) (TopicId, Heart, `Status`, ExpiryTs)
VALUES VALUES
(_topicId, _heart, 'beating', CURRENT_TIMESTAMP() + INTERVAL _timeoutSeconds SECOND) (_topicId, _heart, 'beating', CURRENT_TIMESTAMP() + INTERVAL _timeoutSeconds SECOND)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
`Status` = 'beating', `Status` = 'beating',
LastBeatTs = CURRENT_TIMESTAMP(),
ExpiryTs = CURRENT_TIMESTAMP() + INTERVAL _timeoutSeconds SECOND; ExpiryTs = CURRENT_TIMESTAMP() + INTERVAL _timeoutSeconds SECOND;
SELECT beating;
IF NOT beating THEN
INSERT INTO heartevent
(TopicId, Heart, Event)
SELECT
_topicId, _heart, 'started';
END IF;
SELECT beating;
COMMIT; COMMIT;
END ;; END ;;
DELIMITER ; DELIMITER ;
@ -187,4 +320,4 @@ DELIMITER ;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2022-01-28 17:03:44 -- Dump completed on 2022-05-13 18:44:48