USE [main]
-- Session Management Cheatsheet:
-- https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html

IF OBJECT_ID('dbo.[ActivityLog]', 'U') IS NOT NULL
	DROP TABLE [ActivityLog]

IF OBJECT_ID('dbo.[Session]', 'U') IS NOT NULL
	DROP TABLE [Session]

IF OBJECT_ID('dbo.[Event]', 'U') IS NOT NULL
	DROP TABLE [Event]

IF OBJECT_ID('dbo.[EventLevel]', 'U') IS NOT NULL
	DROP TABLE [EventLevel]

IF OBJECT_ID('dbo.[Login]', 'U') IS NOT NULL
	DROP TABLE [Login]

IF OBJECT_ID('dbo.[User]', 'U') IS NOT NULL
	DROP TABLE [User]

CREATE TABLE [User]
(
	[User ID] INT IDENTITY(1,1),
	[Name] NVARCHAR(64) UNIQUE NOT NULL,
	[Email] NVARCHAR(64) UNIQUE NOT NULL,

	PRIMARY KEY([User ID])
)

CREATE TABLE [Login]
(
	[Login ID] INT NOT NULL IDENTITY(1, 1),
	[Password Hash] VARCHAR(1024) NOT NULL,
	[Salt] VARCHAR(512) NOT NULL,
	[User Key] INT NOT NULL,
	[Login Attempts] INT NOT NULL DEFAULT 0,
	[Last Login Attempt] DATETIME,
	
	PRIMARY KEY([Login ID]),
	FOREIGN KEY ([User Key]) REFERENCES [User]([User ID])
)

CREATE TABLE [Session]
(
	[Session ID] INT NOT NULL IDENTITY(1, 1),
	[Session Token] VARCHAR(1024) UNIQUE NOT NULL,
	[Expiration Date] DATETIME NOT NULL,
	[User ID] INT NOT NULL,
	[IP Address] VARCHAR(256) NOT NULL,
	[User Agent] VARCHAR(256) NOT NULL,
	[Pending Reset] BIT NOT NULL DEFAULT 0,

	PRIMARY KEY([Session ID]),
	FOREIGN KEY([User ID]) REFERENCES [User]([User ID])
)

CREATE TABLE [EventLevel]
(
	[Event Level ID] TINYINT NOT NULL,
	[Event Display Name] VARCHAR(64) NOT NULL,

	PRIMARY KEY([Event Level ID])
)

CREATE TABLE [Event]
(
	[Event ID] INT NOT NULL IDENTITY(1, 1),
	[Event Name] VARCHAR(128) NOT NULL,
	[Event Level ID] TINYINT NOT NULL,

	PRIMARY KEY([Event ID]),
	FOREIGN KEY([Event Level ID]) REFERENCES [EventLevel]([Event Level ID])
)

CREATE TABLE [ActivityLog]
(
	[Log ID] INT NOT NULL IDENTITY(1, 1),
	[Event ID] INT NOT NULL,
	[Event Description] NVARCHAR(512) NOT NULL,
	[Timestamp] DATETIME NOT NULL,
	[Component] VARCHAR(128) NOT NULL,

	PRIMARY KEY([Log ID]),
	FOREIGN KEY([Event ID]) REFERENCES [Event] ([Event ID])
)

-- The smaller the number the higher the severity.
INSERT INTO [EventLevel] ([Event Level ID], [Event Display Name]) VALUES(0, 'Critical')
INSERT INTO [EventLevel] ([Event Level ID], [Event Display Name]) VALUES(1, 'Error')
INSERT INTO [EventLevel] ([Event Level ID], [Event Display Name]) VALUES(2, 'Warning')
INSERT INTO [EventLevel] ([Event Level ID], [Event Display Name]) VALUES(3, 'Information')
INSERT INTO [EventLevel] ([Event Level ID], [Event Display Name]) VALUES(255, 'Debug')

INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Login', 3)
INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Logout', 3)
INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Registration', 3)
INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Session Expiration', 3)
INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Password Reset Request', 3)
INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Password Reset', 3)

/*
INSERT INTO [User] (Name, Email) VALUES ('glm94', 'glmccune@gmail.com')
INSERT INTO [User] (Name, Email) VALUES ('admin', 'admin@gmail.com')
INSERT INTO [User] (Name, Email) VALUES ('nobody', 'none@gmail.com')
*/
-- Define Functions
GO
/****** Object:  UserDefinedFunction [dbo].[UserExists]    Script Date: 1/13/2021 7:23:30 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:		<Author,,Name>
-- Create date: <Create Date, ,>
-- Description:	<Description, ,>
-- =============================================
CREATE OR ALTER FUNCTION [dbo].[UserExists]
(
	@UserName NVARCHAR(512)
)
RETURNS BIT
AS
BEGIN
	
	DECLARE @Exists BIT = 0

	IF EXISTS ( SELECT 1 FROM [User] WHERE [Name] = @UserName )
	BEGIN
		SET @Exists = 1;
	END

	RETURN @Exists;
END
GO
GO
/****** Object:  UserDefinedFunction [dbo].[UserExists]    Script Date: 1/14/2021 5:31:27 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:		<Author,,Name>
-- Create date: <Create Date, ,>
-- Description:	<Description, ,>
-- =============================================
CREATE OR ALTER FUNCTION [dbo].[EmailInUse]
(
	@Email NVARCHAR(512)
)
RETURNS BIT
AS
BEGIN
	
	DECLARE @Exists BIT = 0

	IF EXISTS ( SELECT 1 FROM [User] WHERE [Email] = @Email )
	BEGIN
		SET @Exists = 1;
	END

	RETURN @Exists;
END
GO
GO
-- Define Stored Procedures
/****** Object:  UserDefinedFunction [dbo].[GetUserPasswordHashAndSalt]    Script Date: 1/13/2021 7:23:30 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:		<Author,,Name>
-- Create date: <Create Date,,>
-- Description:	<Description,,>
-- =============================================
CREATE OR ALTER FUNCTION [dbo].[GetUserPasswordHashAndSalt]
(	
	@Username NVARCHAR(64)
)
RETURNS TABLE 
AS
RETURN 
(
	SELECT [Login].[Password Hash], [Login].Salt
	FROM [Login]
	INNER JOIN [User] ON
		[User].[User ID] = [Login].[User Key]
	WHERE [User].[Name] = @Username
)
GO
/****** Object:  StoredProcedure [dbo].[LoginUser]    Script Date: 1/13/2021 7:23:30 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:		<Author,,Name>
-- Create date: <Create Date,,>
-- Description:	<Description,,>
-- =============================================
CREATE OR ALTER PROCEDURE [dbo].[LoginUser]
	@UserName NVARCHAR(64),
	@SessionToken VARCHAR(1024),
	@ExpirationDate DATETIME,
	@IpAddress VARCHAR(256),
	@UserAgent VARCHAR(256)
AS
BEGIN
	-- SET NOCOUNT ON added to prevent extra result sets from
	-- interfering with SELECT statements.
    SET NOCOUNT, XACT_ABORT ON;

	DECLARE @UserID INT
	DECLARE @EventID INT
	DECLARE @Description NVARCHAR(512)

	SELECT @UserID = [User ID] FROM [User] WHERE [User].[Name] = @UserName

	BEGIN TRY
		BEGIN TRANSACTION;
			IF dbo.UserExists(@UserName) = 0
			BEGIN;
				THROW 51000, 'User does not exist', 1;
			END;

			INSERT INTO [Session] ([Session Token], [Expiration Date], [User ID], [IP Address], [User Agent], [Pending Reset]) VALUES (@SessionToken, @ExpirationDate, @UserID, @IpAddress, @UserAgent, 0)

			SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Login'

			SET @Description = FORMATMESSAGE('"%s" logged in', @UserName)

			INSERT INTO [ActivityLog] ([Event ID], [Event Description], [Timestamp], [Component]) VALUES (@EventID, @Description, SYSDATETIME(), 'SP_LoginUser')
				
		COMMIT TRANSACTION;
	END TRY
	BEGIN CATCH 

		IF @@TRANCOUNT > 0
		BEGIN
			ROLLBACK TRANSACTION
		END;

		THROW;

	END CATCH;

	SET NOCOUNT, XACT_ABORT OFF;
END
GO
/****** Object:  StoredProcedure [dbo].[LogoutUser]    Script Date: 1/13/2021 7:23:30 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:		<Author,,Name>
-- Create date: <Create Date,,>
-- Description:	<Description,,>
-- =============================================
CREATE OR ALTER PROCEDURE [dbo].[LogoutUser]
	@SessionToken NVARCHAR(1024)
AS
BEGIN
	-- SET NOCOUNT ON added to prevent extra result sets from
	-- interfering with SELECT statements.
	SET NOCOUNT ON;

	DECLARE @SessionID INT
	DECLARE @UserName NVARCHAR(512)
	DECLARE @Success BIT = 0
	DECLARE @EventID INT

	SELECT @SessionID = [Session ID], @UserName = [Name] FROM [Session] INNER JOIN [User] ON [Session].[User ID] = [User].[User ID] WHERE [Session Token] = @SessionToken
	SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Logout'

	SET NOCOUNT, XACT_ABORT ON;
    BEGIN TRY
        BEGIN TRANSACTION;
			
			IF @SessionID IS NULL
			BEGIN;
				THROW 51000, 'Session token not found', 1;
			END;

			DELETE FROM [Session] WHERE [Session ID] = @SessionID

			IF @EventID IS NOT NULL
				INSERT INTO ActivityLog ([Event Description], [Timestamp], [Component], [Event ID]) VALUES (FORMATMESSAGE('User "%s" has logged out', @UserName), SYSDATETIME(), 'SP_LogoutUser', @EventID)

        COMMIT TRANSACTION;

		SET @Success = 1

    END TRY
    BEGIN CATCH 
        IF @@TRANCOUNT > 0
        BEGIN
            ROLLBACK TRANSACTION
        END;

		THROW;

    END CATCH;
	SET NOCOUNT, XACT_ABORT OFF;

	SELECT @Success AS Success
END
GO
/****** Object:  StoredProcedure [dbo].[LogPasswordResetRequest]    Script Date: 1/13/2021 7:23:30 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:		<Author,,Name>
-- Create date: <Create Date,,>
-- Description:	<Description,,>
-- =============================================
CREATE OR ALTER PROCEDURE [dbo].[LogPasswordResetRequest]
	@Email NVARCHAR(64),
	@SessionToken VARCHAR(1024),
	@ExpirationDate DATETIME,
	@IpAddress VARCHAR(256),
	@UserAgent VARCHAR(256)
AS
BEGIN
	-- SET NOCOUNT ON added to prevent extra result sets from
	-- interfering with SELECT statements.
    SET NOCOUNT, XACT_ABORT ON;

	DECLARE @UserID INT
	DECLARE @EventID INT
	DECLARE @Description NVARCHAR(512)
	DECLARE @UserName NVARCHAR(64)

	SELECT @UserID = [User ID], @UserName = [Name] FROM [User] WHERE [User].[Email] = @Email

	BEGIN TRY
		BEGIN TRANSACTION;
			IF dbo.UserExists(@UserName) = 0
			BEGIN;
				THROW 51000, 'User does not exist', 1;
			END;

			INSERT INTO [Session] ([Session Token], [Expiration Date], [User ID], [IP Address], [User Agent], [Pending Reset]) VALUES (@SessionToken, @ExpirationDate, @UserID, @IpAddress, @UserAgent, 1)

			SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Password Reset Request'

			SET @Description = FORMATMESSAGE('"%s" requested a password reset', @UserName)

			INSERT INTO [ActivityLog] ([Event ID], [Event Description], [Timestamp], [Component]) VALUES (@EventID, @Description, SYSDATETIME(), 'SP_LogPasswordResetRequest')
				
		COMMIT TRANSACTION;
	END TRY
	BEGIN CATCH 

		IF @@TRANCOUNT > 0
		BEGIN
			ROLLBACK TRANSACTION
		END;

		THROW;

	END CATCH;

	SET NOCOUNT, XACT_ABORT OFF;
END
GO
/****** Object:  StoredProcedure [dbo].[RegisterNewUser]    Script Date: 1/13/2021 7:23:30 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:		<Author,,Name>
-- Create date: <Create Date,,>
-- Description:	<Description,,>
-- =============================================
CREATE OR ALTER PROCEDURE [dbo].[RegisterNewUser]
	@UserName NVARCHAR(512),
	@Email NVARCHAR(512),
	@Password NVARCHAR(1024),
	@Salt NVARCHAR(512),
	@SessionToken NVARCHAR(1024),
	@ExpirationDate DATETIME,
	@UserAgent VARCHAR(256),
	@IpAddress VARCHAR(256)
AS
BEGIN
	-- SET NOCOUNT ON added to prevent extra result sets from
	-- interfering with SELECT statements.
	DECLARE @UserID INT
	DECLARE @EventID INT
	DECLARE @SessionID INT

	SET NOCOUNT, XACT_ABORT ON;

		BEGIN TRY

			BEGIN TRANSACTION;
				IF dbo.UserExists(@UserName) = 1
				BEGIN;
					THROW 51000, 'This username is already in use', 1;
				END;

				IF dbo.EmailInUse(@Email) = 1

				BEGIN;
					THROW 51001, 'This email is already in use', 1
				END;

				SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Registration'

				IF @EventID = NULL OR @EventID = 0
				BEGIN;
					THROW 51002, 'Registration event type not found', 1;
				END;

				INSERT INTO [User] (Name, Email) VALUES (@UserName, @Email)

				SELECT @UserID = SCOPE_IDENTITY()

				INSERT INTO [Login] ([Password Hash], [Salt], [User Key]) VALUES (@Password, @Salt, @UserId)

				INSERT INTO [Session] ([Session Token], [Expiration Date], [User ID], [User Agent], [IP Address]) VALUES (@SessionToken, @ExpirationDate, @UserId, @UserAgent, @IpAddress)

				SELECT @SessionID = SCOPE_IDENTITY()

				INSERT INTO [ActivityLog] ([Event ID], [Event Description], [Timestamp], [Component]) VALUES (@EventID, FORMATMESSAGE('Registered user "%s".', @UserName), SYSDATETIME(), 'SP_RegisterUser')

				SELECT @UserID AS UserKey
				
			COMMIT TRANSACTION;

		END TRY
		BEGIN CATCH 

		   IF @@TRANCOUNT > 0
		   BEGIN
			  ROLLBACK TRANSACTION
		   END;

		   THROW;

		END CATCH;

	SET NOCOUNT, XACT_ABORT OFF;
END
GO
/****** Object:  StoredProcedure [dbo].[ResetPassword]    Script Date: 1/13/2021 7:23:30 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:		<Author,,Name>
-- Create date: <Create Date,,>
-- Description:	<Description,,>
-- =============================================
CREATE OR ALTER PROCEDURE [dbo].[ResetPassword]
	@PasswordHash VARCHAR(1024),
	@Salt VARCHAR(512),
	@SessionToken VARCHAR(1024)
AS
BEGIN
	-- SET NOCOUNT ON added to prevent extra result sets from
	-- interfering with SELECT statements.
	DECLARE @Username NVARCHAR(64)
	DECLARE @UserID INT
	DECLARE @EventID INT

	SELECT @UserID = [User ID] FROM [Session] WHERE [Session Token] = @SessionToken
	SELECT @Username = [Name] FROM [User] WHERE [User ID] = @UserID

	SET NOCOUNT, XACT_ABORT ON;

		BEGIN TRY

			BEGIN TRANSACTION;
				IF dbo.UserExists(@UserName) = 0
				BEGIN;
					THROW 51002, 'User does not exist', 1;
				END;

				SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Password Reset'

				IF @EventID = NULL OR @EventID = 0
				BEGIN;
					THROW 51001, 'Password reset event type not found', 1;
				END;

				UPDATE [Login] SET [Password Hash] = @PasswordHash, [Salt] = @Salt, [Last Login Attempt] = NULL WHERE [User Key] = @UserID

				DELETE FROM [Session] WHERE [User ID] = @UserID

				INSERT INTO [ActivityLog] ([Event ID], [Event Description], [Timestamp], [Component]) VALUES (@EventID, FORMATMESSAGE('User "%s" has had their password reset.', @UserName), SYSDATETIME(), 'SP_ResetPassword')
				
			COMMIT TRANSACTION;

		END TRY
		BEGIN CATCH 

		   IF @@TRANCOUNT > 0
		   BEGIN
			  ROLLBACK TRANSACTION
		   END;

		   THROW;

		END CATCH;

	SET NOCOUNT, XACT_ABORT OFF;
END
GO
/****** Object:  StoredProcedure [dbo].[ValidateSessionToken]    Script Date: 1/13/2021 7:23:30 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:		<Author,,Name>
-- Create date: <Create Date,,>
-- Description:	<Description,,>
-- =============================================
CREATE OR ALTER PROCEDURE [dbo].[ValidateSessionToken]
	@SessionToken VARCHAR(1024),
	@IsResetToken BIT
AS
BEGIN
	-- SET NOCOUNT ON added to prevent extra result sets from
	-- interfering with SELECT statements.
	SET NOCOUNT, XACT_ABORT OFF;
	DECLARE @SessionExpiration DATETIME
	DECLARE @SessionID INT
	DECLARE @SessionIsValid BIT = 0
	DECLARE @UserID INT
	DECLARE @EventID TINYINT

	SELECT @SessionID = [Session ID], @SessionExpiration = [Expiration Date], @UserID = [User ID] FROM [Session] WHERE [Session Token] = @SessionToken AND [Pending Reset] = @IsResetToken
	SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Session Expiration'

	SET NOCOUNT, XACT_ABORT ON;
		BEGIN TRY
			BEGIN TRANSACTION;
				-- Code here
				IF @SessionID IS NOT NULL
				BEGIN
					-- Sesssion found.
					IF @SessionExpiration > SYSDATETIME()
						SET @SessionIsValid = 1
					ELSE
					BEGIN
						DELETE FROM [Session] WHERE [Session ID] = @SessionID

						IF @EventID IS NOT NULL
							INSERT INTO [ActivityLog] ([Event Description], [Timestamp], [Event ID], [Component]) VALUES ('Session token has expired', SYSDATETIME(), @EventID, 'SP_ValidateSessionToken')
					END
				END
			COMMIT TRANSACTION;
		END TRY
		BEGIN CATCH 
			IF @@TRANCOUNT > 0
			BEGIN
				ROLLBACK TRANSACTION
			END;
		END CATCH;
	SET NOCOUNT, XACT_ABORT OFF;
	
	SELECT @SessionIsValid AS SessionIsValid
END
GO
