Adds local System Name persistence (trimmed, 1-40) in system_kv, an
injectable sync module with conflict/invalid revert and a pending-sync
flag, a background SystemNameSync worker (retries on boot and on a timer),
and api_client support for PUT /licenses/{key}/system-name plus the
additive systemName field/systemNameStatus on license validation. The
license service sends the stored name and applies the status. Covers
validation, pending retry, and conflict revert with a faked HTTP layer.
108 lines
2.8 KiB
C++
108 lines
2.8 KiB
C++
#include "validation.h"
|
|
|
|
#include <cctype>
|
|
|
|
namespace radiantwave::validation {
|
|
|
|
bool isValidEmail(const std::string& email) {
|
|
if (email.size() < 3 || email.size() > 254) {
|
|
return false;
|
|
}
|
|
|
|
const size_t at = email.find('@');
|
|
if (at == std::string::npos || at == 0 || at == email.size() - 1) {
|
|
return false;
|
|
}
|
|
if (email.find('@', at + 1) != std::string::npos) {
|
|
return false;
|
|
}
|
|
|
|
for (unsigned char c : email) {
|
|
if (std::isspace(c)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const std::string local = email.substr(0, at);
|
|
const std::string domain = email.substr(at + 1);
|
|
|
|
// Local part: alphanumerics plus ._%+- , but never leading/trailing dots.
|
|
if (local.front() == '.' || local.back() == '.') {
|
|
return false;
|
|
}
|
|
for (unsigned char c : local) {
|
|
if (!(std::isalnum(c) || c == '.' || c == '_' || c == '%' || c == '+' || c == '-')) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Domain part: dot-separated labels of alphanumerics/hyphens.
|
|
if (domain.front() == '.' || domain.back() == '.') {
|
|
return false;
|
|
}
|
|
int labels = 0;
|
|
size_t start = 0;
|
|
for (size_t i = 0; i <= domain.size(); ++i) {
|
|
if (i == domain.size() || domain[i] == '.') {
|
|
const size_t len = i - start;
|
|
if (len == 0) {
|
|
return false;
|
|
}
|
|
for (size_t j = start; j < i; ++j) {
|
|
const unsigned char c = static_cast<unsigned char>(domain[j]);
|
|
if (!(std::isalnum(c) || c == '-')) {
|
|
return false;
|
|
}
|
|
}
|
|
++labels;
|
|
start = i + 1;
|
|
}
|
|
}
|
|
return labels >= 2;
|
|
}
|
|
|
|
std::string sanitizeLicenseKey(const std::string& input) {
|
|
std::string out;
|
|
out.reserve(16);
|
|
for (unsigned char c : input) {
|
|
if (out.size() >= 16) {
|
|
break;
|
|
}
|
|
if (std::isalnum(c)) {
|
|
out += static_cast<char>(std::toupper(c));
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string formatLicenseKey(const std::string& sanitized) {
|
|
std::string out;
|
|
out.reserve(19);
|
|
for (size_t i = 0; i < sanitized.size(); ++i) {
|
|
if (i > 0 && i % 4 == 0) {
|
|
out += '-';
|
|
}
|
|
out += sanitized[i];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string trim(const std::string& input) {
|
|
size_t start = 0;
|
|
size_t end = input.size();
|
|
while (start < end && std::isspace(static_cast<unsigned char>(input[start]))) {
|
|
++start;
|
|
}
|
|
while (end > start && std::isspace(static_cast<unsigned char>(input[end - 1]))) {
|
|
--end;
|
|
}
|
|
return input.substr(start, end - start);
|
|
}
|
|
|
|
bool isValidSystemName(const std::string& name) {
|
|
const std::string trimmed = trim(name);
|
|
return !trimmed.empty() && trimmed.size() <= SYSTEM_NAME_MAX_LENGTH;
|
|
}
|
|
|
|
} // namespace radiantwave::validation
|