1using System.Security.Cryptography;
2using System.Security.Claims;
4using Dreamine.Identity;
6using Microsoft.Data.Sqlite;
89 string email,
string displayName,
string password)
91 email = email.Trim().ToLowerInvariant();
92 displayName = displayName.Trim();
94 if (
string.IsNullOrWhiteSpace(email))
return (
false,
"이메일을 입력하세요.",
null);
95 if (
string.IsNullOrWhiteSpace(displayName))
return (
false,
"이름을 입력하세요.",
null);
96 if (
string.IsNullOrWhiteSpace(password) || password.Length < 8)
97 return (
false,
"비밀번호는 8자 이상이어야 합니다.",
null);
100 return (
false,
"이미 사용 중인 이메일입니다.",
null);
102 var hash = DreaminePasswordHasher.HashPassword(password);
107 Id = Guid.NewGuid().ToString(
"N"),
109 DisplayName = displayName,
113 CreatedAt = DateTimeOffset.UtcNow
116 using var conn =
_db.Open();
117 using var cmd = conn.CreateCommand();
118 cmd.CommandText =
"""
119 INSERT INTO users (id, email, display_name, public_slug, password_hash, password_salt, created_at)
120 VALUES ($id, $email, $dn, $slug, $hash, $salt, $ca)
122 cmd.Parameters.AddWithValue(
"$id", user.Id);
123 cmd.Parameters.AddWithValue(
"$email", user.Email);
124 cmd.Parameters.AddWithValue(
"$dn", user.DisplayName);
125 cmd.Parameters.AddWithValue(
"$slug", user.PublicSlug);
126 cmd.Parameters.AddWithValue(
"$hash", user.PasswordHash);
127 cmd.Parameters.AddWithValue(
"$salt", user.PasswordSalt);
128 cmd.Parameters.AddWithValue(
"$ca", user.CreatedAt.ToString(
"O"));
129 await cmd.ExecuteNonQueryAsync();
131 return (
true,
"", user);
166 public async Task<(
bool Ok,
string Error,
VmsUser? User)>
LoginAsync(
string email,
string password)
168 email = email.Trim().ToLowerInvariant();
170 if (user is
null)
return (
false,
"이메일 또는 비밀번호가 올바르지 않습니다.",
null);
172 if (
string.IsNullOrWhiteSpace(user.PasswordHash))
173 return (
false,
"에이전트 연결 비밀번호가 아직 설정되지 않았습니다.",
null);
175 var verification = DreaminePasswordHasher.VerifyPassword(password, user.PasswordHash, out var upgradedHash);
176 if (verification is PasswordHashVerificationResult.Failed &&
177 !
string.IsNullOrWhiteSpace(user.PasswordSalt) &&
180 verification = PasswordHashVerificationResult.SuccessRehashNeeded;
181 upgradedHash = DreaminePasswordHasher.HashPassword(password);
184 if (verification is PasswordHashVerificationResult.Failed)
185 return (
false,
"이메일 또는 비밀번호가 올바르지 않습니다.",
null);
187 if (verification is PasswordHashVerificationResult.SuccessRehashNeeded && upgradedHash is not
null)
193 return (
true,
"", user);
222 if (principal.Identity?.IsAuthenticated !=
true)
227 var identityId = principal.FindFirstValue(DreamineIdentityExtensions.UserIdClaimType);
228 if (
string.IsNullOrWhiteSpace(identityId))
233 var userId = $
"oauth-{identityId}";
235 if (userById is not
null)
240 var
provider = principal.FindFirstValue(DreamineIdentityExtensions.ProviderClaimType) ??
"OAuth";
241 var email =
NormalizeEmail(principal.FindFirstValue(ClaimTypes.Email));
242 var displayName = principal.FindFirstValue(ClaimTypes.Name) ?? email;
243 if (
string.IsNullOrWhiteSpace(displayName))
245 displayName = $
"{provider} 사용자";
248 if (!
string.IsNullOrWhiteSpace(email))
251 if (userByEmail is not
null)
265 DisplayName = displayName.Trim(),
269 CreatedAt = DateTimeOffset.UtcNow
272 using var conn =
_db.Open();
273 using var cmd = conn.CreateCommand();
274 cmd.CommandText =
"""
275 INSERT INTO users (id, email, display_name, public_slug, password_hash, password_salt, created_at)
276 VALUES ($id, $email, $dn, $slug, $hash, $salt, $ca)
278 cmd.Parameters.AddWithValue(
"$id", user.Id);
279 cmd.Parameters.AddWithValue(
"$email", user.Email);
280 cmd.Parameters.AddWithValue(
"$dn", user.DisplayName);
281 cmd.Parameters.AddWithValue(
"$slug", user.PublicSlug);
282 cmd.Parameters.AddWithValue(
"$hash", user.PasswordHash);
283 cmd.Parameters.AddWithValue(
"$salt", user.PasswordSalt);
284 cmd.Parameters.AddWithValue(
"$ca", user.CreatedAt.ToString(
"O"));
285 await cmd.ExecuteNonQueryAsync();
333 string confirmPassword)
335 if (
string.IsNullOrWhiteSpace(password) || password.Length < 8)
337 return (
false,
"에이전트 연결 비밀번호는 8자 이상이어야 합니다.",
null);
340 if (!
string.Equals(password, confirmPassword, StringComparison.Ordinal))
342 return (
false,
"비밀번호 확인이 일치하지 않습니다.",
null);
348 return (
false,
"사용자를 찾을 수 없습니다.",
null);
351 var hash = DreaminePasswordHasher.HashPassword(password);
384 using var conn =
_db.Open();
385 using var cmd = conn.CreateCommand();
386 cmd.CommandText =
"SELECT * FROM users WHERE id = $id LIMIT 1";
387 cmd.Parameters.AddWithValue(
"$id",
id);
409 using var conn =
_db.Open();
410 using var cmd = conn.CreateCommand();
411 cmd.CommandText =
"""
414 INNER JOIN cameras c ON c.tenant_id = u.id
415 WHERE c.is_public = 1 AND c.enabled = 1
416 ORDER BY u.display_name
418 using var reader = cmd.ExecuteReader();
419 var list =
new List<VmsUser>();
420 while (reader.Read())
451 using var conn =
_db.Open();
452 using var cmd = conn.CreateCommand();
453 cmd.CommandText =
"SELECT * FROM users WHERE public_slug = $slug LIMIT 1";
454 cmd.Parameters.AddWithValue(
"$slug", slug.ToLowerInvariant());
484 using var conn =
_db.Open();
485 using var cmd = conn.CreateCommand();
486 cmd.CommandText =
"SELECT * FROM users WHERE email = $email LIMIT 1";
487 cmd.Parameters.AddWithValue(
"$email", email);
525 using var conn =
_db.Open();
526 using var cmd = conn.CreateCommand();
527 cmd.CommandText =
"UPDATE users SET live_layout = $layout WHERE id = $id";
528 cmd.Parameters.AddWithValue(
"$layout", layout);
529 cmd.Parameters.AddWithValue(
"$id", userId);
530 await cmd.ExecuteNonQueryAsync();
581 public async Task
UpdateOgAsync(
string userId,
string ogTitle,
string ogDescription,
string ogImage)
583 using var conn =
_db.Open();
584 using var cmd = conn.CreateCommand();
585 cmd.CommandText =
"""
586 UPDATE users SET og_title = $t, og_description = $d, og_image = $i WHERE id = $id
588 cmd.Parameters.AddWithValue(
"$t", ogTitle.Trim());
589 cmd.Parameters.AddWithValue(
"$d", ogDescription.Trim());
590 cmd.Parameters.AddWithValue(
"$i", ogImage.Trim());
591 cmd.Parameters.AddWithValue(
"$id", userId);
592 await cmd.ExecuteNonQueryAsync();
621 using var reader = await cmd.ExecuteReaderAsync();
622 if (!await reader.ReadAsync())
return null;
652 #pragma warning disable CS1587
679 string Col(
string name) => r.IsDBNull(r.GetOrdinal(name)) ?
"" : r.GetString(r.GetOrdinal(name));
680 #pragma warning restore CS1587
681 var layoutOrd = r.GetOrdinal(
"live_layout");
684 Id = r.GetString(r.GetOrdinal(
"id")),
685 Email = r.GetString(r.GetOrdinal(
"email")),
686 DisplayName = r.GetString(r.GetOrdinal(
"display_name")),
687 PublicSlug = r.GetString(r.GetOrdinal(
"public_slug")),
688 PasswordHash = r.GetString(r.GetOrdinal(
"password_hash")),
689 PasswordSalt = r.GetString(r.GetOrdinal(
"password_salt")),
690 CreatedAt = DateTimeOffset.Parse(r.GetString(r.GetOrdinal(
"created_at"))),
691 LiveLayout = r.IsDBNull(layoutOrd) ?
"auto" : r.GetString(layoutOrd),
692 OgTitle = Col(
"og_title"),
693 OgDescription = Col(
"og_description"),
694 OgImage = Col(
"og_image")
741 Email = source.
Email,
744 PasswordHash = passwordHash,
745 PasswordSalt = passwordSalt,
750 OgImage = source.OgImage
780 var candidate = base_;
783 candidate = $
"{base_}{n++}";
813 var sb =
new StringBuilder();
814 foreach (var c
in name.ToLowerInvariant())
816 if (
char.IsLetterOrDigit(c)) sb.Append(c);
817 else if (c is
' ' or
'-' or
'_') sb.Append(
'-');
819 var s = sb.ToString().Trim(
'-');
820 return string.IsNullOrWhiteSpace(s) ?
"user" : s;
848 string.IsNullOrWhiteSpace(email) ?
"" : email.Trim().ToLowerInvariant();
886 return $
"{safeProvider}-{safeId}@agent.codemaru.local";
931 using var conn =
_db.Open();
932 using var cmd = conn.CreateCommand();
933 cmd.CommandText =
"UPDATE users SET password_hash = $hash, password_salt = $salt WHERE id = $id";
934 cmd.Parameters.AddWithValue(
"$hash", hash);
935 cmd.Parameters.AddWithValue(
"$salt", salt);
936 cmd.Parameters.AddWithValue(
"$id", userId);
937 await cmd.ExecuteNonQueryAsync();
982 var bytes = Encoding.UTF8.GetBytes(password + salt);
983 var actual = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
984 return string.Equals(actual, storedHash, StringComparison.OrdinalIgnoreCase);
async Task< VmsUser?> FindBySlugAsync(string slug)
static string NormalizeEmail(string? email)
async Task<(bool Ok, string Error, VmsUser? User)> SetAgentPasswordAsync(string userId, string password, string confirmPassword)
async Task SetLayoutAsync(string userId, string layout)
static VmsUser WithPasswordHash(VmsUser source, string passwordHash, string passwordSalt)
static string SlugFrom(string name)
async Task UpdateOgAsync(string userId, string ogTitle, string ogDescription, string ogImage)
static string BuildPseudoEmail(string provider, string identityId)
async Task<(bool Ok, string Error, VmsUser? User)> RegisterAsync(string email, string displayName, string password)
async Task UpdatePasswordHashAsync(string userId, string hash, string salt)
async Task< VmsUser?> FindByIdAsync(string id)
static bool VerifyLegacySaltedSha256(string password, string salt, string storedHash)
static async Task< VmsUser?> ReadOneAsync(SqliteCommand cmd)
async Task<(bool Ok, string Error, VmsUser? User)> LoginAsync(string email, string password)
List< VmsUser > GetUsersWithPublicCameras()
async Task< VmsUser?> EnsureExternalUserAsync(ClaimsPrincipal principal)
VmsUserService(VmsDatabase db)
async Task< string > UniqueSlugAsync(string displayName)
async Task< VmsUser?> FindByEmailAsync(string email)
static VmsUser ReadUser(SqliteDataReader r)