Files
SocialPub/PrivaPub.ClientModels/LoginForm.cs
T
thepraandClaude Opus 5.5 1f3bb27e17
Build / Build (push) Successful in 36s
R3 two-factor sign-in: TOTP with recovery codes, asked for only after a correct password
Completion program R3 (docs/ROADMAP.md, 2026-10-17).

- Services/TwoFactor.cs: TOTP (RFC 6238, 30 s, 6 digits, SHA-1, one step of slack, Otp.NET 1.4.1) and ten recovery
  codes (SHA-256 kept, pulled when used). RootUser.Totp holds the secret, the last step taken (moved only by a
  conditional update, so a code works once even for two sign-ins at the same moment) and the codes' hashes.
- /clientapi/user/totp/setup (the password) answers the secret, its otpauth:// URI and a QR code as an SVG data URI
  (QRCoder 1.8.0); /totp/enable takes the app's first code, shows the recovery codes once and ends every other session
  (this device gets a new one); /totp/disable takes the password and a code. GET /clientapi/user/account says whether
  it is on and how many recovery codes are left.
- Sign-in (/clientapi/user/login, the invitation login, /oauth/login) asks for LoginForm.OneTimeCode with a 428, and
  only once the password was right; the OAuth page has the field. Recovery by email leaves two-factor on.
- PrivaPub admin reset-totp <root>: the way back for a login that lost its authenticator and its codes.
- scenarios/accounts.sh checks it all, with codes computed in Python; the OAuth page with its antiforgery token.

Verified: dotnet build PrivaPub.sln -c Release; the pasture (gts, mailpit): accounts 45/45.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
2026-10-17 03:18:19 +02:00

43 lines
2.2 KiB
C#

using PrivaPub.ClientModels.Resources;
using PrivaPub.ClientModels.ValidatorAttributes;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.ClientModels
{
public class LoginForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
Display(Name = "Username", ResourceType = typeof(FieldsNameResource)),
StringLength(32, MinimumLength = 3, ErrorMessageResourceName = "StringLengthMinMax", ErrorMessageResourceType = typeof(ErrorsResource)),
NoWhiteSpaces]
public string UserName { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
DataType(DataType.Password, ErrorMessageResourceName = "InvalidPassword", ErrorMessageResourceType = typeof(ErrorsResource)),
PasswordPropertyText(true),
Display(Name = "Password", ResourceType = typeof(FieldsNameResource)),
StringLength(Constants.MaxPasswordLength, MinimumLength = Constants.MinPasswordLength, ErrorMessageResourceName = "StringLengthMinMax", ErrorMessageResourceType = typeof(ErrorsResource)),
RegularExpression(Constants.PasswordRegex, ErrorMessageResourceName = "InvalidPassword", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Password { get; set; }
[Range(0, 359, ErrorMessageResourceName = nameof(Range), ErrorMessageResourceType = typeof(ErrorsResource))]
public short LightThemeIndexColour { get; set; } = 25;
[Range(0, 359, ErrorMessageResourceName = nameof(Range), ErrorMessageResourceType = typeof(ErrorsResource))]
public short DarkThemeIndexColour { get; set; } = 215;
public short IconsThemeIndexColour { get; set; } = 25;
public bool ThemeIsDarkGray { get; set; } = false;
public bool ThemeIsLightGray { get; set; } = false;
public bool PreferSystemTheming { get; set; } = false;
public bool ThemeIsDarkMode { get; set; } = false;
public string InvitationPassword { get; set; }
// the authenticator app's code, or one of the recovery codes, for a login with two-factor sign-in; asked for with a
// 428 once the password was right
[StringLength(32, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string OneTimeCode { get; set; }
}
}