Merge pull request 'Added tables for MPIN and OTP as well created an Login API for Mobile Application' (#83) from Ashutosh_Task#469_Mobile_Login into Issue_Jun_1W_2
Reviewed-on: #83
This commit is contained in:
commit
7ef2c720cb
@ -70,6 +70,9 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
public DbSet<MailingList> MailingList { get; set; }
|
public DbSet<MailingList> MailingList { get; set; }
|
||||||
public DbSet<MailDetails> MailDetails { get; set; }
|
public DbSet<MailDetails> MailDetails { get; set; }
|
||||||
public DbSet<MailLog> MailLogs { get; set; }
|
public DbSet<MailLog> MailLogs { get; set; }
|
||||||
|
public DbSet<OTPDetails> OTPDetails { get; set; }
|
||||||
|
public DbSet<MPINDetails> MPINDetails { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
2670
Marco.Pms.DataAccess/Migrations/20250605102139_Added_OTP_And_MPIN_Table.Designer.cs
generated
Normal file
2670
Marco.Pms.DataAccess/Migrations/20250605102139_Added_OTP_And_MPIN_Table.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Marco.Pms.DataAccess.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Added_OTP_And_MPIN_Table : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "MPINDetails",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
MPIN = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
MPINToken = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
TimeStamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_MPINDetails", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MPINDetails_Tenants_TenantId",
|
||||||
|
column: x => x.TenantId,
|
||||||
|
principalTable: "Tenants",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OTPDetails",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
OTP = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
ExpriesInSec = table.Column<int>(type: "int", nullable: false),
|
||||||
|
TimeStamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OTPDetails", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OTPDetails_Tenants_TenantId",
|
||||||
|
column: x => x.TenantId,
|
||||||
|
principalTable: "Tenants",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MPINDetails_TenantId",
|
||||||
|
table: "MPINDetails",
|
||||||
|
column: "TenantId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OTPDetails_TenantId",
|
||||||
|
table: "OTPDetails",
|
||||||
|
column: "TenantId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "MPINDetails");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OTPDetails");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -229,6 +229,65 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.ToTable("AttendanceLogs");
|
b.ToTable("AttendanceLogs");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("MPIN")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("MPINToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("TimeStamp")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.ToTable("MPINDetails");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<int>("ExpriesInSec")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("OTP")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("TimeStamp")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.ToTable("OTPDetails");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b =>
|
modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -2118,6 +2177,28 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.Navigation("UpdatedByEmployee");
|
b.Navigation("UpdatedByEmployee");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Tenant");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b =>
|
modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
|
||||||
|
13
Marco.Pms.Model/Authentication/MPINDetails.cs
Normal file
13
Marco.Pms.Model/Authentication/MPINDetails.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Marco.Pms.Model.Utilities;
|
||||||
|
|
||||||
|
namespace Marco.Pms.Model.Authentication
|
||||||
|
{
|
||||||
|
public class MPINDetails : TenantRelation
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public string MPIN { get; set; } = string.Empty;
|
||||||
|
public string MPINToken { get; set; } = string.Empty;
|
||||||
|
public DateTime TimeStamp { get; set; }
|
||||||
|
}
|
||||||
|
}
|
13
Marco.Pms.Model/Authentication/OTPDetails.cs
Normal file
13
Marco.Pms.Model/Authentication/OTPDetails.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Marco.Pms.Model.Utilities;
|
||||||
|
|
||||||
|
namespace Marco.Pms.Model.Authentication
|
||||||
|
{
|
||||||
|
public class OTPDetails : TenantRelation
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public string OTP { get; set; } = string.Empty;
|
||||||
|
public int ExpriesInSec { get; set; }
|
||||||
|
public DateTime TimeStamp { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -71,6 +71,79 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
|
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("login-mobile")]
|
||||||
|
public async Task<IActionResult> LoginMobile([FromBody] LoginDto loginDto)
|
||||||
|
{
|
||||||
|
// Validate input DTO
|
||||||
|
if (loginDto == null || string.IsNullOrWhiteSpace(loginDto.Username) || string.IsNullOrWhiteSpace(loginDto.Password))
|
||||||
|
{
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Username or password is missing.", "Invalid request", 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user by email or phone number
|
||||||
|
var user = await _context.ApplicationUsers
|
||||||
|
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username);
|
||||||
|
|
||||||
|
// If user not found, return unauthorized
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is inactive
|
||||||
|
if (!user.IsActive)
|
||||||
|
{
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("User is inactive", "User is inactive", 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user email is not confirmed
|
||||||
|
if (!user.EmailConfirmed)
|
||||||
|
{
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Your email is not verified. Please verify your email.", "Email not verified", 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate password using ASP.NET Identity
|
||||||
|
var isPasswordValid = await _userManager.CheckPasswordAsync(user, loginDto.Password);
|
||||||
|
if (!isPasswordValid)
|
||||||
|
{
|
||||||
|
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid credentials", 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if username is missing
|
||||||
|
if (string.IsNullOrWhiteSpace(user.UserName))
|
||||||
|
{
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("UserName not found", "Username is missing", 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get employee information for tenant context
|
||||||
|
var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
|
||||||
|
if (emp == null)
|
||||||
|
{
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee details missing", 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT token
|
||||||
|
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings);
|
||||||
|
|
||||||
|
// Generate Refresh Token and store in DB
|
||||||
|
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
|
||||||
|
|
||||||
|
// Generate MPIN Token (custom short-term token)
|
||||||
|
var mpinToken = await _refreshTokenService.CreateMPINToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
|
||||||
|
|
||||||
|
// Combine all tokens in response
|
||||||
|
var responseData = new
|
||||||
|
{
|
||||||
|
token,
|
||||||
|
refreshToken,
|
||||||
|
mpinToken
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return success response
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(responseData, "User logged in successfully.", 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("logout")]
|
[HttpPost("logout")]
|
||||||
public async Task<IActionResult> Logout([FromBody] LogoutDto logoutDto)
|
public async Task<IActionResult> Logout([FromBody] LogoutDto logoutDto)
|
||||||
{
|
{
|
||||||
@ -206,8 +279,6 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return Ok(ApiResponse<object>.SuccessResponse(result.Succeeded, "Password reset successfully.", 200));
|
return Ok(ApiResponse<object>.SuccessResponse(result.Succeeded, "Password reset successfully.", 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("sendmail")]
|
[HttpPost("sendmail")]
|
||||||
public async Task<IActionResult> SendEmail([FromBody] EmailDot emailDot)
|
public async Task<IActionResult> SendEmail([FromBody] EmailDot emailDot)
|
||||||
{
|
{
|
||||||
|
@ -48,36 +48,37 @@ namespace MarcoBMS.Services.Service
|
|||||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> CreateRefreshToken(string userId, string tenantId, JwtSettings _jwtSettings)
|
public async Task<string> CreateRefreshToken(string userId, string tenantId, JwtSettings jwtSettings)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
var claims = new[]
|
||||||
var key = Encoding.UTF8.GetBytes(_jwtSettings.Key);
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, userId),
|
||||||
|
new Claim("TenantId", tenantId),
|
||||||
|
new Claim("token_type", "refresh")
|
||||||
|
};
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key));
|
||||||
|
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
|
||||||
|
|
||||||
var tokenDescriptor = new SecurityTokenDescriptor
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
{
|
{
|
||||||
Subject = new ClaimsIdentity(new[]
|
Subject = new ClaimsIdentity(claims),
|
||||||
{
|
Expires = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays),
|
||||||
new Claim(ClaimTypes.NameIdentifier, userId),
|
Issuer = jwtSettings.Issuer,
|
||||||
new Claim("TenantId", tenantId), // Add TenantId claim
|
Audience = jwtSettings.Audience,
|
||||||
|
SigningCredentials = credentials
|
||||||
new Claim("token_type", "refresh") // Custom claim to differentiate refresh tokens
|
|
||||||
}),
|
|
||||||
Expires = DateTime.UtcNow.AddDays(7), // Refresh token valid for 7 days
|
|
||||||
Issuer = _jwtSettings.Issuer,
|
|
||||||
Audience = _jwtSettings.Audience,
|
|
||||||
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
string strToken = tokenHandler.WriteToken(token);
|
var refreshTokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
|
||||||
|
|
||||||
var refreshToken = new RefreshToken
|
var refreshToken = new RefreshToken
|
||||||
{
|
{
|
||||||
Token = strToken,
|
Token = refreshTokenString,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
ExpiryDate = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiresInDays),
|
ExpiryDate = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays),
|
||||||
IsRevoked = false
|
IsRevoked = false
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,7 +90,7 @@ namespace MarcoBMS.Services.Service
|
|||||||
_context.RefreshTokens.Add(refreshToken);
|
_context.RefreshTokens.Add(refreshToken);
|
||||||
}
|
}
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return strToken;
|
return refreshTokenString;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -97,6 +98,52 @@ namespace MarcoBMS.Services.Service
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public async Task<string> CreateMPINToken(string userId, string tenantId, JwtSettings jwtSettings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existingMPIN = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(userId) && p.TenantId == Guid.Parse(tenantId));
|
||||||
|
if (existingMPIN != null)
|
||||||
|
{
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, userId),
|
||||||
|
new Claim("TenantId", tenantId),
|
||||||
|
new Claim("token_type", "mpin")
|
||||||
|
};
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key));
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
|
||||||
|
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Subject = new ClaimsIdentity(claims),
|
||||||
|
Issuer = jwtSettings.Issuer,
|
||||||
|
Audience = jwtSettings.Audience,
|
||||||
|
SigningCredentials = creds
|
||||||
|
// No 'Expires' means the token won't expire
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var MPINToken = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
existingMPIN.MPINToken = MPINToken;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return MPINToken;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError("Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}, error : {Error}", userId, tenantId, ex.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<RefreshToken> GetRefreshToken(string token)
|
public async Task<RefreshToken> GetRefreshToken(string token)
|
||||||
{
|
{
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
"RefreshTokenExpiresInDays": 7
|
"RefreshTokenExpiresInDays": 7
|
||||||
},
|
},
|
||||||
"MailingList": {
|
"MailingList": {
|
||||||
"RequestDemoReceivers": "ashutosh.nehete@marcoaiot.com;vikas@marcoaiot.com;umesh@marcoait.com",
|
"RequestDemoReceivers": "ashutosh.nehete@marcoaiot.com;vikas@marcoaiot.com;umesh@marcoait.com"
|
||||||
//"ProjectStatisticsReceivers": "ashutosh.nehete@marcoaiot.com;vikas@marcoaiot.com;umesh@marcoait.com"
|
//"ProjectStatisticsReceivers": "ashutosh.nehete@marcoaiot.com;vikas@marcoaiot.com;umesh@marcoait.com"
|
||||||
},
|
},
|
||||||
"AWS": {
|
"AWS": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user