SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO IF OBJECT_ID(N'dbo.AppUser', N'U') IS NULL BEGIN CREATE TABLE dbo.AppUser ( UserID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AppUser PRIMARY KEY, Email nvarchar(256) NOT NULL, DisplayName nvarchar(200) NOT NULL, PasswordHash nvarchar(max) NOT NULL, EmailConfirmed bit NOT NULL CONSTRAINT DF_AppUser_EmailConfirmed DEFAULT 0, IsLocked bit NOT NULL CONSTRAINT DF_AppUser_IsLocked DEFAULT 0, FailedLoginAttempts int NOT NULL CONSTRAINT DF_AppUser_FailedLoginAttempts DEFAULT 0, LockoutEndUtc datetime2 NULL, TwoFactorEnabled bit NOT NULL CONSTRAINT DF_AppUser_TwoFactorEnabled DEFAULT 0, CreatedUtc datetime2 NOT NULL CONSTRAINT DF_AppUser_CreatedUtc DEFAULT SYSUTCDATETIME(), UpdatedUtc datetime2 NOT NULL CONSTRAINT DF_AppUser_UpdatedUtc DEFAULT SYSUTCDATETIME(), LastLoginUtc datetime2 NULL ); END; GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_AppUser_Email' AND object_id = OBJECT_ID(N'dbo.AppUser')) CREATE UNIQUE INDEX UX_AppUser_Email ON dbo.AppUser(Email); GO IF OBJECT_ID(N'dbo.UserEmailVerificationToken', N'U') IS NULL BEGIN CREATE TABLE dbo.UserEmailVerificationToken ( TokenID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_UserEmailVerificationToken PRIMARY KEY, UserID int NOT NULL, Token uniqueidentifier NOT NULL, ExpiryUtc datetime2 NOT NULL, UsedUtc datetime2 NULL, CONSTRAINT FK_UserEmailVerificationToken_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID) ); END; GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_UserEmailVerificationToken_Token' AND object_id = OBJECT_ID(N'dbo.UserEmailVerificationToken')) CREATE UNIQUE INDEX UX_UserEmailVerificationToken_Token ON dbo.UserEmailVerificationToken(Token); GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserEmailVerificationToken_UserID' AND object_id = OBJECT_ID(N'dbo.UserEmailVerificationToken')) CREATE INDEX IX_UserEmailVerificationToken_UserID ON dbo.UserEmailVerificationToken(UserID, ExpiryUtc DESC); GO IF OBJECT_ID(N'dbo.UserPasswordResetToken', N'U') IS NULL BEGIN CREATE TABLE dbo.UserPasswordResetToken ( TokenID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_UserPasswordResetToken PRIMARY KEY, UserID int NOT NULL, Token uniqueidentifier NOT NULL, ExpiryUtc datetime2 NOT NULL, UsedUtc datetime2 NULL, CONSTRAINT FK_UserPasswordResetToken_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID) ); END; GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_UserPasswordResetToken_Token' AND object_id = OBJECT_ID(N'dbo.UserPasswordResetToken')) CREATE UNIQUE INDEX UX_UserPasswordResetToken_Token ON dbo.UserPasswordResetToken(Token); GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserPasswordResetToken_UserID' AND object_id = OBJECT_ID(N'dbo.UserPasswordResetToken')) CREATE INDEX IX_UserPasswordResetToken_UserID ON dbo.UserPasswordResetToken(UserID, ExpiryUtc DESC); GO IF OBJECT_ID(N'dbo.UserTwoFactor', N'U') IS NULL BEGIN CREATE TABLE dbo.UserTwoFactor ( UserID int NOT NULL CONSTRAINT PK_UserTwoFactor PRIMARY KEY, SecretKey nvarchar(500) NOT NULL, RecoveryCodes nvarchar(max) NULL, CreatedUtc datetime2 NOT NULL CONSTRAINT DF_UserTwoFactor_CreatedUtc DEFAULT SYSUTCDATETIME(), CONSTRAINT FK_UserTwoFactor_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID) ); END; GO IF OBJECT_ID(N'dbo.UserLoginAudit', N'U') IS NULL BEGIN CREATE TABLE dbo.UserLoginAudit ( AuditID bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_UserLoginAudit PRIMARY KEY, UserID int NULL, EmailAttempted nvarchar(256) NULL, IpAddress nvarchar(100) NULL, UserAgent nvarchar(500) NULL, WasSuccessful bit NOT NULL, Reason nvarchar(200) NULL, CreatedUtc datetime2 NOT NULL CONSTRAINT DF_UserLoginAudit_CreatedUtc DEFAULT SYSUTCDATETIME(), CONSTRAINT FK_UserLoginAudit_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID) ); END; GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserLoginAudit_UserCreated' AND object_id = OBJECT_ID(N'dbo.UserLoginAudit')) CREATE INDEX IX_UserLoginAudit_UserCreated ON dbo.UserLoginAudit(UserID, CreatedUtc DESC); GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserLoginAudit_EmailCreated' AND object_id = OBJECT_ID(N'dbo.UserLoginAudit')) CREATE INDEX IX_UserLoginAudit_EmailCreated ON dbo.UserLoginAudit(EmailAttempted, CreatedUtc DESC); GO CREATE OR ALTER PROCEDURE dbo.User_GetByEmail @Email nvarchar(256) AS BEGIN SET NOCOUNT ON; SELECT UserID, Email, DisplayName, PasswordHash, EmailConfirmed, IsLocked, FailedLoginAttempts, LockoutEndUtc, TwoFactorEnabled, CreatedUtc, UpdatedUtc, LastLoginUtc FROM dbo.AppUser WHERE Email = @Email; END; GO CREATE OR ALTER PROCEDURE dbo.User_GetById @UserID int AS BEGIN SET NOCOUNT ON; SELECT UserID, Email, DisplayName, PasswordHash, EmailConfirmed, IsLocked, FailedLoginAttempts, LockoutEndUtc, TwoFactorEnabled, CreatedUtc, UpdatedUtc, LastLoginUtc FROM dbo.AppUser WHERE UserID = @UserID; END; GO CREATE OR ALTER PROCEDURE dbo.User_Create @Email nvarchar(256), @DisplayName nvarchar(200), @PasswordHash nvarchar(max) AS BEGIN SET NOCOUNT ON; INSERT INTO dbo.AppUser (Email, DisplayName, PasswordHash) VALUES (@Email, @DisplayName, @PasswordHash); DECLARE @UserID int = SCOPE_IDENTITY(); EXEC dbo.User_GetById @UserID = @UserID; END; GO CREATE OR ALTER PROCEDURE dbo.User_UpdateLoginSuccess @UserID int AS BEGIN SET NOCOUNT ON; UPDATE dbo.AppUser SET FailedLoginAttempts = 0, IsLocked = 0, LockoutEndUtc = NULL, LastLoginUtc = SYSUTCDATETIME(), UpdatedUtc = SYSUTCDATETIME() WHERE UserID = @UserID; EXEC dbo.User_GetById @UserID = @UserID; END; GO CREATE OR ALTER PROCEDURE dbo.User_UpdateFailedLogin @UserID int, @LockoutEndUtc datetime2 = NULL AS BEGIN SET NOCOUNT ON; UPDATE dbo.AppUser SET FailedLoginAttempts = FailedLoginAttempts + 1, IsLocked = CASE WHEN @LockoutEndUtc IS NULL THEN IsLocked ELSE 1 END, LockoutEndUtc = COALESCE(@LockoutEndUtc, LockoutEndUtc), UpdatedUtc = SYSUTCDATETIME() WHERE UserID = @UserID; EXEC dbo.User_GetById @UserID = @UserID; END; GO CREATE OR ALTER PROCEDURE dbo.User_CreateEmailVerificationToken @UserID int, @Token uniqueidentifier, @ExpiryUtc datetime2 AS BEGIN SET NOCOUNT ON; INSERT INTO dbo.UserEmailVerificationToken (UserID, Token, ExpiryUtc) VALUES (@UserID, @Token, @ExpiryUtc); DECLARE @TokenID int = SCOPE_IDENTITY(); SELECT TokenID, UserID, Token, ExpiryUtc, UsedUtc FROM dbo.UserEmailVerificationToken WHERE TokenID = @TokenID; END; GO CREATE OR ALTER PROCEDURE dbo.User_GetEmailVerificationToken @Token uniqueidentifier AS BEGIN SET NOCOUNT ON; SELECT TokenID, UserID, Token, ExpiryUtc, UsedUtc FROM dbo.UserEmailVerificationToken WHERE Token = @Token; END; GO CREATE OR ALTER PROCEDURE dbo.User_ConfirmEmail @UserID int, @Token uniqueidentifier AS BEGIN SET NOCOUNT ON; UPDATE dbo.UserEmailVerificationToken SET UsedUtc = SYSUTCDATETIME() WHERE UserID = @UserID AND Token = @Token AND UsedUtc IS NULL AND ExpiryUtc >= SYSUTCDATETIME(); IF @@ROWCOUNT = 1 BEGIN UPDATE dbo.AppUser SET EmailConfirmed = 1, UpdatedUtc = SYSUTCDATETIME() WHERE UserID = @UserID; SELECT CAST(1 AS bit); RETURN; END; SELECT CAST(0 AS bit); END; GO CREATE OR ALTER PROCEDURE dbo.User_CreatePasswordResetToken @UserID int, @Token uniqueidentifier, @ExpiryUtc datetime2 AS BEGIN SET NOCOUNT ON; INSERT INTO dbo.UserPasswordResetToken (UserID, Token, ExpiryUtc) VALUES (@UserID, @Token, @ExpiryUtc); DECLARE @TokenID int = SCOPE_IDENTITY(); SELECT TokenID, UserID, Token, ExpiryUtc, UsedUtc FROM dbo.UserPasswordResetToken WHERE TokenID = @TokenID; END; GO CREATE OR ALTER PROCEDURE dbo.User_GetPasswordResetToken @Token uniqueidentifier AS BEGIN SET NOCOUNT ON; SELECT TokenID, UserID, Token, ExpiryUtc, UsedUtc FROM dbo.UserPasswordResetToken WHERE Token = @Token; END; GO CREATE OR ALTER PROCEDURE dbo.User_ResetPassword @UserID int, @Token uniqueidentifier, @PasswordHash nvarchar(max) AS BEGIN SET NOCOUNT ON; UPDATE dbo.UserPasswordResetToken SET UsedUtc = SYSUTCDATETIME() WHERE UserID = @UserID AND Token = @Token AND UsedUtc IS NULL AND ExpiryUtc >= SYSUTCDATETIME(); IF @@ROWCOUNT = 1 BEGIN UPDATE dbo.AppUser SET PasswordHash = @PasswordHash, FailedLoginAttempts = 0, IsLocked = 0, LockoutEndUtc = NULL, UpdatedUtc = SYSUTCDATETIME() WHERE UserID = @UserID; SELECT CAST(1 AS bit); RETURN; END; SELECT CAST(0 AS bit); END; GO CREATE OR ALTER PROCEDURE dbo.User_EnableTwoFactor @UserID int AS BEGIN SET NOCOUNT ON; UPDATE dbo.AppUser SET TwoFactorEnabled = 1, UpdatedUtc = SYSUTCDATETIME() WHERE UserID = @UserID; EXEC dbo.User_GetById @UserID = @UserID; END; GO CREATE OR ALTER PROCEDURE dbo.User_DisableTwoFactor @UserID int AS BEGIN SET NOCOUNT ON; UPDATE dbo.AppUser SET TwoFactorEnabled = 0, UpdatedUtc = SYSUTCDATETIME() WHERE UserID = @UserID; EXEC dbo.User_GetById @UserID = @UserID; END; GO CREATE OR ALTER PROCEDURE dbo.User_GetTwoFactor @UserID int AS BEGIN SET NOCOUNT ON; SELECT UserID, SecretKey, RecoveryCodes, CreatedUtc FROM dbo.UserTwoFactor WHERE UserID = @UserID; END; GO CREATE OR ALTER PROCEDURE dbo.User_SaveTwoFactor @UserID int, @SecretKey nvarchar(500), @RecoveryCodes nvarchar(max) = NULL AS BEGIN SET NOCOUNT ON; MERGE dbo.UserTwoFactor AS target USING (SELECT @UserID AS UserID, @SecretKey AS SecretKey, @RecoveryCodes AS RecoveryCodes) AS source ON target.UserID = source.UserID WHEN MATCHED THEN UPDATE SET SecretKey = source.SecretKey, RecoveryCodes = source.RecoveryCodes WHEN NOT MATCHED THEN INSERT (UserID, SecretKey, RecoveryCodes) VALUES (source.UserID, source.SecretKey, source.RecoveryCodes); EXEC dbo.User_GetTwoFactor @UserID = @UserID; END; GO CREATE OR ALTER PROCEDURE dbo.User_InsertLoginAudit @UserID int = NULL, @EmailAttempted nvarchar(256) = NULL, @IpAddress nvarchar(100) = NULL, @UserAgent nvarchar(500) = NULL, @WasSuccessful bit, @Reason nvarchar(200) = NULL AS BEGIN SET NOCOUNT ON; INSERT INTO dbo.UserLoginAudit (UserID, EmailAttempted, IpAddress, UserAgent, WasSuccessful, Reason) VALUES (@UserID, @EmailAttempted, @IpAddress, @UserAgent, @WasSuccessful, @Reason); SELECT CAST(SCOPE_IDENTITY() AS bigint); END; GO