Compare commits

..

155 Commits
main ... Dev2

Author SHA1 Message Date
304b5e6d94 Update 2025-03-26 14:24:44 +08:00
6e44454808 Update 2025-03-21 15:30:55 +08:00
d6d151a3c7 Update & Fix Eerror 2025-03-20 15:46:52 +08:00
7a08ac374a Item Fix 2025-03-19 09:50:27 +08:00
cace9c396a appSetting 2025-03-19 08:30:18 +08:00
4de1a6633c Product & Item Button Update 2025-03-19 08:29:30 +08:00
bad7c62bbb Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-17 10:31:43 +08:00
f4280b9645 Update Item & Station 2025-03-17 10:31:07 +08:00
7c66571264 Update appsettings.json 2025-03-17 09:56:25 +08:00
00a040c0ed Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-14 11:03:22 +08:00
213fd2b460 Update Item & Station 2025-03-14 11:03:12 +08:00
61f053175f Update RBAC 2025-03-14 11:02:45 +08:00
e5e1414826 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-14 07:44:33 +08:00
c45251e08f Update Item & Product 2025-03-14 07:44:19 +08:00
2c10b89147 small fix 2025-03-14 07:29:25 +08:00
58f3fc4c48 Update Item & Product 2025-03-12 07:42:57 +08:00
9f1283f7c9 Update Request & Fix Item Qr Code 2025-03-11 09:54:45 +08:00
bf6e53b56e Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-10 22:28:56 +08:00
ce2a741450 Update Qr 2025-03-10 22:28:46 +08:00
291d4dabdb Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-10 15:59:45 +08:00
a1db9175a3 fix item movement qr master 2025-03-10 15:59:40 +08:00
ed04371e3c Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-10 09:40:42 +08:00
21a950f519 Update Qr 2025-03-10 09:40:33 +08:00
cc99419e49 maintain code readability 2025-03-10 09:31:48 +08:00
6c2923ca1a maintain code readability 2025-03-10 09:30:17 +08:00
53bbde0f0b Update 2025-03-10 08:57:27 +08:00
2d474477fa Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-08 02:24:29 +08:00
1e49f979aa Item Movement User 2025-03-08 02:24:14 +08:00
02197f4c1f fix ui admin item movement 2025-03-07 23:31:07 +08:00
9ed5e6b6a5 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-07 14:31:25 +08:00
d316a42a5e update ui 2025-03-07 14:31:19 +08:00
9857983174 Item Movement User 2025-03-07 14:29:48 +08:00
0225c22696 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-07 09:08:58 +08:00
e43205c08e update 2025-03-07 09:08:48 +08:00
a3dc02fb80 Item Movement User 2025-03-07 09:07:47 +08:00
e78e1c979e Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-06 15:53:28 +08:00
ea9038d6b8 fix ui itemmovement admin
update product image
2025-03-06 15:53:22 +08:00
1542750302 Item Movement & request 2025-03-06 15:52:41 +08:00
37ea90be65 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-06 12:19:49 +08:00
ea54070d54 fix table itemmovement master 2025-03-06 12:19:42 +08:00
ce941d5d53 Item Movement User 2025-03-06 12:18:18 +08:00
48636528b1 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-06 09:11:43 +08:00
c36a1e6094 fix itemmovement display functions 2025-03-06 09:11:38 +08:00
dd23c13644 Item Movement User 2025-03-06 09:10:41 +08:00
1411c0c830 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-06 08:51:57 +08:00
aaa93edb89 localhost launch 2025-03-06 08:51:52 +08:00
51150e5377 tweak camera settings 2025-03-06 08:51:25 +08:00
fdc3c81574 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-06 08:36:24 +08:00
00396e4b0f Qr 2025-03-06 08:36:08 +08:00
6018c3df2c camera settings 2025-03-06 08:34:13 +08:00
f4fc5dc103 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-05 15:55:25 +08:00
e64e288eac Qr & Movement Display 2025-03-05 15:55:20 +08:00
f6f2990c8f update admin itemmovement by station
experiment camera tweaking
2025-03-05 15:54:50 +08:00
98161fd740 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-05 09:45:41 +08:00
7e5f3e3b36 update userinformation and fetchitemmovement 2025-03-05 09:45:35 +08:00
f8be5be392 Qr & Movement Display 2025-03-05 09:38:24 +08:00
3ec456afbd Qr & Movement Display 2025-03-04 15:55:25 +08:00
a0d84272aa Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-04 14:37:28 +08:00
26aaff2d2b update item movement master 2025-03-04 14:37:23 +08:00
eab78d321f Qr & Movement Display 2025-03-04 14:36:04 +08:00
4afac02583 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-03-03 15:57:01 +08:00
18ecaedda8 update qrmaster 2025-03-03 15:56:47 +08:00
eba3f6c5c4 Qr 2025-03-03 15:56:31 +08:00
b7ec8ab7af Qr 2025-03-03 09:12:12 +08:00
f4125a09ed Qr 2025-03-03 09:11:54 +08:00
11e8e84064 Qr 2025-03-03 08:00:11 +08:00
6150baa25d install vueqrscanner 2025-02-28 15:27:44 +08:00
9d9f931642 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-02-28 11:59:12 +08:00
38a9e623a8 Qr 2025-02-28 11:59:05 +08:00
976a83bbcc small fix ui 2025-02-28 09:49:04 +08:00
2d574ad949 Request 2025-02-28 08:58:10 +08:00
dde2da8d41 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-02-28 08:34:31 +08:00
00264dd2d2 fix table in ui 2025-02-28 08:34:26 +08:00
a7ffd18754 update ui/ux movement 2025-02-28 08:33:49 +08:00
e1cf4fc885 Qr and movment UI/UX 2025-02-27 22:12:45 +08:00
0ff7592b01 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-02-27 16:32:42 +08:00
df2ec7e88c update movement user view only 2025-02-27 16:32:32 +08:00
98ae0e1ad6 update itemmovementmaster 2025-02-27 16:32:09 +08:00
ebf8008b22 update movement user view only 2025-02-26 22:09:32 +08:00
788ab1aa5a Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-02-26 16:28:14 +08:00
41d1c8e1af request & Qr 2025-02-26 16:28:08 +08:00
02f8ce4cd8 small update 2025-02-26 16:28:03 +08:00
01814cc26f fix user fullname in product request 2025-02-26 16:23:58 +08:00
20c01825f9 fix merged 2025-02-26 16:09:07 +08:00
e28117b578 fix qrmaster 2025-02-26 16:04:42 +08:00
8cf3fd9b14 request & Qr 2025-02-26 15:04:08 +08:00
31254a6460 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-02-26 11:55:05 +08:00
2cdf176620 update item Movement user 2025-02-26 11:54:53 +08:00
79fb9649d7 update qrmaster and itemmovement table 2025-02-26 11:54:07 +08:00
ea4666690e Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-02-25 16:24:34 +08:00
43962646bb update qr scanner 2025-02-25 16:24:27 +08:00
ca46776473 fix itemmovementmaster
update qrmaster
2025-02-25 16:23:45 +08:00
0f2b065c66 update & fix request 2025-02-24 21:53:19 +08:00
9214a081d3 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-02-24 16:32:55 +08:00
646136aade update Qr 2025-02-24 16:32:17 +08:00
1c4e1a32c6 update item request inv master 2025-02-24 16:27:41 +08:00
531106d90d duplicate qr scanner for user and master 2025-02-21 16:24:41 +08:00
92ce4b6267 pull update 2025-02-21 16:19:53 +08:00
4d517fdd6c update 2025-02-21 16:17:19 +08:00
8b7a4d5390 update qr 2025-02-21 16:15:29 +08:00
a6572b6b22 updatee request 2025-02-21 16:11:16 +08:00
e478637fba update request 2025-02-21 07:42:03 +08:00
d8850efed7 Merge branch 'Dev2' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev2 2025-02-20 20:08:34 +08:00
1e1b65dce6 updatess 2025-02-20 20:08:03 +08:00
85fce16dbe item movement admin 2025-02-20 16:33:09 +08:00
d9e67e6139 item request api 2025-02-20 07:42:06 +08:00
5f4e8c6c22 initial itemmovement and initial itemrequest 2025-02-19 14:08:16 +08:00
f8ef2b449c update station and request 2025-02-19 12:11:22 +08:00
9b31a50115 Update 2025-01-24 16:29:47 +08:00
d7ea028d82 Update 2025-01-24 10:15:34 +08:00
9324f61d05 Update 2025-01-13 16:20:41 +08:00
d1682750dc Update 2025-01-13 16:20:13 +08:00
38c4629302 Update 2025-01-09 15:26:05 +08:00
6e9ec353b9 Update 2025-01-09 14:42:47 +08:00
0d511921e8 Update 2025-01-07 10:16:22 +08:00
0bf343b65d Update 2025-01-06 16:31:22 +08:00
52421e9693 Update 2024-12-30 16:32:16 +08:00
5576acc67d Update 2024-12-27 16:25:50 +08:00
5a786d221d update 2024-12-26 16:30:28 +08:00
082be76c51 Update 2024-12-24 16:30:20 +08:00
292f516e33 Update 2024-12-23 16:30:34 +08:00
8ddf9752f4 Update 2024-12-23 08:46:22 +08:00
b5d3829457 Update 2024-12-19 16:30:59 +08:00
a1e2bf6ae0 Update 2024-12-18 14:37:58 +08:00
3610536233 Update 2024-12-17 16:30:35 +08:00
3cfcfd7a3a Update 2024-12-13 16:30:09 +08:00
45a94c99fc Update 2024-12-12 14:30:49 +08:00
970ece5602 Update 2024-12-10 16:25:59 +08:00
5db84d96a2 update 2024-12-10 15:27:21 +08:00
7cbc870f78 Update 2024-12-09 16:28:42 +08:00
907a171616 Update 2024-12-06 16:30:48 +08:00
8135725180 Update 2024-12-05 16:11:52 +08:00
391a359a9f Update 2024-12-04 16:29:41 +08:00
348206d306 Update 2024-12-02 09:39:54 +08:00
961b8c6db5 Update and merge 2024-12-02 09:39:41 +08:00
cf714b92f6 Update 2024-11-29 15:58:32 +08:00
18d053800a Update 2024-11-28 16:29:46 +08:00
fd34ac4822 Update 2024-11-27 16:30:29 +08:00
0ee2817376 Update 2024-11-27 15:52:26 +08:00
0168aa1929 Update 2024-11-26 16:30:40 +08:00
caac6c0d39 Merge branch 'Dev' of https://git.pstw.com.my/catalyx/PSTW_CentralizeSystem into Dev 2024-11-26 12:48:28 +08:00
05a07ca5ea Update 2024-11-26 12:48:14 +08:00
72dbbde075 Update 2024-11-26 12:33:06 +08:00
b378c73152 update 2024-11-26 12:31:45 +08:00
09fa8fc604 reversed pull 2024-11-26 11:40:53 +08:00
d7e93a98b3 Update 2024-11-26 10:50:50 +08:00
87c447e935 add model 2024-11-26 09:48:19 +08:00
b6b8b4a705 Update 2024-11-25 16:31:24 +08:00
630a3175ce update module & roless 2024-11-22 16:25:41 +08:00
5910e4f15f Update 2024-11-22 14:23:30 +08:00
5569a72e68 Update 2024-11-22 10:10:54 +08:00
54b2affe28 Update 2024-11-21 16:32:12 +08:00
3f3ad980b9 Update 2024-11-20 16:27:35 +08:00
8dc8dd90da Update 2024-11-15 16:30:43 +08:00
f51c8ffe65 Update 2024-11-15 16:29:52 +08:00
1391 changed files with 780388 additions and 883 deletions

View File

@ -0,0 +1,5 @@
{
"version": 1,
"isRoot": true,
"tools": {}
}

View File

@ -0,0 +1,83 @@
@page
@model AccessDeniedModel
@{
ViewData["Title"] = "Access denied";
@inject UserManager<UserModel> _userManager
var user = await _userManager.GetUserAsync(User);
if (user != null)
{
var userComDept = user.departmentId;
var userRole = await _userManager.GetRolesAsync(user);
}
}
<header id="deniedHeader">
<template v-if="ldapUserInfo.role.length == 0"><p class="text-danger">You do not have access to this resource because you have no role. Please contact the system administrator.</p></template>
<template v-else><p class="text-danger">You do not have access to this resource.</p></template>
</header>
@section Scripts {
<script>
if (typeof jQuery === 'undefined') {
console.error('jQuery is not loaded.');
}
$(function () {
app.mount('#deniedHeader');
});
const app = Vue.createApp({
data() {
return {
ldapUserInfo: {
role: [],
},
};
},
mounted() {
this.getUserInfo();
},
watch: {
},
methods: {
async getUserInfo() {
try {
// Show the loading modal
$('#loadingModal').modal('show');
// Perform the fetch request
const response = await fetch('/IdentityAPI/GetUserInformation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
// Check if the response is OK
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message);
}
const data = await response.json();
if (data.userInfo) {
console.log(data.userInfo)
this.ldapUserInfo = data.userInfo
} else {
console.error('Get user failed:', data);
}
}
catch (error) {
console.error('Error getting user information:', error);
}
finally {
await new Promise(resolve => {
$('#loadingModal').on('shown.bs.modal', resolve);
});
$('#loadingModal').modal('hide');
}
},
},
});
</script>
}

View File

@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class AccessDeniedModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,8 @@
@page
@model ConfirmEmailModel
@{
ViewData["Title"] = "Confirm email";
}
<h1>@ViewData["Title"]</h1>
<partial name="_StatusMessage" model="Model.StatusMessage" />

View File

@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
public class ConfirmEmailModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
public ConfirmEmailModel(UserManager<UserModel> userManager)
{
_userManager = userManager;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
public async Task<IActionResult> OnGetAsync(string userId, string code)
{
if (userId == null || code == null)
{
return RedirectToPage("/Index");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return NotFound($"Unable to load user with ID '{userId}'.");
}
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
var result = await _userManager.ConfirmEmailAsync(user, code);
StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
return Page();
}
}
}

View File

@ -0,0 +1,8 @@
@page
@model ConfirmEmailChangeModel
@{
ViewData["Title"] = "Confirm email change";
}
<h1>@ViewData["Title"]</h1>
<partial name="_StatusMessage" model="Model.StatusMessage" />

View File

@ -0,0 +1,70 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
public class ConfirmEmailChangeModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly SignInManager<UserModel> _signInManager;
public ConfirmEmailChangeModel(UserManager<UserModel> userManager, SignInManager<UserModel> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
public async Task<IActionResult> OnGetAsync(string userId, string email, string code)
{
if (userId == null || email == null || code == null)
{
return RedirectToPage("/Index");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return NotFound($"Unable to load user with ID '{userId}'.");
}
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
var result = await _userManager.ChangeEmailAsync(user, email, code);
if (!result.Succeeded)
{
StatusMessage = "Error changing email.";
return Page();
}
// In our UI email and user name are one and the same, so when we update the email
// we need to update the user name.
var setUserNameResult = await _userManager.SetUserNameAsync(user, email);
if (!setUserNameResult.Succeeded)
{
StatusMessage = "Error changing user name.";
return Page();
}
await _signInManager.RefreshSignInAsync(user);
StatusMessage = "Thank you for confirming your email change.";
return Page();
}
}
}

View File

@ -0,0 +1,33 @@
@page
@model ExternalLoginModel
@{
ViewData["Title"] = "Register";
}
<h1>@ViewData["Title"]</h1>
<h2 id="external-login-title">Associate your @Model.ProviderDisplayName account.</h2>
<hr />
<p id="external-login-description" class="text-info">
You've successfully authenticated with <strong>@Model.ProviderDisplayName</strong>.
Please enter an email address for this site below and click the Register button to finish
logging in.
</p>
<div class="row">
<div class="col-md-4">
<form asp-page-handler="Confirmation" asp-route-returnUrl="@Model.ReturnUrl" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.Email" class="form-control" autocomplete="email" placeholder="Please enter your email."/>
<label asp-for="Input.Email" class="form-label"></label>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,224 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class ExternalLoginModel : PageModel
{
private readonly SignInManager<UserModel> _signInManager;
private readonly UserManager<UserModel> _userManager;
private readonly IUserStore<UserModel> _userStore;
private readonly IUserEmailStore<UserModel> _emailStore;
private readonly IEmailSender _emailSender;
private readonly ILogger<ExternalLoginModel> _logger;
public ExternalLoginModel(
SignInManager<UserModel> signInManager,
UserManager<UserModel> userManager,
IUserStore<UserModel> userStore,
ILogger<ExternalLoginModel> logger,
IEmailSender emailSender)
{
_signInManager = signInManager;
_userManager = userManager;
_userStore = userStore;
_emailStore = GetEmailStore();
_logger = logger;
_emailSender = emailSender;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string ProviderDisplayName { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string ReturnUrl { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string ErrorMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }
}
public IActionResult OnGet() => RedirectToPage("./Login");
public IActionResult OnPost(string provider, string returnUrl = null)
{
// Request a redirect to the external login provider.
var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return new ChallengeResult(provider, properties);
}
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
ErrorMessage = "Error loading external login information.";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation("{CompanyName} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
return LocalRedirect(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToPage("./Lockout");
}
else
{
// If the user does not have an account, then ask the user to create an account.
ReturnUrl = returnUrl;
ProviderDisplayName = info.ProviderDisplayName;
if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
{
Input = new InputModel
{
Email = info.Principal.FindFirstValue(ClaimTypes.Email)
};
}
return Page();
}
}
public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
ErrorMessage = "Error loading external login information during confirmation.";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
if (ModelState.IsValid)
{
var user = CreateUser();
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
_logger.LogInformation("User created an account using {CompanyName} provider.", info.LoginProvider);
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { area = "Identity", userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
// If account confirmation is required, we need to show the link if we don't have a real email sender
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
}
await _signInManager.SignInAsync(user, isPersistent: false, info.LoginProvider);
return LocalRedirect(returnUrl);
}
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
ProviderDisplayName = info.ProviderDisplayName;
ReturnUrl = returnUrl;
return Page();
}
private UserModel CreateUser()
{
try
{
return Activator.CreateInstance<UserModel>();
}
catch
{
throw new InvalidOperationException($"Can't create an instance of '{nameof(UserModel)}'. " +
$"Ensure that '{nameof(UserModel)}' is not an abstract class and has a parameterless constructor, or alternatively " +
$"override the external login page in /Areas/Identity/Pages/Account/ExternalLogin.cshtml");
}
}
private IUserEmailStore<UserModel> GetEmailStore()
{
if (!_userManager.SupportsUserEmail)
{
throw new NotSupportedException("The default UI requires a user store with email support.");
}
return (IUserEmailStore<UserModel>)_userStore;
}
}
}

View File

@ -0,0 +1,25 @@
@page
@model ForgotPasswordModel
@{
ViewData["Title"] = "Forgot your password?";
}
<h2>Enter your email.</h2>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label asp-for="Input.Email" class="form-label"></label>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset Password</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,85 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
public class ForgotPasswordModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly IEmailSender _emailSender;
public ForgotPasswordModel(UserManager<UserModel> userManager, IEmailSender emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }
}
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
var user = await _userManager.FindByEmailAsync(Input.Email);
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed
return RedirectToPage("./ForgotPasswordConfirmation");
}
// For more information on how to enable account confirmation and password reset please
// visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page(
"/Account/ResetPassword",
pageHandler: null,
values: new { area = "Identity", code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
Input.Email,
"Reset Password",
$"Please reset your password by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
return RedirectToPage("./ForgotPasswordConfirmation");
}
return Page();
}
}
}

View File

@ -0,0 +1,9 @@
@page
@model ForgotPasswordConfirmation
@{
ViewData["Title"] = "Forgot password confirmation";
}
<p>
Please check your email to reset your password.
</p>

View File

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[AllowAnonymous]
public class ForgotPasswordConfirmation : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,10 @@
@page
@model LockoutModel
@{
ViewData["Title"] = "Locked out";
}
<header>
<h1 class="text-danger">@ViewData["Title"]</h1>
<p class="text-danger">This account has been locked out, please try again later.</p>
</header>

View File

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[AllowAnonymous]
public class LockoutModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,188 @@
@page
@model LoginModel
@{
ViewData["Title"] = "Log in";
}
<div class="row" id="systemLogin">
<div class="row">
<h2><label class="col-md-2">Login Type</label></h2>
<div class="btn-group col-md-4" role="group" aria-label="Login type">
<input type="radio" class="btn-check" name="loginType" id="local-login" value="Local" v-model="loginType">
<label class="btn btn-outline-primary" for="local-login">Local</label>
<input type="radio" class="btn-check" name="loginType" id="ad-login" value="AD" v-model="loginType" checked>
<label class="btn btn-outline-primary" for="ad-login">AD</label>
</div>
</div>
<div class="row">
<div class="col-md-4" v-if="loginType == 'Local'">
<form id="account" method="post">
<h2>Use a local account to log in.</h2>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label asp-for="Input.Email" class="form-label">Email</label>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password" />
<label asp-for="Input.Password" class="form-label">Password</label>
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div>
<button id="login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</div>
</form>
</div>
<div class="col-md-4" v-if="loginType == 'AD'">
<form v-on:submit.prevent="ldapLogin" id="login" method="post">
<h2>Use a AD account to log in.</h2>
<hr />
<div class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input v-model="ldapLoginInfo.username" id="ldapUsername" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label id="ldapEmailLabel" class="form-label">Windows Login</label>
<span id="ldapEmailError" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input v-model="ldapLoginInfo.password" id="ldapPassword" class="form-control" type="password" autocomplete="current-password" aria-required="true" placeholder="password" />
<label id="ldapPasswordLabel" class="form-label">Password</label>
<span id="ldapPasswordError" class="text-danger"></span>
</div>
<div>
<button id="ldap-login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</div>
</form>
</div>
<div class="col-md-6 col-md-offset-2">
<section>
<h3>Use another service to log in.</h3>
<hr />
@{
if ((Model.ExternalLogins?.Count ?? 0) == 0)
{
<div>
<p>
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
about setting up this ASP.NET application to support logging in via external services</a>.
</p>
</div>
}
else
{
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div>
<p>
@foreach (var provider in Model.ExternalLogins!)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
</p>
</div>
</form>
}
}
</section>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
$(function () {
app.mount('#systemLogin');
$('.closeModal').on('click', function () {
$('.modal').modal('hide');
});
});
const app = Vue.createApp({
data() {
return {
loginType: 'AD',
ldapLoginInfo: {
username: '',
password: '',
},
};
},
mounted() {
},
watch: {
},
methods: {
async ldapLogin() {
try {
// Show the loading modal
$('#loadingModal').modal('show');
// Perform the fetch request
const response = await fetch('/IdentityAPI/LdapLogin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(this.ldapLoginInfo),
});
// Check if the response is OK
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message);
}
const data = await response.json();
// Redirect if a URL is provided
if (data.redirectUrl) {
window.location.href = data.redirectUrl;
} else {
console.error('Login failed:', data);
alert('Login failed.');
}
}
catch (error) {
console.error('Error during LDAP login:', error);
alert(error.message);
}
finally {
await new Promise(resolve => {
$('#loadingModal').on('shown.bs.modal', resolve);
});
$('#loadingModal').modal('hide');
}
},
async fetchControllerMethodList() {
try {
const response = await fetch('/AdminAPI/GetListClassAndMethodInformation', {
method: 'POST',
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
// Assign data if it exists
if (data) {
this.controllerMethodData = data;
}
} catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
},
});
</script>
}

View File

@ -0,0 +1,141 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
public class LoginModel : PageModel
{
private readonly SignInManager<UserModel> _signInManager;
private readonly ILogger<LoginModel> _logger;
public LoginModel(SignInManager<UserModel> signInManager, ILogger<LoginModel> logger)
{
_signInManager = signInManager;
_logger = logger;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public IList<AuthenticationScheme> ExternalLogins { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string ReturnUrl { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string ErrorMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
public async Task OnGetAsync(string returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
{
ModelState.AddModelError(string.Empty, ErrorMessage);
}
returnUrl ??= Url.Content("~/");
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
// If we got this far, something failed, redisplay form
return Page();
}
}
}

View File

@ -0,0 +1,39 @@
@page
@model LoginWith2faModel
@{
ViewData["Title"] = "Two-factor authentication";
}
<h1>@ViewData["Title"]</h1>
<hr />
<p>Your login is protected with an authenticator app. Enter your authenticator code below.</p>
<div class="row">
<div class="col-md-4">
<form method="post" asp-route-returnUrl="@Model.ReturnUrl">
<input asp-for="RememberMe" type="hidden" />
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.TwoFactorCode" class="form-control" autocomplete="off" />
<label asp-for="Input.TwoFactorCode" class="form-label"></label>
<span asp-validation-for="Input.TwoFactorCode" class="text-danger"></span>
</div>
<div class="checkbox mb-3">
<label asp-for="Input.RememberMachine" class="form-label">
<input asp-for="Input.RememberMachine" />
@Html.DisplayNameFor(m => m.Input.RememberMachine)
</label>
</div>
<div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</div>
</form>
</div>
</div>
<p>
Don't have access to your authenticator device? You can
<a id="recovery-code-login" asp-page="./LoginWithRecoveryCode" asp-route-returnUrl="@Model.ReturnUrl">log in with a recovery code</a>.
</p>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,132 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
public class LoginWith2faModel : PageModel
{
private readonly SignInManager<UserModel> _signInManager;
private readonly UserManager<UserModel> _userManager;
private readonly ILogger<LoginWith2faModel> _logger;
public LoginWith2faModel(
SignInManager<UserModel> signInManager,
UserManager<UserModel> userManager,
ILogger<LoginWith2faModel> logger)
{
_signInManager = signInManager;
_userManager = userManager;
_logger = logger;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool RememberMe { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string ReturnUrl { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Authenticator code")]
public string TwoFactorCode { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Display(Name = "Remember this machine")]
public bool RememberMachine { get; set; }
}
public async Task<IActionResult> OnGetAsync(bool rememberMe, string returnUrl = null)
{
// Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
}
ReturnUrl = returnUrl;
RememberMe = rememberMe;
return Page();
}
public async Task<IActionResult> OnPostAsync(bool rememberMe, string returnUrl = null)
{
if (!ModelState.IsValid)
{
return Page();
}
returnUrl = returnUrl ?? Url.Content("~/");
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
}
var authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine);
var userId = await _userManager.GetUserIdAsync(user);
if (result.Succeeded)
{
_logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id);
return LocalRedirect(returnUrl);
}
else if (result.IsLockedOut)
{
_logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id);
return RedirectToPage("./Lockout");
}
else
{
_logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return Page();
}
}
}
}

View File

@ -0,0 +1,29 @@
@page
@model LoginWithRecoveryCodeModel
@{
ViewData["Title"] = "Recovery code verification";
}
<h1>@ViewData["Title"]</h1>
<hr />
<p>
You have requested to log in with a recovery code. This login will not be remembered until you provide
an authenticator app code at log in or disable 2FA and log in again.
</p>
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.RecoveryCode" class="form-control" autocomplete="off" placeholder="RecoveryCode" />
<label asp-for="Input.RecoveryCode" class="form-label"></label>
<span asp-validation-for="Input.RecoveryCode" class="text-danger"></span>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,113 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
public class LoginWithRecoveryCodeModel : PageModel
{
private readonly SignInManager<UserModel> _signInManager;
private readonly UserManager<UserModel> _userManager;
private readonly ILogger<LoginWithRecoveryCodeModel> _logger;
public LoginWithRecoveryCodeModel(
SignInManager<UserModel> signInManager,
UserManager<UserModel> userManager,
ILogger<LoginWithRecoveryCodeModel> logger)
{
_signInManager = signInManager;
_userManager = userManager;
_logger = logger;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string ReturnUrl { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
[Required]
[DataType(DataType.Text)]
[Display(Name = "Recovery Code")]
public string RecoveryCode { get; set; }
}
public async Task<IActionResult> OnGetAsync(string returnUrl = null)
{
// Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
}
ReturnUrl = returnUrl;
return Page();
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
}
var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty);
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
var userId = await _userManager.GetUserIdAsync(user);
if (result.Succeeded)
{
_logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id);
return LocalRedirect(returnUrl ?? Url.Content("~/"));
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
_logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id);
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
return Page();
}
}
}
}

View File

@ -0,0 +1,21 @@
@page
@model LogoutModel
@{
ViewData["Title"] = "Log out";
}
<header>
<h1>@ViewData["Title"]</h1>
@{
if (User.Identity?.IsAuthenticated ?? false)
{
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
<button type="submit" class="nav-link btn btn-link text-dark">Click here to Logout</button>
</form>
}
else
{
<p>You have successfully logged out of the application.</p>
}
}
</header>

View File

@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
public class LogoutModel : PageModel
{
private readonly SignInManager<UserModel> _signInManager;
private readonly ILogger<LogoutModel> _logger;
public LogoutModel(SignInManager<UserModel> signInManager, ILogger<LogoutModel> logger)
{
_signInManager = signInManager;
_logger = logger;
}
public async Task<IActionResult> OnPost(string returnUrl = null)
{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
if (returnUrl != null)
{
return LocalRedirect(returnUrl);
}
else
{
// This needs to be a redirect so that the browser performs a new
// request and the identity for the user gets updated.
return RedirectToPage();
}
}
}
}

View File

@ -0,0 +1,36 @@
@page
@model ChangePasswordModel
@{
ViewData["Title"] = "Change password";
ViewData["ActivePage"] = ManageNavPages.ChangePassword;
}
<h3>@ViewData["Title"]</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<form id="change-password-form" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.OldPassword" class="form-control" autocomplete="current-password" aria-required="true" placeholder="Please enter your old password." />
<label asp-for="Input.OldPassword" class="form-label"></label>
<span asp-validation-for="Input.OldPassword" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.NewPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please enter your new password." />
<label asp-for="Input.NewPassword" class="form-label"></label>
<span asp-validation-for="Input.NewPassword" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please confirm your new password."/>
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Update password</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,128 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class ChangePasswordModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly SignInManager<UserModel> _signInManager;
private readonly ILogger<ChangePasswordModel> _logger;
public ChangePasswordModel(
UserManager<UserModel> userManager,
SignInManager<UserModel> signInManager,
ILogger<ChangePasswordModel> logger)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[DataType(DataType.Password)]
[Display(Name = "Current password")]
public string OldPassword { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
public async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var hasPassword = await _userManager.HasPasswordAsync(user);
if (!hasPassword)
{
return RedirectToPage("./SetPassword");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
if (!changePasswordResult.Succeeded)
{
foreach (var error in changePasswordResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return Page();
}
await _signInManager.RefreshSignInAsync(user);
_logger.LogInformation("User changed their password successfully.");
StatusMessage = "Your password has been changed.";
return RedirectToPage();
}
}
}

View File

@ -0,0 +1,33 @@
@page
@model DeletePersonalDataModel
@{
ViewData["Title"] = "Delete Personal Data";
ViewData["ActivePage"] = ManageNavPages.PersonalData;
}
<h3>@ViewData["Title"]</h3>
<div class="alert alert-warning" role="alert">
<p>
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
</p>
</div>
<div>
<form id="delete-user" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
@if (Model.RequirePassword)
{
<div class="form-floating mb-3">
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="Please enter your password." />
<label asp-for="Input.Password" class="form-label"></label>
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
}
<button class="w-100 btn btn-lg btn-danger" type="submit">Delete data and close my account</button>
</form>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,104 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class DeletePersonalDataModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly SignInManager<UserModel> _signInManager;
private readonly ILogger<DeletePersonalDataModel> _logger;
public DeletePersonalDataModel(
UserManager<UserModel> userManager,
SignInManager<UserModel> signInManager,
ILogger<DeletePersonalDataModel> logger)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool RequirePassword { get; set; }
public async Task<IActionResult> OnGet()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
RequirePassword = await _userManager.HasPasswordAsync(user);
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
RequirePassword = await _userManager.HasPasswordAsync(user);
if (RequirePassword)
{
if (!await _userManager.CheckPasswordAsync(user, Input.Password))
{
ModelState.AddModelError(string.Empty, "Incorrect password.");
return Page();
}
}
var result = await _userManager.DeleteAsync(user);
var userId = await _userManager.GetUserIdAsync(user);
if (!result.Succeeded)
{
throw new InvalidOperationException($"Unexpected error occurred deleting user.");
}
await _signInManager.SignOutAsync();
_logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
return Redirect("~/");
}
}
}

View File

@ -0,0 +1,25 @@
@page
@model Disable2faModel
@{
ViewData["Title"] = "Disable two-factor authentication (2FA)";
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
}
<partial name="_StatusMessage" for="StatusMessage" />
<h3>@ViewData["Title"]</h3>
<div class="alert alert-warning" role="alert">
<p>
<strong>This action only disables 2FA.</strong>
</p>
<p>
Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key
used in an authenticator app you should <a asp-page="./ResetAuthenticator">reset your authenticator keys.</a>
</p>
</div>
<div>
<form method="post">
<button class="btn btn-danger" type="submit">Disable 2FA</button>
</form>
</div>

View File

@ -0,0 +1,70 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class Disable2faModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly ILogger<Disable2faModel> _logger;
public Disable2faModel(
UserManager<UserModel> userManager,
ILogger<Disable2faModel> logger)
{
_userManager = userManager;
_logger = logger;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
public async Task<IActionResult> OnGet()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!await _userManager.GetTwoFactorEnabledAsync(user))
{
throw new InvalidOperationException($"Cannot disable 2FA for user as it's not currently enabled.");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new InvalidOperationException($"Unexpected error occurred disabling 2FA.");
}
_logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User));
StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app";
return RedirectToPage("./TwoFactorAuthentication");
}
}
}

View File

@ -0,0 +1,12 @@
@page
@model DownloadPersonalDataModel
@{
ViewData["Title"] = "Download Your Data";
ViewData["ActivePage"] = ManageNavPages.PersonalData;
}
<h3>@ViewData["Title"]</h3>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,68 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class DownloadPersonalDataModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly ILogger<DownloadPersonalDataModel> _logger;
public DownloadPersonalDataModel(
UserManager<UserModel> userManager,
ILogger<DownloadPersonalDataModel> logger)
{
_userManager = userManager;
_logger = logger;
}
public IActionResult OnGet()
{
return NotFound();
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
_logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User));
// Only include personal data for download
var personalData = new Dictionary<string, string>();
var personalDataProps = typeof(UserModel).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
foreach (var p in personalDataProps)
{
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
}
var logins = await _userManager.GetLoginsAsync(user);
foreach (var l in logins)
{
personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey);
}
personalData.Add($"Authenticator Key", await _userManager.GetAuthenticatorKeyAsync(user));
Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json");
return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json");
}
}
}

View File

@ -0,0 +1,44 @@
@page
@model EmailModel
@{
ViewData["Title"] = "Manage Email";
ViewData["ActivePage"] = ManageNavPages.Email;
}
<h3>@ViewData["Title"]</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<form id="email-form" method="post">
<div asp-validation-summary="All" class="text-danger" role="alert"></div>
@if (Model.IsEmailConfirmed)
{
<div class="form-floating mb-3 input-group">
<input asp-for="Email" class="form-control" placeholder="Please enter your email." disabled />
<div class="input-group-append">
<span class="h-100 input-group-text text-success font-weight-bold">✓</span>
</div>
<label asp-for="Email" class="form-label"></label>
</div>
}
else
{
<div class="form-floating mb-3">
<input asp-for="Email" class="form-control" placeholder="Please enter your email." disabled />
<label asp-for="Email" class="form-label"></label>
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
</div>
}
<div class="form-floating mb-3">
<input asp-for="Input.NewEmail" class="form-control" autocomplete="email" aria-required="true" placeholder="Please enter new email." />
<label asp-for="Input.NewEmail" class="form-label"></label>
<span asp-validation-for="Input.NewEmail" class="text-danger"></span>
</div>
<button id="change-email-button" type="submit" asp-page-handler="ChangeEmail" class="w-100 btn btn-lg btn-primary">Change email</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,172 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class EmailModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly SignInManager<UserModel> _signInManager;
private readonly IEmailSender _emailSender;
public EmailModel(
UserManager<UserModel> userManager,
SignInManager<UserModel> signInManager,
IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool IsEmailConfirmed { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
[Display(Name = "New email")]
public string NewEmail { get; set; }
}
private async Task LoadAsync(UserModel user)
{
var email = await _userManager.GetEmailAsync(user);
Email = email;
Input = new InputModel
{
NewEmail = email,
};
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
}
public async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await LoadAsync(user);
return Page();
}
public async Task<IActionResult> OnPostChangeEmailAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!ModelState.IsValid)
{
await LoadAsync(user);
return Page();
}
var email = await _userManager.GetEmailAsync(user);
if (Input.NewEmail != email)
{
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page(
"/Account/ConfirmEmailChange",
pageHandler: null,
values: new { area = "Identity", userId = userId, email = Input.NewEmail, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
Input.NewEmail,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Confirmation link to change email sent. Please check your email.";
return RedirectToPage();
}
StatusMessage = "Your email is unchanged.";
return RedirectToPage();
}
public async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!ModelState.IsValid)
{
await LoadAsync(user);
return Page();
}
var userId = await _userManager.GetUserIdAsync(user);
var email = await _userManager.GetEmailAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { area = "Identity", userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
}
}
}

View File

@ -0,0 +1,53 @@
@page
@model EnableAuthenticatorModel
@{
ViewData["Title"] = "Configure authenticator app";
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
}
<partial name="_StatusMessage" for="StatusMessage" />
<h3>@ViewData["Title"]</h3>
<div>
<p>To use an authenticator app go through the following steps:</p>
<ol class="list">
<li>
<p>
Download a two-factor authenticator app like Microsoft Authenticator for
<a href="https://go.microsoft.com/fwlink/?Linkid=825072">Android</a> and
<a href="https://go.microsoft.com/fwlink/?Linkid=825073">iOS</a> or
Google Authenticator for
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&amp;hl=en">Android</a> and
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">iOS</a>.
</p>
</li>
<li>
<p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
<div class="alert alert-info">Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423">enable QR code generation</a>.</div>
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Model.AuthenticatorUri"></div>
</li>
<li>
<p>
Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
with a unique code. Enter the code in the confirmation box below.
</p>
<div class="row">
<div class="col-md-6">
<form id="send-code" method="post">
<div class="form-floating mb-3">
<input asp-for="Input.Code" class="form-control" autocomplete="off" placeholder="Please enter the code."/>
<label asp-for="Input.Code" class="control-label form-label">Verification Code</label>
<span asp-validation-for="Input.Code" class="text-danger"></span>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Verify</button>
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
</form>
</div>
</div>
</li>
</ol>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,189 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class EnableAuthenticatorModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly ILogger<EnableAuthenticatorModel> _logger;
private readonly UrlEncoder _urlEncoder;
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
public EnableAuthenticatorModel(
UserManager<UserModel> userManager,
ILogger<EnableAuthenticatorModel> logger,
UrlEncoder urlEncoder)
{
_userManager = userManager;
_logger = logger;
_urlEncoder = urlEncoder;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string AuthenticatorUri { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string[] RecoveryCodes { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Verification Code")]
public string Code { get; set; }
}
public async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await LoadSharedKeyAndQrCodeUriAsync(user);
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!ModelState.IsValid)
{
await LoadSharedKeyAndQrCodeUriAsync(user);
return Page();
}
// Strip spaces and hyphens
var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
ModelState.AddModelError("Input.Code", "Verification code is invalid.");
await LoadSharedKeyAndQrCodeUriAsync(user);
return Page();
}
await _userManager.SetTwoFactorEnabledAsync(user, true);
var userId = await _userManager.GetUserIdAsync(user);
_logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId);
StatusMessage = "Your authenticator app has been verified.";
if (await _userManager.CountRecoveryCodesAsync(user) == 0)
{
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
RecoveryCodes = recoveryCodes.ToArray();
return RedirectToPage("./ShowRecoveryCodes");
}
else
{
return RedirectToPage("./TwoFactorAuthentication");
}
}
private async Task LoadSharedKeyAndQrCodeUriAsync(UserModel user)
{
// Load the authenticator key & QR code URI to display on the form
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
SharedKey = FormatKey(unformattedKey);
var email = await _userManager.GetEmailAsync(user);
AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey);
}
private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' ');
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.AsSpan(currentPosition));
}
return result.ToString().ToLowerInvariant();
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
CultureInfo.InvariantCulture,
AuthenticatorUriFormat,
_urlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"),
_urlEncoder.Encode(email),
unformattedKey);
}
}
}

View File

@ -0,0 +1,53 @@
@page
@model ExternalLoginsModel
@{
ViewData["Title"] = "Manage your external logins";
ViewData["ActivePage"] = ManageNavPages.ExternalLogins;
}
<partial name="_StatusMessage" for="StatusMessage" />
@if (Model.CurrentLogins?.Count > 0)
{
<h3>Registered Logins</h3>
<table class="table">
<tbody>
@foreach (var login in Model.CurrentLogins)
{
<tr>
<td id="@($"login-provider-{login.LoginProvider}")">@login.ProviderDisplayName</td>
<td>
@if (Model.ShowRemoveButton)
{
<form id="@($"remove-login-{login.LoginProvider}")" asp-page-handler="RemoveLogin" method="post">
<div>
<input asp-for="@login.LoginProvider" name="LoginProvider" type="hidden" />
<input asp-for="@login.ProviderKey" name="ProviderKey" type="hidden" />
<button type="submit" class="btn btn-primary" title="Remove this @login.ProviderDisplayName login from your account">Remove</button>
</div>
</form>
}
else
{
@: &nbsp;
}
</td>
</tr>
}
</tbody>
</table>
}
@if (Model.OtherLogins?.Count > 0)
{
<h4>Add another service to log in.</h4>
<hr />
<form id="link-login-form" asp-page-handler="LinkLogin" method="post" class="form-horizontal">
<div id="socialLoginList">
<p>
@foreach (var provider in Model.OtherLogins)
{
<button id="@($"link-login-button-{provider.Name}")" type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
</p>
</div>
</form>
}

View File

@ -0,0 +1,142 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class ExternalLoginsModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly SignInManager<UserModel> _signInManager;
private readonly IUserStore<UserModel> _userStore;
public ExternalLoginsModel(
UserManager<UserModel> userManager,
SignInManager<UserModel> signInManager,
IUserStore<UserModel> userStore)
{
_userManager = userManager;
_signInManager = signInManager;
_userStore = userStore;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public IList<UserLoginInfo> CurrentLogins { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public IList<AuthenticationScheme> OtherLogins { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool ShowRemoveButton { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
public async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
CurrentLogins = await _userManager.GetLoginsAsync(user);
OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
.Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider))
.ToList();
string passwordHash = null;
if (_userStore is IUserPasswordStore<UserModel> userPasswordStore)
{
passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted);
}
ShowRemoveButton = passwordHash != null || CurrentLogins.Count > 1;
return Page();
}
public async Task<IActionResult> OnPostRemoveLoginAsync(string loginProvider, string providerKey)
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey);
if (!result.Succeeded)
{
StatusMessage = "The external login was not removed.";
return RedirectToPage();
}
await _signInManager.RefreshSignInAsync(user);
StatusMessage = "The external login was removed.";
return RedirectToPage();
}
public async Task<IActionResult> OnPostLinkLoginAsync(string provider)
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
// Request a redirect to the external login provider to link a login for the current user
var redirectUrl = Url.Page("./ExternalLogins", pageHandler: "LinkLoginCallback");
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
return new ChallengeResult(provider, properties);
}
public async Task<IActionResult> OnGetLinkLoginCallbackAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var userId = await _userManager.GetUserIdAsync(user);
var info = await _signInManager.GetExternalLoginInfoAsync(userId);
if (info == null)
{
throw new InvalidOperationException($"Unexpected error occurred loading external login info.");
}
var result = await _userManager.AddLoginAsync(user, info);
if (!result.Succeeded)
{
StatusMessage = "The external login was not added. External logins can only be associated with one account.";
return RedirectToPage();
}
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
StatusMessage = "The external login was added.";
return RedirectToPage();
}
}
}

View File

@ -0,0 +1,27 @@
@page
@model GenerateRecoveryCodesModel
@{
ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes";
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
}
<partial name="_StatusMessage" for="StatusMessage" />
<h3>@ViewData["Title"]</h3>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<strong>Put these codes in a safe place.</strong>
</p>
<p>
If you lose your device and don't have the recovery codes you will lose access to your account.
</p>
<p>
Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key
used in an authenticator app you should <a asp-page="./ResetAuthenticator">reset your authenticator keys.</a>
</p>
</div>
<div>
<form method="post">
<button class="btn btn-danger" type="submit">Generate Recovery Codes</button>
</form>
</div>

View File

@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class GenerateRecoveryCodesModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly ILogger<GenerateRecoveryCodesModel> _logger;
public GenerateRecoveryCodesModel(
UserManager<UserModel> userManager,
ILogger<GenerateRecoveryCodesModel> logger)
{
_userManager = userManager;
_logger = logger;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string[] RecoveryCodes { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
public async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
if (!isTwoFactorEnabled)
{
throw new InvalidOperationException($"Cannot generate recovery codes for user because they do not have 2FA enabled.");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
var userId = await _userManager.GetUserIdAsync(user);
if (!isTwoFactorEnabled)
{
throw new InvalidOperationException($"Cannot generate recovery codes for user as they do not have 2FA enabled.");
}
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
RecoveryCodes = recoveryCodes.ToArray();
_logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId);
StatusMessage = "You have generated new recovery codes.";
return RedirectToPage("./ShowRecoveryCodes");
}
}
}

View File

@ -0,0 +1,30 @@
@page
@model IndexModel
@{
ViewData["Title"] = "Profile";
ViewData["ActivePage"] = ManageNavPages.Index;
}
<h3>@ViewData["Title"]</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<form id="profile-form" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Username" class="form-control" placeholder="Please choose your username." disabled />
<label asp-for="Username" class="form-label"></label>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.PhoneNumber" class="form-control" placeholder="Please enter your phone number."/>
<label asp-for="Input.PhoneNumber" class="form-label"></label>
<span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
</div>
<button id="update-profile-button" type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,119 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class IndexModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly SignInManager<UserModel> _signInManager;
public IndexModel(
UserManager<UserModel> userManager,
SignInManager<UserModel> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string Username { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Phone]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
}
private async Task LoadAsync(UserModel user)
{
var userName = await _userManager.GetUserNameAsync(user);
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
Username = userName;
Input = new InputModel
{
PhoneNumber = phoneNumber
};
}
public async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await LoadAsync(user);
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!ModelState.IsValid)
{
await LoadAsync(user);
return Page();
}
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
if (Input.PhoneNumber != phoneNumber)
{
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
if (!setPhoneResult.Succeeded)
{
StatusMessage = "Unexpected error when trying to set phone number.";
return RedirectToPage();
}
}
await _signInManager.RefreshSignInAsync(user);
StatusMessage = "Your profile has been updated";
return RedirectToPage();
}
}
}

View File

@ -0,0 +1,123 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static class ManageNavPages
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string Index => "Index";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string Email => "Email";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string ChangePassword => "ChangePassword";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string DownloadPersonalData => "DownloadPersonalData";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string DeletePersonalData => "DeletePersonalData";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string ExternalLogins => "ExternalLogins";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string PersonalData => "PersonalData";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string PageNavClass(ViewContext viewContext, string page)
{
var activePage = viewContext.ViewData["ActivePage"] as string
?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName);
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
}
}
}

View File

@ -0,0 +1,27 @@
@page
@model PersonalDataModel
@{
ViewData["Title"] = "Personal Data";
ViewData["ActivePage"] = ManageNavPages.PersonalData;
}
<h3>@ViewData["Title"]</h3>
<div class="row">
<div class="col-md-6">
<p>Your account contains personal data that you have given us. This page allows you to download or delete that data.</p>
<p>
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
</p>
<form id="download-data" asp-page="DownloadPersonalData" method="post">
<button class="btn btn-primary" type="submit">Download</button>
</form>
<p>
<a id="delete" asp-page="DeletePersonalData" class="btn btn-danger">Delete</a>
</p>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class PersonalDataModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly ILogger<PersonalDataModel> _logger;
public PersonalDataModel(
UserManager<UserModel> userManager,
ILogger<PersonalDataModel> logger)
{
_userManager = userManager;
_logger = logger;
}
public async Task<IActionResult> OnGet()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
return Page();
}
}
}

View File

@ -0,0 +1,24 @@
@page
@model ResetAuthenticatorModel
@{
ViewData["Title"] = "Reset authenticator key";
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
}
<partial name="_StatusMessage" for="StatusMessage" />
<h3>@ViewData["Title"]</h3>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong>
</p>
<p>
This process disables 2FA until you verify your authenticator app.
If you do not complete your authenticator app configuration you may lose access to your account.
</p>
</div>
<div>
<form id="reset-authenticator-form" method="post">
<button id="reset-authenticator-button" class="btn btn-danger" type="submit">Reset authenticator key</button>
</form>
</div>

View File

@ -0,0 +1,68 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class ResetAuthenticatorModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly SignInManager<UserModel> _signInManager;
private readonly ILogger<ResetAuthenticatorModel> _logger;
public ResetAuthenticatorModel(
UserManager<UserModel> userManager,
SignInManager<UserModel> signInManager,
ILogger<ResetAuthenticatorModel> logger)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
public async Task<IActionResult> OnGet()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await _userManager.SetTwoFactorEnabledAsync(user, false);
await _userManager.ResetAuthenticatorKeyAsync(user);
var userId = await _userManager.GetUserIdAsync(user);
_logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id);
await _signInManager.RefreshSignInAsync(user);
StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.";
return RedirectToPage("./EnableAuthenticator");
}
}
}

View File

@ -0,0 +1,35 @@
@page
@model SetPasswordModel
@{
ViewData["Title"] = "Set password";
ViewData["ActivePage"] = ManageNavPages.ChangePassword;
}
<h3>Set your password</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<p class="text-info">
You do not have a local username/password for this site. Add a local
account so you can log in without an external login.
</p>
<div class="row">
<div class="col-md-6">
<form id="set-password-form" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.NewPassword" class="form-control" autocomplete="new-password" placeholder="Please enter your new password."/>
<label asp-for="Input.NewPassword" class="form-label"></label>
<span asp-validation-for="Input.NewPassword" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" placeholder="Please confirm your new password."/>
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Set password</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,115 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class SetPasswordModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly SignInManager<UserModel> _signInManager;
public SetPasswordModel(
UserManager<UserModel> userManager,
SignInManager<UserModel> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
public async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var hasPassword = await _userManager.HasPasswordAsync(user);
if (hasPassword)
{
return RedirectToPage("./ChangePassword");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword);
if (!addPasswordResult.Succeeded)
{
foreach (var error in addPasswordResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return Page();
}
await _signInManager.RefreshSignInAsync(user);
StatusMessage = "Your password has been set.";
return RedirectToPage();
}
}
}

View File

@ -0,0 +1,25 @@
@page
@model ShowRecoveryCodesModel
@{
ViewData["Title"] = "Recovery codes";
ViewData["ActivePage"] = "TwoFactorAuthentication";
}
<partial name="_StatusMessage" for="StatusMessage" />
<h3>@ViewData["Title"]</h3>
<div class="alert alert-warning" role="alert">
<p>
<strong>Put these codes in a safe place.</strong>
</p>
<p>
If you lose your device and don't have the recovery codes you will lose access to your account.
</p>
</div>
<div class="row">
<div class="col-md-12">
@for (var row = 0; row < Model.RecoveryCodes.Length; row += 2)
{
<code class="recovery-code">@Model.RecoveryCodes[row]</code><text>&nbsp;</text><code class="recovery-code">@Model.RecoveryCodes[row + 1]</code><br />
}
</div>
</div>

View File

@ -0,0 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class ShowRecoveryCodesModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string[] RecoveryCodes { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public IActionResult OnGet()
{
if (RecoveryCodes == null || RecoveryCodes.Length == 0)
{
return RedirectToPage("./TwoFactorAuthentication");
}
return Page();
}
}
}

View File

@ -0,0 +1,71 @@
@page
@using Microsoft.AspNetCore.Http.Features
@model TwoFactorAuthenticationModel
@{
ViewData["Title"] = "Two-factor authentication (2FA)";
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
}
<partial name="_StatusMessage" for="StatusMessage" />
<h3>@ViewData["Title"]</h3>
@{
var consentFeature = HttpContext.Features.Get<ITrackingConsentFeature>();
@if (consentFeature?.CanTrack ?? true)
{
@if (Model.Is2faEnabled)
{
if (Model.RecoveryCodesLeft == 0)
{
<div class="alert alert-danger">
<strong>You have no recovery codes left.</strong>
<p>You must <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
</div>
}
else if (Model.RecoveryCodesLeft == 1)
{
<div class="alert alert-danger">
<strong>You have 1 recovery code left.</strong>
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
</div>
}
else if (Model.RecoveryCodesLeft <= 3)
{
<div class="alert alert-warning">
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
</div>
}
if (Model.IsMachineRemembered)
{
<form method="post" style="display: inline-block">
<button type="submit" class="btn btn-primary">Forget this browser</button>
</form>
}
<a asp-page="./Disable2fa" class="btn btn-primary">Disable 2FA</a>
<a asp-page="./GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
}
<h4>Authenticator app</h4>
@if (!Model.HasAuthenticator)
{
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
}
else
{
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Set up authenticator app</a>
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
}
}
else
{
<div class="alert alert-danger">
<strong>Privacy and cookie policy have not been accepted.</strong>
<p>You must accept the policy before you can enable two factor authentication.</p>
</div>
}
}
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,90 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
{
public class TwoFactorAuthenticationModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly SignInManager<UserModel> _signInManager;
private readonly ILogger<TwoFactorAuthenticationModel> _logger;
public TwoFactorAuthenticationModel(
UserManager<UserModel> userManager, SignInManager<UserModel> signInManager, ILogger<TwoFactorAuthenticationModel> logger)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool HasAuthenticator { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public int RecoveryCodesLeft { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public bool Is2faEnabled { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool IsMachineRemembered { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
public async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null;
Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user);
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user);
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await _signInManager.ForgetTwoFactorClientAsync();
StatusMessage = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.";
return RedirectToPage();
}
}
}

View File

@ -0,0 +1,29 @@
@{
if (ViewData.TryGetValue("ParentLayout", out var parentLayout) && parentLayout != null)
{
Layout = parentLayout.ToString();
}
else
{
Layout = "/Areas/Identity/Pages/_Layout.cshtml";
}
}
<h1>Manage your account</h1>
<div>
<h2>Change your account settings</h2>
<hr />
<div class="row">
<div class="col-md-3">
<partial name="_ManageNav" />
</div>
<div class="col-md-9">
@RenderBody()
</div>
</div>
</div>
@section Scripts {
@RenderSection("Scripts", required: false)
}

View File

@ -0,0 +1,15 @@
@inject SignInManager<UserModel> SignInManager
@{
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
}
<ul class="nav nav-pills flex-column">
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
@if (hasExternalLogins)
{
<li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
}
<li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>
<li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data" asp-page="./PersonalData">Personal data</a></li>
</ul>

View File

@ -0,0 +1,10 @@
@model string
@if (!String.IsNullOrEmpty(Model))
{
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
@Model
</div>
}

View File

@ -0,0 +1 @@
@using PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage

View File

@ -0,0 +1,67 @@
@page
@model RegisterModel
@{
ViewData["Title"] = "Register";
}
<h1>@ViewData["Title"]</h1>
<div class="row">
<div class="col-md-4">
<form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
<h2>Create a new account.</h2>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label asp-for="Input.Email">Email</label>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" />
<label asp-for="Input.Password">Password</label>
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" />
<label asp-for="Input.ConfirmPassword">Confirm Password</label>
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
</form>
</div>
<div class="col-md-6 col-md-offset-2">
<section>
<h3>Use another service to register.</h3>
<hr />
@{
if ((Model.ExternalLogins?.Count ?? 0) == 0)
{
<div>
<p>
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
about setting up this ASP.NET application to support logging in via external services</a>.
</p>
</div>
}
else
{
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div>
<p>
@foreach (var provider in Model.ExternalLogins!)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
</p>
</div>
</form>
}
}
</section>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,182 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
[Authorize]
public class RegisterModel : PageModel
{
private readonly SignInManager<UserModel> _signInManager;
private readonly UserManager<UserModel> _userManager;
private readonly IUserStore<UserModel> _userStore;
private readonly IUserEmailStore<UserModel> _emailStore;
private readonly ILogger<RegisterModel> _logger;
private readonly IEmailSender _emailSender;
public RegisterModel(
UserManager<UserModel> userManager,
IUserStore<UserModel> userStore,
SignInManager<UserModel> signInManager,
ILogger<RegisterModel> logger,
IEmailSender emailSender)
{
_userManager = userManager;
_userStore = userStore;
_emailStore = GetEmailStore();
_signInManager = signInManager;
_logger = logger;
_emailSender = emailSender;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string ReturnUrl { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public IList<AuthenticationScheme> ExternalLogins { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
public async Task OnGetAsync(string returnUrl = null)
{
ReturnUrl = returnUrl;
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (ModelState.IsValid)
{
var user = CreateUser();
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
}
else
{
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
// If we got this far, something failed, redisplay form
return Page();
}
private UserModel CreateUser()
{
try
{
return Activator.CreateInstance<UserModel>();
}
catch
{
throw new InvalidOperationException($"Can't create an instance of '{nameof(UserModel)}'. " +
$"Ensure that '{nameof(UserModel)}' is not an abstract class and has a parameterless constructor, or alternatively " +
$"override the register page in /Areas/Identity/Pages/Account/Register.cshtml");
}
}
private IUserEmailStore<UserModel> GetEmailStore()
{
if (!_userManager.SupportsUserEmail)
{
throw new NotSupportedException("The default UI requires a user store with email support.");
}
return (IUserEmailStore<UserModel>)_userStore;
}
}
}

View File

@ -0,0 +1,23 @@
@page
@model RegisterConfirmationModel
@{
ViewData["Title"] = "Register confirmation";
}
<h1>@ViewData["Title"]</h1>
@{
if (@Model.DisplayConfirmAccountLink)
{
<p>
This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these docs</a> for how to configure a real email sender.
Normally this would be emailed: <a id="confirm-link" href="@Model.EmailConfirmationUrl">Click here to confirm your account</a>
</p>
}
else
{
<p>
Please check your email to confirm your account.
</p>
}
}

View File

@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class RegisterConfirmationModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly IEmailSender _sender;
public RegisterConfirmationModel(UserManager<UserModel> userManager, IEmailSender sender)
{
_userManager = userManager;
_sender = sender;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool DisplayConfirmAccountLink { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string EmailConfirmationUrl { get; set; }
public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
{
if (email == null)
{
return RedirectToPage("/Index");
}
returnUrl = returnUrl ?? Url.Content("~/");
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return NotFound($"Unable to load user with email '{email}'.");
}
Email = email;
// Once you add a real email sender, you should remove this code that lets you confirm the account
DisplayConfirmAccountLink = true;
if (DisplayConfirmAccountLink)
{
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
EmailConfirmationUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
protocol: Request.Scheme);
}
return Page();
}
}
}

View File

@ -0,0 +1,26 @@
@page
@model ResendEmailConfirmationModel
@{
ViewData["Title"] = "Resend email confirmation";
}
<h1>@ViewData["Title"]</h1>
<h2>Enter your email.</h2>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="All" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.Email" class="form-control" aria-required="true" placeholder="name@example.com" />
<label asp-for="Input.Email" class="form-label"></label>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Resend</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,89 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class ResendEmailConfirmationModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
private readonly IEmailSender _emailSender;
public ResendEmailConfirmationModel(UserManager<UserModel> userManager, IEmailSender emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }
}
public void OnGet()
{
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.FindByEmailAsync(Input.Email);
if (user == null)
{
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
return Page();
}
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
Input.Email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
return Page();
}
}
}

View File

@ -0,0 +1,37 @@
@page
@model ResetPasswordModel
@{
ViewData["Title"] = "Reset password";
}
<h1>@ViewData["Title"]</h1>
<h2>Reset your password.</h2>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<input asp-for="Input.Code" type="hidden" />
<div class="form-floating mb-3">
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label asp-for="Input.Email" class="form-label"></label>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please enter your password." />
<label asp-for="Input.Password" class="form-label"></label>
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please confirm your password." />
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,118 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
public class ResetPasswordModel : PageModel
{
private readonly UserManager<UserModel> _userManager;
public ResetPasswordModel(UserManager<UserModel> userManager)
{
_userManager = userManager;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
public string Password { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
public string Code { get; set; }
}
public IActionResult OnGet(string code = null)
{
if (code == null)
{
return BadRequest("A code must be supplied for password reset.");
}
else
{
Input = new InputModel
{
Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code))
};
return Page();
}
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.FindByEmailAsync(Input.Email);
if (user == null)
{
// Don't reveal that the user does not exist
return RedirectToPage("./ResetPasswordConfirmation");
}
var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password);
if (result.Succeeded)
{
return RedirectToPage("./ResetPasswordConfirmation");
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return Page();
}
}
}

View File

@ -0,0 +1,10 @@
@page
@model ResetPasswordConfirmationModel
@{
ViewData["Title"] = "Reset password confirmation";
}
<h1>@ViewData["Title"]</h1>
<p>
Your password has been reset. Please <a asp-page="./Login">click here to log in</a>.
</p>

View File

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[AllowAnonymous]
public class ResetPasswordConfirmationModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,10 @@
@model string
@if (!String.IsNullOrEmpty(Model))
{
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
@Model
</div>
}

View File

@ -0,0 +1 @@
@using PSTW_CentralSystem.Areas.Identity.Pages.Account

View File

@ -0,0 +1,23 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>Development environment should not be enabled in deployed applications</strong>, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application.
</p>

View File

@ -0,0 +1,41 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PSTW_CentralSystem.Areas.Identity.Pages
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[AllowAnonymous]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string RequestId { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
}

View File

@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

View File

@ -0,0 +1,5 @@
@using Microsoft.AspNetCore.Identity
@using PSTW_CentralSystem.Areas.Identity
@using PSTW_CentralSystem.Areas.Identity.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using PSTW_CentralSystem.Models

View File

@ -0,0 +1,4 @@

@{
Layout = "/Views/Shared/_Layout.cshtml";
}

View File

@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace PSTW_CentralSystem.Areas.Inventory.Controllers.Admin
{
[Area("Inventory")]
[Authorize(Policy = "RoleModulePolicy")]
public class InventoryMasterController : Controller
{
public IActionResult AdminDashboard()
{
return View();
}
public IActionResult ItemRegistration()
{
return View();
}
public IActionResult ItemMovement()
{
return View();
}
public IActionResult ItemRequestMaster()
{
return View();
}
public IActionResult ProductRegistration()
{
return View();
}
public IActionResult SupplierRegistration()
{
return View();
}
public IActionResult ManifacturerRegistration()
{
return View();
}
public IActionResult StationRegistration()
{
return View();
}
public IActionResult QrMaster()
{
return View();
}
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PSTW_CentralSystem.Areas.Inventory.Models;
namespace PSTW_CentralSystem.Areas.Inventory.Controllers
{
[Area("Inventory")]
[Authorize(Policy = "RoleModulePolicy")]
public class ItemMovementController : Controller
{
// GET: Inventory
public ActionResult Index()
{
return View();
}
public ActionResult ItemMovementUser()
{
return View();
}
public ActionResult QrUser()
{
return View();
}
public IActionResult ItemRequest()
{
return View();
}
public IActionResult UserDashboard()
{
return View();
}
[Authorize]
[HttpPost("/i/{id}")]
public IActionResult ItemRecognization(string id, [FromBody] ItemModel item)
{
return View();
}
}
}

View File

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace PSTW_CentralSystem.Areas.Inventory.Controllers
{
[Area("Inventory")]
//[Authorize(Policy = "RoleModulePolicy")]
public class MainController : Controller
{
// GET: Inventory
public ActionResult Index()
{
return View();
}
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using PSTW_CentralSystem.Models;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace PSTW_CentralSystem.Areas.Inventory.Models
{
public class InventoryMasterModel
{
[Key]
public int StoreId { get; set; }
public int UserId { get; set; }
[ForeignKey("UserId")]
public virtual UserModel? User { get; set; }
[ForeignKey("StoreId")]
public virtual StoreModel? Store { get; set; }
}
}

View File

@ -0,0 +1,50 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Inventory.Models
{
public class ItemModel
{
[Key]
public int ItemID { get; set; }
public string UniqueID { get; set; } = string.Empty;
public required int CompanyId { get; set; }
public required int DepartmentId { get; set; }
public required int ProductId { get; set; }
public required string? SerialNumber { get; set; }
public required string? TeamType { get; set; }
public required int Quantity { get; set; }
public required string Supplier { get; set; }
public required DateTime PurchaseDate { get; set; }
public required string PONo { get; set; }
public required string Currency { get; set; }
public required float DefaultPrice { get; set; }
public required float CurrencyRate { get; set; }
public required float ConvertPrice { get; set; }
public string? DONo { get; set; }
public DateTime? DODate { get; set; }
public required int Warranty { get; set; }
public required DateTime EndWDate { get; set; }
public string? InvoiceNo { get; set; }
public DateTime? InvoiceDate { get; set; }
[Comment("1 = In stock; 2 = Item Moving; 3 = Item Out; 4 = Item Broken; 5 = Item Lost; 6 = Item Stolen; 7 = Item Damaged; 8 = Item Discarded; 9 = Item Destroyed; 10 = Item Finished;")]
public int ItemStatus { get; set; } = 1;
public int? MovementId { get; set; }
public string PartNumber { get; set; } = string.Empty;
public int CreatedByUserId { get; set; }
[ForeignKey("CreatedByUserId")]
public virtual UserModel? CreatedBy { get; set; }
[ForeignKey("CompanyId")]
public virtual CompanyModel? Company { get; set; }
[ForeignKey("DepartmentId")]
public virtual DepartmentModel? Department { get; set; }
[ForeignKey("ProductId")]
public virtual ProductModel? Product { get; set; }
[ForeignKey("MovementId")]
public virtual ItemMovementModel? Movement { get; set; }
}
}

View File

@ -0,0 +1,48 @@
using PSTW_CentralSystem.Models;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace PSTW_CentralSystem.Areas.Inventory.Models
{
public class ItemMovementModel
{
[Key]
public int Id { get; set; }
public int? ItemId { get; set; }
public int? ToStation { get; set; }
public int? ToStore { get; set; }
public int? ToUser { get; set; }
[Comment("Repair, Calibration, Faulty, Ready To Deploy, On Delivery")]
public string? ToOther { get; set; }
public DateTime? sendDate { get; set; }
[Comment("Register, StockIn, Stock Out")]
public string? Action { get; set; }
public int? Quantity { get; set; }
public string? Remark { get; set; }
public string? ConsignmentNote { get; set; }
public DateTime Date { get; set; }
public int? LastUser { get; set; }
public int? LastStore{ get; set; }
public int? LastStation{ get; set; }
[Comment("Repair, Calibration, Faulty, Ready To Deploy, On Delivery")]
public string? LatestStatus { get; set; }
public DateTime? receiveDate { get; set; }
public bool MovementComplete { get; set; } = false;
//public virtual ItemModel? Item { get; set; }
//[ForeignKey("ToStore")]
[ForeignKey("ItemId")]
public virtual ItemModel? Item { get; set; }
[ForeignKey("ToStore")]
public virtual StoreModel? NextStore { get; set; }
[ForeignKey("ToStation")]
public virtual StationModel? NextStation { get; set; }
[ForeignKey("ToUser")]
public virtual UserModel? NextUser { get; set; }
[ForeignKey("LastStore")]
public virtual StoreModel? FromStore { get; set; }
[ForeignKey("LastStation")]
public virtual StationModel? FromStation { get; set; }
[ForeignKey("LastUser")]
public virtual UserModel? FromUser { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace PSTW_CentralSystem.Areas.Inventory.Models
{
public class ManufacturerModel
{
[Key]
public int ManufacturerId { get; set; }
public required string ManufacturerName { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.Inventory.Models
{
public class ProductModel
{
[Key]
public int ProductId { get; set; }
public required string ProductName { get; set; }
public required string ProductShortName { get; set; }
public required int ManufacturerId { get; set; }
public required string Category { get; set; }
public required string ModelNo { get; set; }
public int? QuantityProduct { get; set; }
public required string ImageProduct { get; set; }
[ForeignKey("ManufacturerId")]
public virtual ManufacturerModel? Manufacturer { get; set; }
public virtual ICollection<ItemModel>? Items { get; set; } // Navigation property>
}
}

View File

@ -0,0 +1,37 @@
using PSTW_CentralSystem.Models;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PSTW_CentralSystem.Areas.Inventory.Models
{
[Table("request")]
public class RequestModel
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int requestID { get; set; }
public int ProductId { get; set; }
[ForeignKey("ProductId")]
public virtual ProductModel? Product { get; set; }
public int? StationId { get; set; }
[ForeignKey("StationId")]
public virtual StationModel? Station { get; set; }
public int UserId { get; set; }
[ForeignKey("UserId")]
public virtual UserModel? User { get; set; }
public string? ProductCategory { get; set; }
public string? remarkUser { get; set; }
public string? remarkMasterInv { get; set; }
public string? status { get; set; }
public DateTime requestDate { get; set; }
public DateTime? approvalDate { get; set; }
public int? RequestQuantity { get; set; }
public string? Document { get; set; }
public int? fromStoreItem { get; set; }
[ForeignKey("fromStoreItem")]
public virtual StoreModel? Store { get; set; }
public int? assignStoreItem { get; set; }
[ForeignKey("assignStoreItem")]
public virtual StoreModel? Stores { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using PSTW_CentralSystem.Models;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PSTW_CentralSystem.Areas.Inventory.Models
{
public class StationModel
{
[Key]
public int StationId { get; set; }
public int StationPicID { get; set; }
public string? StationName { get; set; }
public int DepartmentId { get; set; }
[ForeignKey("DepartmentId")]
public virtual DepartmentModel? Department { get; set; }
[ForeignKey("StationPicID")]
public virtual UserModel? StationPic { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using PSTW_CentralSystem.Models;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PSTW_CentralSystem.Areas.Inventory.Models
{
public class StoreModel
{
[Key]
public int Id { get; set; }
public int CompanyId { get; set; }
public string StoreName { get; set; } = string.Empty;
[ForeignKey("CompanyId")]
public virtual CompanyModel? Company { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
namespace PSTW_CentralSystem.Areas.Inventory.Models
{
public class SupplierModel
{
[Key]
public int SupplierId { get; set; }
public required string SupplierCompName { get; set; }
public required string SupplierAddress { get; set; }
[AllowNull]
public string? SupplierPIC { get; set; }
[AllowNull]
public string? SupplierEmail { get; set; }
[AllowNull]
public string? SupplierPhoneNo { get; set; }
}
}

View File

@ -0,0 +1,116 @@
@{
ViewData["Title"] = "Dashboard";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div class="container" id="invAdmin">
<div class="row">
<div class="text-center">
<p><h1 class="display-4">Inventory Admin Dashboard</h1></p>
<p v-show="currentUserCompanyDept.departmentName"><h2 class="display-6">Store: {{ currentUserCompanyDept.departmentName }}</h2></p>
</div>
</div>
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml")
<div class="row card">
<div class="card-header">
<h3 class="card-title">Inventory Report</h3>
</div>
<div class="card-body">
<div v-if="reportData">
<div class="row justify-content-center">
<div class="col-3">
<h4>Statistic</h4>
<p>Total Number of Item Registered: {{ reportData.itemCountRegistered }}</p>
<p>Total Number of Item Still in Stock: {{ reportData.itemCountStillInStock }}</p>
</div>
<div class="col-3">
<h4>Item Registered </h4>
<p>This Month: {{ reportData.itemCountRegisteredThisMonth }}</p>
<p>Last Month: {{ reportData.itemCountRegisteredLastMonth }}</p>
</div>
<div class="col-3">
<h4>Item Stock Out </h4>
<p>This Month: {{ reportData.itemCountStockOutThisMonth }}</p>
<p>Last Month: {{ reportData.itemCountStockOutLastMonth }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
$(function () {
app.mount('#invAdmin');
$('.closeModal').on('click', function () {
// Show the modal with the ID 'addManufacturerModal'.
$('.modal').modal('hide');
});
});
const app = Vue.createApp({
data() {
return {
currentUser: null,
currentUserCompanyDept: {
departmentName: null,
departmentId: null
},
reportData: null,
}
},
mounted() {
this.fetchUser();
},
methods: {
async fetchUser() {
try {
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
this.currentUser = data?.userInfo || null;
const companyDeptData = await this.currentUser.department;
const userRole = this.currentUser.role;
if(userRole == "SuperAdmin" || userRole == "SystemAdmin"){
this.currentUserCompanyDept = {departmentId : 0, departmentName : "All"}
this.fetchInventoryReport(0);
}
else{
this.currentUserCompanyDept = companyDeptData;
this.fetchInventoryReport(companyDeptData.departmentId);
}
}
else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
async fetchInventoryReport(deptId){
try {
const response = await fetch(`/InvMainAPI/GetInventoryReport/` + deptId, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
this.reportData = data;
}
else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
},
});
</script>
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,207 @@
@{
ViewData["Title"] = "Manufactures";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml")
<div id="app">
<div class="row card">
<div class="card-header">
<button id="addManufacturerBtn" class="btn btn-success col-md-3 m-1"><i class="fa fa-plus"></i>&nbsp;Add Manufacturer</button>
</div>
<div class="card-body">
<div v-if="loading">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<table class="table table-bordered table-hover table-striped no-wrap" id="manufacturerTable" style="width:100%;border-style: solid; border-width: 1px"></table>
</div>
</div>
<div class="modal fade" id="addManufacturerModal" tabindex="-1" role="dialog" aria-labelledby="addManufacturerModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addManufacturerModalLabel">Add Manufacturer</h5>
<button type="button" class="closeModal" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form v-on:submit.prevent="addManufacturer">
<div class="modal-body">
<div class="form-group">
<label for="manufacturerName">Manufacturer Name:</label>
<input type="text" class="form-control" id="manufacturerName" v-model="newManufacturer.manufacturerName" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary closeModal" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
const app = Vue.createApp({
data() {
return {
manufacturer: null,
manufacturerDatatable: null,
newManufacturer: {
manufacturerName: null,
},
loading: true,
}
},
mounted() {
// Fetch companies, depts, and products from the API
this.fetchManufactures();
this.initiateTable();
},
methods: {
async fetchManufactures() {
fetch('/InvMainAPI/ManufacturerList', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data != null && data.length > 0)
{
this.manufacturer = data;
}
if (!this.manufacturerDatatable) {
this.initiateTable();
} else {
this.fillTable(data);
}
})
.catch(error => {
console.error('There was a problem with the fetch operation:', error);
});
},
async initiateTable() {
this.manufacturerDatatable = $('#manufacturerTable').DataTable({
"data": this.manufacturer,
"columns": [
{ "title": "Manufacturer Name",
"data": "manufacturerName",
},
{ "title": "Delete",
"data": "manufacturerName",
"render": function (data, type, full, meta) {
var deleteButton = `<button type="button" class="btn btn-danger delete-btn" data-id="${full.manufacturerId}">Delete</button>`;
return deleteButton;
},
"width": '10%',
},
],
})
self = this;
// Attach click event listener to the delete buttons
$('#manufacturerTable tbody').on('click', '.delete-btn', function () {
const manufacturerId = $(this).data('id'); // Get the manufacturer ID from the button
self.deleteManufacturer(manufacturerId); // Call the Vue method
});
this.loading = false;
},
fillTable(data){
if (!this.manufacturerDatatable) {
console.error("DataTable not initialized");
return;
}
this.manufacturerDatatable.clear();
this.manufacturerDatatable.rows.add(data);
this.manufacturerDatatable.draw();
this.loading = false;
},
addManufacturer() {
this.loading = true;
const existingManufacturer = this.manufacturer != null ? this.manufacturer.find(m => m.manufacturerName.toLowerCase() === this.newManufacturer.manufacturerName.toLowerCase()) : null;
if (existingManufacturer) {
alert('Manufacturer already exists');
return;
}
fetch('/InvMainAPI/AddManufacturer', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.newManufacturer)
})
.then(response => response.json())
.then(data => {
if (data != null && data.length > 0)
{
this.manufacturer = data;
}
this.fillTable(data);
})
.catch(error => {
console.error('There was a problem with the fetch operation:', error);
})
.finally(() => {
$('#preloader').modal('hide');
this.newManufacturer.manufacturerName = null;
});
},
async deleteManufacturer(manufacturerId) {
if (!confirm("Are you sure you want to delete this manufacturer?")) {
return;
}
try {
const response = await fetch(`/InvMainAPI/DeleteManufacturer/${manufacturerId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (result.success) {
alert(result.message);
// Remove the row from DataTables
this.manufacturerDatatable
.row($(`.delete-btn[data-id="${manufacturerId}"]`).closest('tr'))
.remove()
.draw();
} else {
alert(result.message);
}
}
catch (error) {
console.error("Error deleting manufacturer:", error);
alert("An error occurred while deleting the manufacturer.");
}
finally {
this.loading = false;
}
},
}
});
$(function () {
app.mount('#app');
// Attach a click event listener to elements with the class 'btn-success'.
$('#addManufacturerBtn').on('click', function () {
// Show the modal with the ID 'addManufacturerModal'.
$('#addManufacturerModal').modal('show');
});
$('.closeModal').on('click', function () {
// Show the modal with the ID 'addManufacturerModal'.
$('#addManufacturerModal').modal('hide');
});
});
</script>
}

View File

@ -0,0 +1,584 @@
@{
ViewData["Title"] = "Product Form";
Layout = "~/Views/Shared/_Layout.cshtml";
string userId = ViewBag.UserId;
}
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml")
<div class="row">
<div id="registerProduct" class="card m-1">
<div class="row" v-if="addSection == true">
<form v-on:submit.prevent="addProduct" data-aos="fade-right" >
<div class="container register" data-aos="fade-right">
<div class="row" data-aos="fade-right">
<div class="col-md-12">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<div class="card-header">
<h3 class="register-heading">REGISTRATION PRODUCT</h3>
</div>
<div class="row register-form card-body">
<div class="col-md-6">
@* Product Name *@
<div class="form-group row">
<label for="productName" class="col-sm-3">Product Name:</label>
<div class="col-sm-9">
<input type="text" id="productName" name="productName" class="form-control" required v-model="productName">
</div>
</div>
@* Product Short Name *@
<div class="form-group row">
<label for="productName" class="col-sm-3">Product Short Name:</label>
<div class="col-sm-9">
<input type="text" id="productShortName" name="productShortName" class="form-control" maxlength="13" v-model="productShortName" required>
<p><em><small class="text-danger">* Product short name limited to 13 characters</small></em></p>
</div>
</div>
@* Manufacturer *@
<div class="form-group row">
<label class="col-sm-3">Manufacturer:</label>
<div class="col-sm-9">
<div class="">
<select class="btn btn-primary form-select" v-model="manufacturer" required>
<option class="btn-light" value="" selected disabled>Select Manufacturer</option>
<option class="btn-light" v-for="(item, index) in manufacturers" :key="item.manufacturerId" :value="item.manufacturerId">{{ item.manufacturerName ?? 'Select Manufacturer' }}</option>
</select>
</div>
</div>
</div>
@* Category *@
<div class="form-group row">
<label class="col-sm-3">Category:</label>
<div class="col-sm-9">
<div class="">
<select class="btn btn-primary form-select" v-model="category" required>
<option class="btn-light" value="" selected disabled>Select Category</option>
<option class="btn-light" v-for="(item, index) in categories" :key="item" :value="item">{{ item ?? 'Select Category' }}</option>
</select>
</div>
</div>
</div>
</div>
<div class="col-md-6">
@* Model No Coding *@
<div class="form-group row">
<label for="modelNo" class="col-sm-3">Model No:</label>
<div class="col-sm-9">
<input type="text" id="modelNo" name="modelNo" class="form-control" required v-model="modelNo">
</div>
</div>
@* Image Product Coding *@
<div class="form-group row">
<label for="imageProduct" class="col-sm-3">Image:</label>
<div class="col-sm-9">
<input type="file" id="imageProduct" name="imageProduct" class="form-control" v-on:change="previewImage" accept="image/*" required>
<br>
<img v-if="imageSrc" :src="imageSrc" alt="Image Preview" class="img-thumbnail" style="width: 200px; margin-top: 10px;" />
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-9 offset-sm-3">
<button type="button" v-on:click="resetForm" class="btn btn-secondary mx-1">Reset</button>
<input type="submit" class="btn btn-primary mx-1">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<div class="row">
<button class="btn btn-danger col-md-3 m-1" v-on:click=" resetForm(); addSection = false;"><i class="fa fa-minus"></i>&nbsp;Hide Add Product Section</button>
</div>
</div>
<div class="row" v-if="editSection == true">
<form v-on:submit.prevent="submitEditProduct" data-aos="fade-right">
<div class="container register" data-aos="fade-right">
<div class="row" data-aos="fade-right">
<div class="col-md-12">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<div class="card-header">
<h3 class="register-heading">EDIT PRODUCT</h3>
</div>
<div class="row register-form card-body">
<div class="col-md-6">
@* Product Name *@
<div class="form-group row">
<label for="productName" class="col-sm-3">Product Name:</label>
<div class="col-sm-9">
<input type="text" id="productName" name="productName" class="form-control" required v-model="productName">
</div>
</div>
@* Product Short Name *@
<div class="form-group row">
<label for="productName" class="col-sm-3">Product Short Name:</label>
<div class="col-sm-9">
<input type="text" id="productShortName" name="productShortName" class="form-control" maxlength="13" v-model="productShortName" required>
<p><em><small class="text-danger">* Product short name limited to 13 characters</small></em></p>
</div>
</div>
@* Manufacturer *@
<div class="form-group row">
<label class="col-sm-3">Manufacturer:</label>
<div class="col-sm-9">
<div class="">
<select class="btn btn-primary form-select" v-model="manufacturer" required>
<option class="btn-light" value="" selected disabled>Select Manufacturer</option>
<option class="btn-light" v-for="(item, index) in manufacturers" :key="item.manufacturerId" :value="item.manufacturerId">{{ item.manufacturerName ?? 'Select Manufacturer' }}</option>
</select>
</div>
</div>
</div>
@* Category *@
<div class="form-group row">
<label class="col-sm-3">Category:</label>
<div class="col-sm-9">
<div class="">
<select class="btn btn-primary form-select" v-model="category" required>
<option class="btn-light" value="" selected disabled>Select Category</option>
<option class="btn-light" v-for="(item, index) in categories" :key="item" :value="item">{{ item ?? 'Select Category' }}</option>
</select>
</div>
</div>
</div>
</div>
<div class="col-md-6">
@* Model No Coding *@
<div class="form-group row">
<label for="modelNo" class="col-sm-3">Model No:</label>
<div class="col-sm-9">
<input type="text" id="modelNo" name="modelNo" class="form-control" required v-model="modelNo">
</div>
</div>
@* Image Product Coding *@
<div class="form-group row">
<label for="imageProduct" class="col-sm-3">Image:</label>
<div class="col-sm-9">
<input type="file" id="imageProduct" name="imageProduct" class="form-control" v-on:change="previewImage" accept="image/*" ref="fileInput">
<small v-if="existingFileName">{{ existingFileName }}</small>
<br>
<img v-if="imageSrc" :src="imageSrc" alt="Image Preview" class="img-thumbnail" style="width: 200px; margin-top: 10px;" />
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-9 offset-sm-3">
<button type="button" v-on:click="resetForm" class="btn btn-secondary mx-1">Reset</button>
<input type="submit" class="btn btn-primary mx-1">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<div class="row">
<button class="btn btn-danger col-md-3 m-1" v-on:click=" resetForm(); editSection = false; addSection = false;"><i class="fa fa-minus"></i>&nbsp;Hide Edit Product Section</button>
</div>
</div>
<div class="row">
<button id="addProductBtn" class="btn btn-success col-md-3 m-1" v-show="addSection == false" v-on:click="resetForm(); addSection = true; editSection = false;"><i class="fa fa-plus"></i>&nbsp;Show Add Product Section</button>
</div>
<div class="row table-responsive">
<table class="table table-bordered table-hover table-striped no-wrap" id="productDatatable" style="width:100%;border-style: solid; border-width: 1px"></table>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
$(function () {
app.mount('#registerProduct');
});
const app = Vue.createApp({
data() {
return {
editSection: null,
productId: null,
addSection: false,
productName: null,
manufacturer: '',
manufacturers: null,
category: '',
categories: ["Asset", "Part", "Disposable"],
modelNo: null,
imageProduct: null,
manufactures: [],
showOtherManufacturer: false,
imageSrc: '',
products: null,
productDatatable: null,
productShortName: null,
existingFileName: null,
}
},
mounted() {
// Fetch companies, depts, and products from the API
this.fetchManufactures();
this.fetchProducts();
},
computed() {
},
methods: {
initiateTable() {
this.productDatatable = $('#productDatatable').DataTable({
"data": this.products,
"columns": [
{
"title": "Product Name",
"data": "productName",
},
{
"title": "Product Short Name",
"data": "productShortName",
},
{
"title": "Model Number",
"data": "modelNo",
},
{
"title": "Manufacturer",
"data": "manufacturer.manufacturerName",
},
{
"title": "Product Category",
"data": "category",
},
{
"title": "Product Stock",
"data": "quantityProduct",
},
{
"title": "Image",
"data": "imageProduct",
"render": function (data, type, full, meta) {
var image = `<a href="${data}" target="_blank" data-lightbox="image-1">
<img src="${data}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
</a>`;
return image;
},
},
{
"title": "Edit",
"data": "productId",
"render": function (data) {
var editButton = `<button type="button" class="btn btn-success edit-btn" data-id="${data}">Edit</button>`;
return editButton;
},
},
{
"title": "Delete",
"data": "productId",
"render": function (data) {
var deleteButton = `<button type="button" class="btn btn-danger delete-btn" data-id="${data}">Delete</button>`;
return deleteButton;
},
}
],
responsive: true,
})
self = this;
// Attach click event listener to the edit buttons
$('#productDatatable tbody').off('click', '.edit-btn').on('click', '.edit-btn', function () {
const productId = $(this).data('id');
self.editProduct(productId);
});
// Attach click event listener to the delete buttons
$('#productDatatable tbody').off('click', '.delete-btn').on('click', '.delete-btn', function () {
const productId = $(this).data('id'); // Get the manufacturer ID from the button
self.deleteProduct(productId); // Call the Vue method
});
this.loading = false;
},
async fetchManufactures() {
fetch('/InvMainAPI/ManufacturerList', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data != null && data.length > 0) {
this.manufacturers = data;
}
})
.catch(error => {
console.error('There was a problem with the fetch operation:', error);
});
},
async fetchProducts() {
try {
const response = await fetch('/InvMainAPI/ProductList', {
method: 'POST'
}); // Call the API
if (!response.ok) {
throw new Error('Failed to fetch products');
}
this.products = await response.json(); // Store the fetched products
if (this.productDatatable) {
this.productDatatable.clear().destroy();
}
this.$nextTick(() => {
this.initiateTable()
})
} catch (error) {
console.error('Error fetching products:', error);
}
},
async editProduct(productId) {
// Find the selected station data
const product = this.products.find(s => s.productId === productId);
if (!product) {
alert('Error', 'Product not found!', 'warning');
return;
}
// Populate form fields
this.productId = productId;
this.productName = product.productName;
this.stationName = product.stationName;
this.productShortName = product.productShortName;
this.manufacturer = product.manufacturer.manufacturerId;
this.category = product.category;
this.modelNo = product.modelNo;
this.imageProduct = product.imageProduct;
this.previewImage(this.imageProduct);
// Show the edit form and hide the add form
this.addSection = false;
this.editSection = true;
},
async submitEditProduct() {
const formData = {
ProductId: this.productId,
productName: this.productName,
ProductShortName: this.productShortName,
ManufacturerId: this.manufacturer,
category: this.category,
ModelNo: this.modelNo,
ImageProduct: this.imageProduct
};
try {
// List of required fields
const requiredFields = ['productName', 'manufacturer', 'category', 'modelNo', 'imageProduct'];
// Loop through required fields and check if any are null or empty
for (let field of requiredFields) {
if (this[field] === null || this[field] === '') {
alert('Product Error', `Please fill in required fields: ${field}.`, 'warning');
return; // Exit early if validation fails
}
}
const response = await fetch('/InvMainAPI/EditProduct', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
const errorData = await response.json();
console.error('Error response:', errorData);
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
return;
}
// Refresh station list and reset form
alert('Success!', 'Product form has been successfully submitted.', 'success');
this.fetchProducts();
this.resetForm();
this.editSection = false;
} catch (error) {
console.error('Error editing Product:', error);
this.errorMessage = 'Error: ' + error.message;
}
},
async addProduct() {
// const existingProduct = this.products.find(p => p.modelNo === this.modelNo);
// if (existingProduct) {
// alert(`Product Error: The model number ${this.modelNo} already exists.`, 'error');
// return; // Exit early if the modelNo exists
// }
// Create the payload
const formData = {
productName: this.productName,
productShortName: this.productShortName,
manufacturerId: this.manufacturer,
category: this.category,
modelNo: this.modelNo,
imageProduct: this.imageProduct
};
try {
// List of required fields
const requiredFields = ['productName', 'manufacturer', 'category', 'modelNo', 'imageProduct'];
// Loop through required fields and check if any are null or empty
for (let field of requiredFields) {
if (this[field] === null || this[field] === '') {
alert('Product Error', `Please fill in required fields: ${field}.`, 'warning');
return; // Exit early if validation fails
}
}
// Proceed to send the data as raw JSON string
const response = await fetch('/InvMainAPI/AddProduct', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData) // Convert the formData to a JSON string
});
if (!response.ok) {
const errorData = await response.json();
console.error('Error response:', errorData);
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
} else {
this.products = await response.json();
alert('Success!', 'Product form has been successfully submitted.', 'success');
this.fillTable(this.products);
this.resetForm();
}
} catch (error) {
console.error('Error:', error);
alert('Product Error', `An error occurred: ${error.message}`, 'error');
}
},
resetForm() {
this.productName = null;
this.manufacturer = '';
this.productShortName = '';
this.category = '';
this.modelNo = null;
this.imageProduct = null;
this.imageSrc = '';
const fileInput = document.getElementById('imageProduct');
if (fileInput) {
fileInput.value = ''; // Clear the file input value
}
},
// Update Select View
updateManufacturer(manufacturer) {
this.manufacturer = manufacturer;
this.showOtherManufacturer = false;
},
updateCategory(category) {
this.category = category;
},
// When User Presses Button Other
toggleOtherInput(type) {
if (type === 'manufacturer') {
this.showOtherManufacturer = true;
}
},
// User Inserting an Image
previewImage(event) {
if (typeof event === "string") {
this.imageSrc = event;
this.existingFileName = event.split('/').pop(); // Ambil nama fail daripada URL
return;
}
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.imageSrc = e.target.result;
this.imageProduct = e.target.result.split(',')[1];
this.existingFileName = file.name; // Simpan nama fail
};
reader.readAsDataURL(file);
} else {
this.imageSrc = '';
this.imageProduct = null;
this.existingFileName = null;
}
},
async deleteProduct(productId) {
if (!confirm("Are you sure you want to delete this product?")) {
return;
}
try {
const response = await fetch(`/InvMainAPI/DeleteProduct/${productId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (result.success) {
alert(result.message);
// Remove the row from DataTables
this.productDatatable
.row($(`.delete-btn[data-id="${productId}"]`).closest('tr'))
.remove()
.draw();
} else {
alert(result.message);
}
}
catch (error) {
console.error("Error deleting product:", error);
alert("An error occurred while deleting the product.");
}
finally {
this.loading = false;
}
},
fillTable(data){
if (!this.productDatatable) {
console.error("DataTable not initialized");
return;
}
this.productDatatable.clear();
this.productDatatable.rows.add(data);
this.productDatatable.draw();
this.loading = false;
},
}
});
</script>
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,437 @@
@{
ViewData["Title"] = "Station";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml")
<div id="registerStation">
<form v-on:submit.prevent="addStation" data-aos="fade-right" id="registerStationForm" v-if="registerStationForm">
<div class="container register" data-aos="fade-right">
<div class="row">
<div class="col-md-9 register-rights" data-aos="fade-right">
<div class="tab-content" id="myTabContent" data-aos="fade-right">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab" data-aos="fade-right">
<h3 class="register-heading">REGISTRATION STATION</h3>
<div class="row register-form">
<div class="col-md-61">
@* Station Name *@
<div class="form-group row">
<label for="stationName" class="col-sm-3">Station Name:</label>
<div class="col-sm-9">
<input type="text" id="stationName" name="stationName" class="form-control" required v-model="stationName">
</div>
</div>
@* User ID *@
<div class="form-group row">
<label class="col-sm-4 col-form-label">Station User PIC</label>
<div class="col-sm-8">
<div class="">
<select class="btn btn-primary form-select" v-model="selectedUserName" :disabled="currentUser != null" required v-on:change="updateDepartment()">
<option class="btn-light" value="" disabled selected>Select User</option>
<option class="btn-light" v-for="(technicianUser, index) in users" :key="index" :value="technicianUser.fullName">
{{ technicianUser.fullName }}
</option>
</select>
</div>
</div>
</div>
@* Department ID *@
<!-- Department Dropdown -->
<div class="form-group row">
<label class="col-sm-4 col-form-label">Department: </label>
<div class="col-sm-8">
<!-- Use a span to keep the same space occupied -->
<span v-if="selectedDepartment">{{ selectedDepartment }}</span>
<span v-else>No department assigned</span>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-9 offset-sm-3">
<button type="button" v-on:click="resetForm" class="btn btn-secondary m-1">Reset</button>
<button type="submit" class="btn btn-primary m-1">Submit</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<form v-on:submit.prevent="submitEditStation" data-aos="fade-right" id="editStationForm" v-if="editStationForm">
<div class="container register" data-aos="fade-right">
<div class="row">
<div class="col-md-9 register-rights" data-aos="fade-right">
<div class="tab-content" id="myTabContent" data-aos="fade-right">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab" data-aos="fade-right">
<h3 class="register-heading">EDIT STATION</h3>
<div class="row register-form">
<div class="col-md-61">
@* Station Name *@
<div class="form-group row">
<label for="stationName" class="col-sm-3">Station Name:</label>
<div class="col-sm-9">
<input type="text" id="stationName" name="stationName" class="form-control" required v-model="stationName">
</div>
</div>
@* User ID *@
<div class="form-group row">
<label class="col-sm-4 col-form-label">Station User PIC</label>
<div class="col-sm-8">
<div class="">
<select class="btn btn-primary form-select" v-model="selectedUserName" :disabled="currentUser != null" required v-on:change="updateDepartment()">
<option class="btn-light" value="" disabled selected>Select User</option>
<option class="btn-light" v-for="(technicianUser, index) in users" :key="index" :value="technicianUser.fullName">
{{ technicianUser.fullName }}
</option>
</select>
</div>
</div>
</div>
@* Department ID *@
<!-- Department Dropdown -->
<div class="form-group row">
<label class="col-sm-4 col-form-label">Department: </label>
<div class="col-sm-8">
<!-- Use a span to keep the same space occupied -->
<span v-if="selectedDepartment">{{ selectedDepartment }}</span>
<span v-else>No department assigned</span>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-9 offset-sm-3">
<button type="button" v-on:click="resetForm" class="btn btn-secondary m-1">Reset</button>
<button type="submit" class="btn btn-primary m-1">Submit</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<div class="row card">
<div class="card-header">
<button id="addStationBtn" :class="['btn', 'col-md-3', 'col-lg-3', 'm-1', 'col-12', registerStationForm ? 'btn-danger' : 'btn-success']" v-on:click="registerStationForm = !registerStationForm"><i :class="['fa', registerStationForm ? 'fa-minus' : 'fa-plus']"></i>&nbsp;{{registerStationForm ? 'Hide Add Station' : 'Show Add Station'}}</button>
</div>
<div class="card-body">
<table class="table table-bordered table-hover table-striped no-wrap" id="stationDatatable" style="width:100%;border-style: solid; border-width: 1px"></table>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
$(function () {
app.mount('#registerStation');
// Attach a click event listener to elements with the class 'btn-success'.
$('#addStationBtn').on('click', function () {
// Show the modal with the ID 'addStationModal'.
$('#registerStationModal').modal('show');
});
$('.closeModal').on('click', function () {
// Show the modal with the ID 'addStationModal'.
$('.modal').modal('hide');
});
});
const app = Vue.createApp({
data() {
return {
stationId: null,
stationName: null,
stationUserPIC: null,
departmentId : null,
selectedUserName : null,
selectedDepartment: null,
stations: null,
currentUser: null,
users : null,
registerStationForm: false,
editStationForm: false,
}
},
mounted() {
this.fetchUsers();
this.fetchStations();
},
methods: {
async fetchUsers() {
try {
const response = await fetch(`/InvMainAPI/UserList/`, {
method: 'POST',
});
if (response.ok) {
this.users = await response.json();
}
else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
async fetchStations() {
try {
const response = await fetch('/InvMainAPI/StationList', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
console.error('Error response:', errorData);
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
return;
}
if (this.stationDatatable) {
this.stationDatatable.clear().destroy();
}
const stations = await response.json();
this.stations = stations;
if ($.fn.dataTable.isDataTable('#stationDatatable')) {
$('#stationDatatable').DataTable().clear().destroy();
}
this.initiateTable();
} catch (error) {
console.error('Error fetching stations:', error);
this.errorMessage = 'Error: ' + error.message;
}
},
async addStation() {
$('#loadingModal').modal('show');
// Create the payload
const formData = {
StationName: this.stationName,
StationPicID: this.stationUserPIC,
DepartmentId: this.departmentId,
};
try {
// List of required fields
const requiredFields = ['stationName', 'stationUserPIC', 'departmentId'];
// Loop through required fields and check if any are null or empty
for (let field of requiredFields) {
if (this[field] === null || this[field] === '') {
alert('Station Error', `Please fill in required fields: ${field}.`, 'warning');
return; // Exit early if validation fails
}
}
// Proceed to send the data as raw JSON string
const response = await fetch('/InvMainAPI/AddStation', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData) // Convert the formData to a JSON string
});
await this.fetchStations();
$('#loadingModal').modal('hide');
if (!response.ok) {
const errorData = await response.json();
console.error('Error response:', errorData);
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
} else {
this.fetchStations();
this.resetForm();
}
}
catch (error) {
console.error('Error:', error);
alert('Station Error', `An error occurred: ${error.message}`, 'error');
}
finally {
await new Promise(resolve => {
$('#loadingModal').on('shown.bs.modal', resolve);
});
$('#loadingModal').modal('hide');
}
},
resetForm() {
this.selectedUserName = null;
this.selectedDepartment = null;
this.stationName = null;
this.stationUserPIC = null;
this.departmentId = '';
},
initiateTable() {
self = this;
this.stationDatatable = $('#stationDatatable').DataTable({
"data": this.stations,
"columns": [
{
"title": "Station Name",
"data": "stationName",
},
{
"title": "Station User PIC",
"data": "fullName",
},
{
"title": "Department",
"data": "departmentName",
},
{
"title": "Edit",
"data": "stationId",
"render": function (data) {
var editButton = `<button type="button" class="btn btn-success edit-btn" data-id="${data}">Edit</button>`;
return editButton;
},
},
{
"title": "Delete",
"data": "stationId",
"render": function (data) {
var deleteButton = `<button type="button" class="btn btn-danger delete-btn" data-id="${data}">Delete</button>`;
return deleteButton;
},
}
],
responsive: true,
})
// Attach click event listener to the edit buttons
$('#stationDatatable tbody').off('click', '.edit-btn').on('click', '.edit-btn', function () {
const stationId = $(this).data('id');
self.editStation(stationId);
});
// Attach click event listener to the delete buttons
$('#stationDatatable tbody').off('click', '.delete-btn').on('click', '.delete-btn', function () {
const stationId = $(this).data('id');
self.deleteStation(stationId);
});
this.loading = false;
},
updateDepartment() {
// Find the selected user by their full name
const selectedUser = this.users.find(user => user.fullName === this.selectedUserName);
console.log(selectedUser);
if (selectedUser) {
this.stationUserPIC = selectedUser.id;
this.departmentId = selectedUser.department.departmentId;
this.selectedDepartment = selectedUser.department.departmentName; // Set department name
}
},
async editStation(stationId) {
// Find the selected station data
const station = this.stations.find(s => s.stationId === stationId);
if (!station) {
alert('Error', 'Station not found!', 'warning');
return;
}
// Populate form fields
this.stationId = stationId;
this.stationName = station.stationName;
this.selectedUserName = station.fullName;
this.departmentId = station.departmentId;
this.selectedDepartment = station.departmentName;
this.stationUserPIC = station.stationPicID;
// Show the edit form and hide the add form
this.registerStationForm = false;
this.editStationForm = true;
},
async submitEditStation() {
const formData = {
StationId: this.stationId,
StationName: this.stationName,
StationPicID: this.stationUserPIC,
DepartmentId: this.departmentId,
};
try {
// Ensure all required fields are filled
const requiredFields = ['stationName', 'stationUserPIC', 'departmentId'];
for (let field of requiredFields) {
if (!this[field]) {
alert('Error', `Please fill in required fields: ${field}`, 'warning');
return;
}
}
const response = await fetch('/InvMainAPI/EditStation', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
const errorData = await response.json();
console.error('Error response:', errorData);
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
return;
}
// Refresh station list and reset form
await this.fetchStations();
this.resetForm();
this.editStationForm = false;
} catch (error) {
console.error('Error editing station:', error);
this.errorMessage = 'Error: ' + error.message;
}
},
async deleteStation(stationId) {
if (!confirm("Are you sure you want to delete this station?")) {
return;
}
try {
const response = await fetch(`/InvMainAPI/DeleteStation/${stationId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ stationId })
});
if (!response.ok) {
const errorData = await response.json();
console.error('Error response:', errorData);
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
return;
}
await this.fetchStations();
} catch (error) {
console.error('Error deleting stations:', error);
this.errorMessage = 'Error: ' + error.message;
}
}
}
});
</script>
}

View File

@ -0,0 +1,287 @@
@{
ViewData["Title"] = "User Form";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml")
<div id="registerSupplier">
<form v-on:submit.prevent="addSupplier" data-aos="fade-right" id="registerSupplierForm" v-if="registerSupplierForm">
<div class="container register" data-aos="fade-right">
<div class="row">
@*Right Side*@
<div class="col-md-9 register-rights" data-aos="fade-right">
<div class="tab-content" id="myTabContent" data-aos="fade-right">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab" data-aos="fade-right">
<h3 class="register-heading">REGISTRATION SUPPLIER</h3>
<div class="row register-form">
<div class="col-md-61">
@* Supplier Name *@
<div class="form-group row">
<label for="supplierCompName" class="col-sm-3">Supplier Company Name:</label>
<div class="col-sm-9">
<input type="text" id="supplierCompName" name="supplierCompName" class="form-control" required v-model="supplierCompName">
</div>
</div>
@* Supplier Gender *@
<div class="form-group row">
<label class="col-sm-3">Supplier Address:</label>
<div class="col-sm-9">
<div class="dropdown">
<textarea type="text" id="supplierAddress" name="supplierAddress" class="form-control" required v-model="supplierAddress"></textarea>
</div>
</div>
</div>
@* Supplier PIC *@
<div class="form-group row">
<label class="col-sm-3">Supplier PIC:</label>
<div class="col-sm-9">
<input type="email" id="supplierPIC" name="supplierPIC" class="form-control" v-model="supplierPIC">
</div>
</div>
@* Supplier Email *@
<div class="form-group row">
<label class="col-sm-3">Supplier Email:</label>
<div class="col-sm-9">
<input type="email" id="supplierEmail" name="supplierEmail" class="form-control" v-model="supplierEmail">
</div>
</div>
@* Supplier Number Phone *@
<div class="form-group row">
<label class="col-sm-3">Supplier Phone Number:</label>
<div class="col-sm-9">
<input type="tel" id="supplierPhoneNo" name="supplierPhoneNo" class="form-control" v-model="supplierPhoneNo">
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-9 offset-sm-3">
<button type="button" v-on:click="resetForm" class="btn btn-secondary m-1">Reset</button>
<button type="submit" class="btn btn-primary m-1">Submit</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<div class="row card">
<div class="card-header">
<button id="addSupplierBtn" :class="['btn', 'col-md-3', 'col-lg-3', 'm-1', 'col-12', registerSupplierForm ? 'btn-danger' : 'btn-success']" v-on:click="registerSupplierForm = !registerSupplierForm"><i :class="['fa', registerSupplierForm ? 'fa-minus' : 'fa-plus']"></i>&nbsp;{{registerSupplierForm ? 'Hide Add Supplier' : 'Show Add Supplier'}}</button>
</div>
<div class="card-body">
<table class="table table-bordered table-hover table-striped no-wrap" id="supplierDatatable" style="width:100%;border-style: solid; border-width: 1px"></table>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
$(function () {
app.mount('#registerSupplier');
// Attach a click event listener to elements with the class 'btn-success'.
$('#addSupplierBtn').on('click', function () {
// Show the modal with the ID 'addManufacturerModal'.
$('#registerSupplierModal').modal('show');
});
$('.closeModal').on('click', function () {
// Show the modal with the ID 'addManufacturerModal'.
$('.modal').modal('hide');
});
});
const app = Vue.createApp({
data() {
return {
supplierCompName : null,
supplierEmail : null,
supplierAddress : null,
supplierPhoneNo : null,
supplierPIC : null,
suppliers: null,
supplierDatatable: null,
gender: ["Male", "Female", "Helicopter"],
registerSupplierForm: false
}
},
mounted(){
this.fetchSuppliers();
},
methods: {
async fetchSuppliers() {
try {
const response = await fetch('/InvMainAPI/SupplierList', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
console.error('Error response:', errorData);
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
return;
}
if (this.supplierDatatable){
this.supplierDatatable.clear().destroy();
}
const suppliers = await response.json();
this.suppliers = suppliers;
if (this.itemDatatable) {
this.itemDatatable.clear().destroy();
}
this.initiateTable();
} catch (error) {
console.error('Error fetching suppliers:', error);
this.errorMessage = 'Error: ' + error.message;
}
},
async addSupplier() {
$('#loadingModal').modal('show');
// Create the payload
const formData = {
supplierCompName: this.supplierCompName,
supplierAddress: this.supplierAddress,
supplierPIC: this.supplierPIC,
supplierEmail: this.supplierEmail,
supplierPhoneNo: this.supplierPhoneNo,
};
try {
// List of required fields
const requiredFields = ['supplierCompName', 'supplierAddress'];
// Loop through required fields and check if any are null or empty
for (let field of requiredFields) {
if (this[field] === null || this[field] === '') {
alert('Product Error', `Please fill in required fields: ${field}.`, 'warning');
return; // Exit early if validation fails
}
}
// Proceed to send the data as raw JSON string
const response = await fetch('/InvMainAPI/AddSupplier', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData) // Convert the formData to a JSON string
});
if (!response.ok) {
const errorData = await response.json();
console.error('Error response:', errorData);
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
} else {
alert('Success!', 'Supplier form has been successfully submitted.', 'success');
this.fetchSuppliers();
this.resetForm();
}
}
catch (error) {
console.error('Error:', error);
alert('Product Error', `An error occurred: ${error.message}`, 'error');
}
finally {
await new Promise(resolve => {
$('#loadingModal').on('shown.bs.modal', resolve);
});
$('#loadingModal').modal('hide');
}
},
resetForm() {
this.supplierCompName = null;
this.supplierAddress = null;
this.supplierEmail = null;
this.supplierPhoneNo = null;
this.supplierPIC = null;
},
initiateTable() {
self = this;
this.supplierDatatable = $('#supplierDatatable').DataTable({
"data": this.suppliers,
"columns": [
{
"title": "Supplier Company Name",
"data": "supplierCompName",
},
{
"title": "Supplier Address",
"data": "supplierAddress",
},
{
"title": "Company PIC",
"data": "supplierPIC",
},
{
"title": "Supplier Email",
"data": "supplierEmail",
},
{
"title": "Supplier Phone No",
"data": "supplierPhoneNo",
},
{
"title": "Delete",
"data": "supplierId",
"render": function (data) {
var deleteButton = `<button type="button" class="btn btn-danger delete-btn" data-id="${data}">Delete</button>`;
return deleteButton;
},
}
],
responsive: true,
})
// Attach click event listener to the delete buttons
$('#supplierDatatable tbody').on('click', '.delete-btn', function () {
const supplierId = $(this).data('id');
self.deleteSupplier(supplierId);
});
this.loading = false;
},
async deleteSupplier(supplierId) {
if (!confirm("Are you sure you want to delete this supplier?")) {
return;
}
try {
const response = await fetch(`/InvMainAPI/DeleteSupplier/${supplierId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ supplierId })
});
if (!response.ok) {
const errorData = await response.json();
console.error('Error response:', errorData);
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
return;
}
this.fetchSuppliers();
} catch (error) {
console.error('Error deleting supplier:', error);
this.errorMessage = 'Error: ' + error.message;
}
}
}
});
</script>
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,681 @@

@{
ViewData["Title"] = "Product Request";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartialUser.cshtml")
<style>
.dropdown {
position: relative;
width: 100%;
}
.dropdown-toggle-box {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 5px;
overflow: hidden;
}
.dropdown-toggle-box input {
flex: 1;
border: none;
padding: 8px;
}
.dropdown-btn {
border: none;
background-color: #007bff;
color: white;
padding: 8px 12px;
cursor: pointer;
}
.dropdown-content {
position: absolute;
width: 100%;
max-height: 200px;
overflow-y: auto;
background: #fff;
border: 1px solid #ddd;
z-index: 10;
}
.dropdown-content option {
padding: 10px;
cursor: pointer;
display: block;
}
.dropdown-content option:hover {
background-color: #f1f1f1;
}
.modal-body img {
max-width: 100%; /* Ensure it fits the modal */
max-height: 150px; /* Adjust max height */
display: block;
margin: auto;
}
</style>
<div id="requestProduct" class="row">
<!-- Document Preview Modal -->
<div id="documentModal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Document Preview</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body text-center">
<img id="documentPreview" src="" alt="Document" class="img-fluid" style="max-height: 80vh;">
</div>
</div>
</div>
</div>
<div class="row card">
<div class="card-header">
<h2>Pending Request</h2>
<button id="addRequestBtn" class="btn btn-success col-md-3 col-lg-3 m-1 col-12"><i class="fa fa-plus"></i>&nbsp;Add Request</button>
</div>
<div class="card-body">
@* <div v-if="loading">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div> *@
<table class="table table-bordered table-hover table-striped no-wrap" id="requestDatatable" style=" width:100%;border-style: solid; border-width: 1px"></table>
</div>
</div>
<div class="row card">
<div class="card-header">
<h2>Complete Request</h2>
</div>
<div class="card-body">
@* <div v-if="loading">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div> *@
<table class="table table-bordered table-hover table-striped no-wrap" id="settledrequestDatatable" style=" width:100%;border-style: solid; border-width: 1px"></table>
</div>
</div>
<div class="modal fade" id="requestModal" tabindex="-1" role="dialog" aria-labelledby="addRequestModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addRequestModalLabel">Add Request</h5>
<button type="button" class="closeModal" data-dismiss="modal" aria-label="Close" v-on:click="showRequestModal=false">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container-fluid">
<form v-on:submit.prevent="addRequest" data-aos="fade-right">
<div class=" register" data-aos="fade-right">
<div class="row" data-aos="fade-right">
<div class="col-md-12">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<h3 class="register-heading">PRODUCT REQUEST</h3>
<div class="row register-form">
<div class="col-md-13">
<!-- Product Name -->
<div class="form-group row">
<label class="col-sm-2 col-form-label">Product</label>
<div class="col-sm-8">
<div class="dropdown" v-click-outside="closeDropdown">
<!-- Button + Input dalam satu box -->
<div class="dropdown-toggle-box" v-on:click="dropdownOpen = !dropdownOpen">
<input type="text" class="form-control" v-model="searchQuery"
placeholder="Search product..." v-on:focus="dropdownOpen = true" v-on:click.stop />
<button type="button" class="btn btn-primary dropdown-btn" v-on:click.stop="dropdownOpen = !dropdownOpen">
</button>
</div>
<!-- Dropdown list -->
<div v-if="dropdownOpen" class="dropdown-content" v-on:click.stop>
<div v-for="(item, index) in filteredProducts"
:key="index" class="dropdown-item" v-on:mousedown.prevent="selectProduct(item)">
{{ item.productName + ' (' + item.modelNo + ')' }}
</div>
</div>
</div>
</div>
</div>
<!-- Category Dropdown -->
<div class="form-group row">
<label class="col-sm-2 col-form-label">Category:</label>
<div class="col-sm-8">
<input type="text" id="category" name="category" v-model="showProduct.category" class="form-control" readonly />
</div>
</div>
<!-- Image Product Dropdown-->
<div class="form-group row align-items-center">
<label for="imageProduct" class="col-sm-2 col-form-label">Product Image: </label>
<div class="col-sm-8">
<img v-if="showProduct.imageProduct" :src="showProduct.imageProduct" alt="Product Image" class="img-fluid" data-toggle="modal" data-target="#imageModal" />
<input type="hidden" id="imageProduct" name="imageProduct" v-model="showProduct">
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="imageModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageModalLabel">Product Image</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<img :src="showProduct.imageProduct" alt="Product Image" class="img-fluid">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Quantity Input -->
<div class="form-group row d-flex align-items-center">
<label class="col-sm-2 col-form-label">Quantity:</label>
<div class="col-sm-8">
<input type="number" class="form-control" v-model="quantity" required />
</div>
</div>
@* Who will assign to *@
<div class="form-group row">
<label class="col-sm-2 col-form-label">Item Assignment : </label>
<div class="col-sm-8">
<div class="dropdown">
<select class="btn btn-primary dropdown-toggle col-md-12" v-model="assign"required>
<option class="btn-light" value="" selected disabled>Select Assignment Type</option>
<option class="btn-light" v-for="(item, index) in assigns" :key="item" :value="item">{{ item ?? 'Select Assignment Type' }}</option>
</select>
</div>
</div>
</div>
<!-- Station Dropdown -->
<div class="form-group row" v-if="assign === 'Station'">
<label class="col-sm-2 col-form-label">Station:</label>
<div class="col-sm-8">
<div class="dropdown">
<select class="btn btn-primary dropdown-toggle col-md-12" data-toggle="dropdown" aria-expanded="false" v-model="stationId" required>
<option class="btn-light" value="" disabled selected>Select Station</option>
<option v-if="stations.length === 0" class="btn-light" disabled>No Station Assigned to You</option>
<option class="btn-light" v-for="(item, index) in stations" :key="index" :value="item.stationId">
{{ item.stationName }}
</option>
</select>
</div>
</div>
</div>
<!-- Remark Input (Wider) -->
<div class="form-group row d-flex align-items-center">
<label class="col-sm-2 col-form-label">Remark:</label>
<div class="col-sm-8">
<input type="text" class="form-control col-md-10" v-model="remark" />
</div>
</div>
<!-- Document/Picture Input -->
<div class="form-group row d-flex align-items-center">
<label class="col-sm-2 col-form-label">Document/Picture:</label>
<div class="col-sm-8">
<input type="file" id="document" name="document" class="form-control-file" v-on:change="handleFileUpload" accept="image/png, image/jpeg, application/pdf" />
</div>
</div>
@* Submit and Reset Buttons *@
<div class="form-group row">
<div class="col-sm-8 offset-sm-3">
<button type="button" v-on:click="resetForm" class="btn btn-secondary m-1">Reset</button>
<button type="submit" class="btn btn-primary m-1">Submit</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
$(function () {
app.mount('#requestProduct');
// Attach a click event listener to elements with the class 'btn-success'.
$('#addRequestBtn').on('click', function () {
$('#requestModal').modal('show');
});
$('.closeModal').on('click', function () {
$('.modal').modal('hide');
});
});
const app = Vue.createApp({
data() {
return {
requestID : null,
userId : null,
stationId : "",
productId : "",
remark: "",
document: null,
quantity: 0,
status: "",
requestDate : null,
approvalDate : null,
productCategory: "",
assign: "",
productName: null,
searchQuery: "",
dropdownOpen: false,
stations: [],
selectedProduct: "",
selectedStation: "",
selectedCategory: "",
assigns: ["Self-Assign", "Station"],
showRequestModal: false,
loading: false,
products: [],
request: [],
currentUser: null,
}
},
async mounted() {
this.fetchProducts();
await this.fetchUser();
await Promise.all([
this.fetchStation(),
this.fetchRequest(),
]);
},
computed: {
showProduct() {
if (!this.productId) {
return []; // No company selected, return empty list
}
const product = this.products.find(c => c.productId === this.productId);
this.productCategory = product.category;
return product ? product : {};
},
filteredProducts() {
return this.products.filter(item =>
item.productName.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
item.modelNo.toLowerCase().includes(this.searchQuery.toLowerCase())
);
}
},
methods: {
closeDropdown() {
this.dropdownOpen = false; // Tutup dropdown
},
selectProduct(item) {
this.selectedProduct = item;
this.productId = item.productId;
this.searchQuery = item.productName + " (" + item.modelNo + ")";
this.dropdownOpen = false;
},
handleFileUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.document = e.target.result.split(',')[1]; // Get Base64 string (remove metadata)
};
reader.readAsDataURL(file);
} else {
this.document = null;
}
},
async addRequest() {
try {
const requiredFields = ['productId', 'quantity', 'assign'];
// Loop through required fields and check if any are null or empty
for (let field of requiredFields) {
if (!this[field]) {
alert('Request Error', `Please fill in required fields: ${field}.`, 'warning');
return;
}
}
if (this.assign === "Station"){
if (this.stationId == null) {
alert('Request Error', `Please fill in required fields : Station.`, 'warning');
return;
}
}
if (this.assign === "Self-Assign"){
this.stationId = null;
}
this.userId = this.currentUser.id;
this.status = "Requested";
const now = new Date();
this.requestDate = new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString();
// Prepare data as JSON (No file upload)
const requestData = {
ProductId: this.productId,
StationId: this.stationId,
UserId: this.userId,
ProductCategory: this.productCategory,
RequestQuantity: this.quantity,
remarkUser: this.remark || '',
remarkMasterInv: '',
status: this.status,
requestDate: this.requestDate,
approvalDate: null,
Document: this.document
};
$('.modal').modal('hide');
// Send the data to the API
const response = await fetch('/InvMainAPI/AddRequest', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (response.ok) {
this.resetForm();
alert('Success!', 'Request form has been successfully submitted.', 'success');
const requestItem = await response.json();
this.request.push(requestItem);
this.fetchRequest();
} else {
throw new Error('Failed to submit form.');
}
} catch (error) {
console.error('Error:', error);
alert('Inventory PSTW Error', `An error occurred: ${error.message}`, 'error');
}
},
initiateTable() {
let self = this;
function renderDocument(data, full) {
if (!data) return "No Document";
let isImage = /\.(jpeg|jpg|png|gif)$/i.test(data);
let isPdf = /\.pdf$/i.test(data);
if (isImage) {
return `<a href="${data}" target="_blank" data-lightbox="image-1">
<img src="${data}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
</a>`;
} else if (isPdf) {
return `<a href="${data}" target="_blank">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
alt="PDF Document" class="img-thumbnail"
style="width: 50px; height: 50px;" />
<br>View PDF
</a>`;
} else {
return `<a href="${data}" target="_blank">Download File</a>`;
}
}
function renderDeleteButton(data) {
return `<button type="button" class="btn btn-danger delete-btn" data-id="${data}">Delete</button>`;
}
this.pendingRequestDatatable = $('#requestDatatable').DataTable({
"data": this.request.filter(req => req.status === "Requested"),
"columns": [
{ "title": "Request ID", "data": "requestID", "createdCell": (td, cellData) => $(td).attr('id', `qr${cellData}`) },
{ "title": "Product Name", "data": "productName", "render": (data, type, full) => renderDocument(full.productPicture) },
{ "title": "Product Category", "data": "productCategory" },
{ "title": "Request Quantity", "data": "requestQuantity" },
{ "title": "Document / Picture", "data": "document", "render": (data, type, full) => renderDocument(data) },
{ "title": "Remark", "data": "remarkUser" },
{ "title": "Station Deploy", "data": "stationName", "render": (data) => data || "Self Assign" },
{ "title": "Request Date", "data": "requestDate" },
{ "title": "Status", "data": "status" },
{ "title": "Delete", "data": "requestID", "render": renderDeleteButton, "className": "align-middle" }
],
responsive: true,
});
this.settledRequestDatatable = $('#settledrequestDatatable').DataTable({
"data": this.request.filter(req => req.status !== "Requested"),
"columns": [
{ "title": "Request ID", "data": "requestID", "createdCell": (td, cellData) => $(td).attr('id', `qr${cellData}`) },
{ "title": "Status", "data": "status" },
{ "title": "Product Name", "data": "productName", "render": (data, type, full) => renderDocument(full.productPicture) },
{ "title": "Product Category", "data": "productCategory" },
{ "title": "Request Quantity", "data": "requestQuantity" },
{ "title": "Station Deploy", "data": "stationName", "render": (data) => data || "Self Assign" },
{ "title": "Document / Picture", "data": "document", "render": (data, type, full) => renderDocument(data) },
{ "title": "Remark", "data": "remarkUser" },
{ "title": "Remark (Master)", "data": "remarkMasterInv" },
{ "title": "Request Date", "data": "requestDate" },
{ "title": "Approval Date", "data": "approvalDate" }
],
responsive: true,
});
$('#requestDatatable tbody').off('click', '.delete-btn');
$('#requestDatatable tbody').on('click', '.delete-btn', function () {
const requestID = $(this).data('id');
self.deleteRequestItem(requestID);
});
this.loading = false;
}
async fetchRequest() {
try
{
const response = await fetch(`/InvMainAPI/ItemRequestListEachUser/${this.userId}`,
{
method: 'GET', // Specify the HTTP method
headers: {
'Content-Type': 'application/json', // Set content type
}
});
if (!response.ok) {
throw new Error('Failed to fetch request List');
}
this.request = await response.json();
if ($.fn.dataTable.isDataTable('#requestDatatable')) {
$('#requestDatatable').DataTable().clear().destroy();
}
if ($.fn.dataTable.isDataTable('#settledrequestDatatable')) {
$('#settledrequestDatatable').DataTable().clear().destroy();
}
this.initiateTable();
}
catch (error) {
console.error('Error fetching request List:', error);
}
},
async fetchProducts() {
try {
const response = await fetch('/InvMainAPI/ProductList', {
method: 'POST', // Specify the HTTP method
headers: {
'Content-Type': 'application/json', // Set content type
}
});
if (!response.ok) {
throw new Error('Failed to fetch products');
}
this.products = await response.json();
}
catch (error) {
console.error('Error fetching products:', error);
}
},
async fetchStation() {
try {
const response = await fetch('/InvMainAPI/StationList', {
method: 'POST', // Specify the HTTP method
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch Station');
}
const data = await response.json();
this.stations = data.filter(station => station.stationPicID === this.userId);
} catch (error) {
console.error('Error fetching Station:', error);
}
},
async fetchUser() {
try {
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
this.currentUser = data?.userInfo || null;
this.userId = await this.currentUser.id;
}
else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
resetForm() {
this.searchQuery = "";
this.stationId = "";
this.productId = "";
this.remark = "";
this.document = null;
this.quantity = 0;
this.status = "";
this.requestDate = null;
this.approvalDate = null;
this.productCategory = "";
this.assign = "",
this.productName = null;
this.selectedProduct = "";
this.selectedStation = "";
this.selectedCategory = "";
this.showRequestModal = false;
this.loading = false;
},
async deleteRequestItem(requestID) {
if (!confirm("Are you sure you want to delete this request?")) {
return false;
}
try {
const response = await fetch(`/InvMainAPI/DeleteRequest/${requestID}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (result.success) {
alert(result.message);
if ($.fn.dataTable.isDataTable('#requestDatatable')) {
const table = $('#requestDatatable').DataTable();
table.row($(`.delete-btn[data-id="${requestID}"]`).closest('tr')).remove().draw();
}
this.request = this.request.filter(req => req.requestID !== requestID);
} else {
alert(result.message);
}
}
catch (error) {
console.error("Error deleting item:", error);
alert("An error occurred while deleting the item.");
}
finally {
this.loading = false;
}
},
},
directives: {
clickOutside: {
beforeMount(el, binding) {
el.clickOutsideEvent = (event) => {
if (!(el.contains(event.target))) {
binding.value?.(); // Guna optional chaining untuk elak error
}
};
document.body.addEventListener("click", el.clickOutsideEvent);
},
unmounted(el) {
document.body.removeEventListener("click", el.clickOutsideEvent);
}
}
}
});
</script>
}

View File

@ -0,0 +1,828 @@
@{
ViewData["Title"] = "QR Scanner";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<style scoped>
.error {
font-weight: bold;
color: red;
}
.barcode-format-checkbox {
margin-right: 10px;
white-space: nowrap;
display: inline-block;
}
select {
width: 200px; /* Adjust width as needed */
padding: 5px;
font-size: 16px;
}
</style>
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartialUser.cshtml")
<div id="registerItem" class="row">
<div class="row card">
<div class="card-header">
<button v-if="displayStatus !== null" v-on:click="resetScanner" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Scanner
</button>
</div>
<div class="card-body">
<select v-if="displayStatus == null" class="form-select" v-model="selectedCameraId" v-on:change="updateCamera">
<option v-for="device in videoInputDevices" :key="device.deviceId" :value="device.deviceId">
{{ device.label || `Camera ${videoInputDevices.indexOf(device) + 1}` }}
</option>
</select>
<div id="registerItem" v-if="displayStatus == null" data-aos="fade-right">
<p style="text-align:center; padding:10px;">Scan QR Code Here:</p>
<qrcode-stream :constraints="selectedConstraints"
:formats="['qr_code']"
:track="trackFunctionSelected.value"
v-on:camera-on="onCameraReady"
v-on:detect="onDecode"
v-on:error="onError">
</qrcode-stream>
<p class="error">{{ error }}</p>
</div>
<!--RECEIVE OR RETURN INTERFACE -->
<div style="text-align: center; margin: 20px 0;" v-if="displayStatus === 'arrived' && thisItem.currentStationId != null">
<h2>Item Receive Information :</h2>
<h3>Station Assign</h3>
</div>
<div style="text-align: center; margin: 20px 0;" v-if="displayStatus === 'arrived' && thisItem.currentStationId == null">
<h2>Item Receive Information :</h2>
<h3>Self Assign</h3>
</div>
<div style="text-align: center; margin: 20px 0;" v-if="displayStatus === 'return' && thisItem.currentStationId != null">
<h2>Item Return Information :</h2>
<h3>Station</h3>
</div>
<div style="text-align: center; margin: 20px 0;" v-if="displayStatus === 'return' && thisItem.currentStationId == null">
<h2>Item Return Information :</h2>
<h3>Self Assign</h3>
</div>
<div v-if="displayStatus === 'arrived' || displayStatus === 'return'" style="display: flex; justify-content: center; align-items: center;">
<div class="col-lg-7 col-11 border rounded p-3 shadow-sm">
<div class="row m-3 d-flex align-items-center justify-content-center">
<div class="col-lg-7 col-11 border rounded p-3 shadow-sm">
<div class="col-12 text-center">
<img :src="thisItem.imageProduct" alt="Product Image" class="img-fluid rounded" data-toggle="modal" data-target="#imageModal" style="max-height: 300px;" />
</div>
<div class="col-12 text-center mt-3">
<p class="h4 fw-bold text-primary">{{ thisItem.uniqueID }}</p>
</div>
</div>
</div>
<!-- Item Name -->
<div class="col-12 mb-3">
<p class="h5 fw-bold">
<i class="fas fa-tag me-2 text-secondary"></i>Item Name:
<span class="text-muted">{{ thisItem.productName }}</span>
</p>
</div>
<!-- Part Number -->
<div class="col-12 mb-3">
<p class="h5 fw-bold">
<i class="fas fa-barcode me-2 text-secondary"></i>Part Number:
<span class="text-muted">{{ thisItem.partNumber }}</span>
</p>
</div>
<!-- Serial Number -->
<div class="col-12 mb-3">
<p class="h5 fw-bold">
<i class="fas fa-hashtag me-2 text-secondary"></i>Serial Number:
<span class="text-muted">{{ thisItem.serialNumber }}</span>
</p>
</div>
<!-- Station -->
<div class="col-12 mb-3">
<p class="h5 fw-bold">
<i class="fas fa-user-tie me-2 text-secondary"></i>PIC:
<span class="text-muted">{{thisItem.currentUserFullName}}</span>
</p>
</div>
<!-- Station -->
<div class="col-12 mb-3">
<p class="h5 fw-bold">
<i class="fas fa-user-tie me-2 text-secondary"></i>Station:
<span class="text-muted">{{thisItem.currentStation || 'No Station Deploy (Self Assign)' }}</span>
</p>
</div>
<!--RECIEVE INTERFACE -->
<div class="col-12" v-if="displayStatus === 'arrived'">
<div class="card shadow-sm border-0">
<div class="card-body">
<h5 class="card-title mb-4 text-primary">
<i class="fas fa-info-circle me-2"></i>Receiver Information
</h5>
<ul class="list-group list-group-flush">
<!-- User -->
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<span class="fw-bold">
<i class="fas fa-user me-2 text-secondary"></i>User:
</span>
<span class="text-muted text-end" style="max-width: 70%; word-wrap: break-word;">
{{ thisItem.currentUser }}
</span>
</div>
</li>
</ul>
</div>
</div>
</div>
<!--RETURN INTERFACE -->
<div class="col-12" v-if="displayStatus === 'return'">
<div class="card shadow-sm border-0">
<div class="card-body">
<h5 class="card-title mb-4 text-primary">
<i class="fas fa-info-circle me-2"></i>Sender Information
</h5>
<ul class="list-group list-group-flush">
<!-- User -->
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<span class="fw-bold">
<i class="fas fa-user me-2 text-secondary"></i>User:
</span>
<span class="text-muted text-end" style="max-width: 70%; word-wrap: break-word;">
{{ thisItem.currentUser }}
</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!--RECIEVE INTERFACE -->
<form v-on:submit.prevent="updateItemMovement" v-if="displayStatus === 'arrived'" data-aos="fade-right">
<div class=" register" data-aos="fade-right">
<div class="row" data-aos="fade-right">
@*Right Side*@
<div class="col-md-12">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<br><br>
@* Submit and Reset Buttons *@
<div class="form-group row">
<div class="col-sm-8 offset-sm-5">
<button type="button" v-on:click="receiveReturnMessage" class="btn btn-secondary m-1">Return Item</button>
<button type="submit" class="btn btn-primary m-1">Receive Item</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<!--RETURN INTERFACE -->
<form v-on:submit.prevent="" v-if="displayStatus === 'return'" data-aos="fade-right">
<div class=" register" data-aos="fade-right">
<div class="row" data-aos="fade-right">
@*Right Side*@
<div class="col-md-12">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<br><br>
@* Submit and Reset Buttons *@
<div class="form-group row">
<div class="col-sm-8 offset-sm-5">
<button type="submit" v-on:click="ReturnMessage" class="btn btn-primary m-1">Return Item</button>
<button type="submit" v-on:click="StationMessage" class="btn btn-primary m-1">{{ thisItem?.currentStationId == null ? "Deploy Station" : "Change Station" }}</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<!--STATION DEPLOY MESSAGE BOX INTERFACE-->
<div class="modal fade" id="stationMessage" tabindex="-1" role="dialog" aria-labelledby="stationModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="stationModalLabel">{{ thisItem?.currentStationId == null ? "Deploy Station" : "Change Station" }}</h5>
<button type="button" class="close" data-dismiss="modal" v-on:click="closeStationMessageModal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form v-on:submit.prevent="updateStationItemMovement">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Deploy Station : </label>
<div class="col-sm-8">
<select class="btn btn-primary dropdown-toggle col-md-10" v-model="selectedStation">
<option class="btn-light" value="" disabled selected>Select Station</option>
<option v-if="stationList.length === 0" class="btn-light" disabled>No Station Assigned to You</option>
<option class="btn-light" v-for="(station, index) in stationList" :key="index" :value="station.stationId">{{ station.stationName}}</option>
</select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Remark:</label>
<div class="col-sm-8">
<input type="text" class="form-control" v-model="remark" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Consignment Note:</label>
<div class="col-sm-8">
<input type="file" class="form-control-file" v-on:change="handleFileUpload" accept="image/png, image/jpeg, application/pdf" />
</div>
</div>
<button type="submit" class="btn btn-primary">Deploy Station</button>
</form>
</div>
</div>
</div>
</div>
<!--RETURN MESSAGE BOX INTERFACE-->
<div class="modal fade" id="returnMessage" tabindex="-1" role="dialog" aria-labelledby="returnModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="returnModalLabel">Return Item</h5>
<button type="button" class="close" data-dismiss="modal" v-on:click="closeMessageModal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form v-on:submit.prevent="returnItemMovement">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Remark:</label>
<div class="col-sm-8">
<input type="text" class="form-control" v-model="remark" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Consignment Note:</label>
<div class="col-sm-8">
<input type="file" class="form-control-file" v-on:change="handleFileUpload" accept="image/png, image/jpeg, application/pdf" />
</div>
</div>
<button type="submit" class="btn btn-primary">Return Item</button>
</form>
</div>
</div>
</div>
</div>
<!--RECEIVE THEN RETURN INTERFACE -->
<div class="modal fade" id="returnModal" tabindex="-1" role="dialog" aria-labelledby="returnModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="returnModalLabel">Return Item</h5>
<button type="button" class="close" data-dismiss="modal" v-on:click="closeModal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form v-on:submit.prevent="receiveReturnAPI">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Remark:</label>
<div class="col-sm-8">
<input type="text" class="form-control" v-model="remark" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Consignment Note:</label>
<div class="col-sm-8">
<input type="file" class="form-control-file" v-on:change="handleFileUpload" accept="image/png, image/jpeg, application/pdf" />
</div>
</div>
<button type="submit" class="btn btn-primary">Return Item</button>
</form>
</div>
</div>
</div>
</div>
<!-- ALREADY RETURN ITEM INTERFACE -->
<div v-if="displayStatus === 'requestAgain'" class="d-flex justify-content-center align-items-center vh-100">
<div class="col-lg-10 col-11 border rounded p-5 shadow-lg text-center min-vh-50">
<h1 class="text-danger">The item has been register as returned.</h1>
<h3 class="text-muted">You need to request this item again to used it legally.</h3>
</div>
</div>
<!-- NOT SIGN IN ITEM INTERFACE -->
<div v-if="displayStatus === 'differentUser'" class="d-flex justify-content-center align-items-center vh-100">
<div class="col-lg-10 col-11 border rounded p-5 shadow-lg text-center min-vh-50">
<h1 class="text-danger">The item is not assigned to you.</h1>
<h3 class="text-muted">You need to request this item to validly use it.</h3>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script src="~/js/vue-qrcode-reader.umd.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
thisItem: null,
selectedUser: "",
selectedStation: "",
stationList: [],
displayStatus: null,
debounceTime: 500,
currentUser: null,
movementId: null,
currentUserId: null,
remark: null,
consignmentNote: null,
receiveReturn: null,
UniqueID: null,
InventoryMasterId: null,
//QR VARIABLE
qrCodeResult: null,
debounceTimeout: null,
error: "",
selectedConstraints: { facingMode: "user" },
trackFunctionSelected: { text: 'outline', value: null },
barcodeFormats: {
qr_code: true, // Hanya mendukung QR Code
code_128: true,
ean_13: true
},
constraintOptions: [
{ label: "Rear Camera", constraints: { facingMode: "environment" } },
{ label: "Front Camera", constraints: { facingMode: "user" } }
],
videoInputDevices: [],
selectedCameraId: null,
scanStartTime: null,
scanTime: null
};
},
async mounted() {
this.trackFunctionSelected.value = this.paintOutline;
await this.fetchUser();
await Promise.all([
this.fetchStation(),
]);
},
methods: {
handleFileUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.consignmentNote = e.target.result.split(',')[1]; // Get Base64 string (remove metadata)
};
reader.readAsDataURL(file);
} else {
this.consignmentNote = null;
}
},
async updateStationItemMovement() {
const requiredFields = ['selectedStation'];
for (let field of requiredFields) {
if (!this[field]) {
alert(`Request Error: Please fill in required field ${field}.`, 'warning');
return;
}
}
try {
const now = new Date();
const formData = {
ItemId: this.thisItem.itemID,
ToStation: this.thisItem.currentStationId,
ToStore: this.thisItem.toStore,
ToUser: this.currentUserId,
ToOther: "Delivered",
SendDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
Action: "Assign",
Quantity: this.thisItem.quantity,
Remark: this.remark,
ConsignmentNote: this.consignmentNote,
Date: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
LastUser: this.currentUserId,
LastStore: this.thisItem.toStore,
LastStation: this.selectedStation,
LatestStatus: "Delivered",
ReceiveDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
MovementComplete: true,
};
const response = await fetch('/InvMainAPI/StationItemMovementUser', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
if (response.ok) {
this.thisItem = await response.json();
this.fetchItem(this.thisItem.uniqueID);
alert('Success! Item assign to the Station.');
$('#stationMessage').modal('hide');
this.displayStatus = "return";
} else {
throw new Error('Failed to submit form.');
}
} catch (error) {
console.error('Error:', error);
alert('Inventory PSTW Error: An error occurred.');
}
},
async updateItemMovement() {
if (this.receiveReturn == null) {
if (!confirm("Are you sure you already received this item?")) {
return false;
}
}
try {
const now = new Date();
const formData = {
Id: this.thisItem.id,
LastStore: this.thisItem.toStore,
LatestStatus: "Delivered",
ReceiveDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
MovementComplete: true,
};
const response = await fetch('/InvMainAPI/UpdateItemMovementUser', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
if (response.ok) {
if (this.receiveReturn == null) {
alert('Success! Item has been successfully received.');
this.thisItem = await response.json();
this.displayStatus = "return";
this.fetchItem(this.UniqueID);
this.resetForm();
} else {
this.returnItemMovement();
}
} else {
throw new Error('Failed to submit form.');
}
} catch (error) {
console.error('Error:', error);
alert('Inventory PSTW Error: An error occurred.');
}
},
async returnItemMovement() {
if (!confirm("Are you sure you want to return this item?")) {
return false;
}
try {
const now = new Date();
const formData = {
ItemId: this.thisItem.itemID,
ToStation: this.thisItem.currentStationId,
ToStore: this.thisItem.currentStoreId,
ToUser: this.currentUserId,
ToOther: "Return",
SendDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
Action: "StockIn",
Quantity: this.thisItem.quantity,
Remark: this.remark,
ConsignmentNote: this.consignmentNote,
Date: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
LastUser: this.InventoryMasterId,
LastStore: this.thisItem.toStore,
LastStation: this.thisItem.toStation,
LatestStatus: null,
ReceiveDate: null,
MovementComplete: false,
};
const response = await fetch('/InvMainAPI/ReturnItemMovementUser', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
if (response.ok) {
alert('Success! Item is on the delivery to return to Inventory Master.');
this.thisItem = await response.json();
$('#returnModal').modal('hide');
$('#returnMessage').modal('hide');
this.displayStatus = "requestAgain";
this.resetForm();
} else {
throw new Error('Failed to submit form.');
}
} catch (error) {
console.error('Error:', error);
alert('Inventory PSTW Error: An error occurred.');
}
},
async fetchItem(itemid) {
try {
const response = await fetch('/InvMainAPI/GetItem/' + itemid, {
method: 'POST',
}
);
if (response.ok) {
this.thisItem = await response.json();
this.fetchStore(this.thisItem.toStore);
if (this.thisItem.movementId != null && this.thisItem.toOther === "On Delivery" && this.thisItem.latestStatus == null && this.thisItem.currentUserId == this.currentUserId && this.thisItem.movementComplete == 0) {
this.displayStatus = "arrived";
} else if (this.thisItem.movementId != null && this.thisItem.latestStatus != null && this.thisItem.currentUserId == this.currentUserId && this.thisItem.latestStatus != "Ready To Deploy") {
this.displayStatus = "return";
} else if (this.thisItem.movementId != null && this.thisItem.toOther === "Return" && this.thisItem.latestStatus == null && this.thisItem.toUser == this.currentUserId) {
this.displayStatus = "requestAgain";
} else {
this.displayStatus = "differentUser";
this.thisItem = null;
}
} else {
this.error = 'Qr Code Not Register to the system';
}
} catch (error) {
console.error('Error fetching item information:', error);
}
},
async fetchStation() {
try {
const response = await fetch('/InvMainAPI/StationList', {
method: 'POST', // Specify the HTTP method
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch suppliers');
}
const data = await response.json();
this.stationList = data.filter(station => station.stationPicID === this.currentUserId);
} catch (error) {
console.error('Error fetching suppliers:', error);
}
},
async fetchStore(storeId) {
try {
const response = await fetch('/InvMainAPI/StoreSpecificMaster/' + storeId, {
method: 'POST', // Specify the HTTP method
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch Store');
}
const data = await response.json();
this.InventoryMasterId = data.userId;
} catch (error) {
console.error('Error fetching suppliers:', error);
}
},
async fetchUser() {
try {
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
this.currentUser = data?.userInfo || null;
this.currentUserId = this.currentUser.id;
}
else {
console.error('Failed to fetch user');
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
resetScanner() {
this.displayStatus = null;
this.qrCodeResult = null;
this.thisItem = null;
},
resetForm() {
this.selectedStation = null;
},
async receiveReturnAPI() {
this.receiveReturn = 1;
this.updateItemMovement();
},
// Split Url dapatkan unique ID Je
onDecode(detectedCodes) {
if (detectedCodes.length > 0) {
this.qrCodeResult = detectedCodes[0].rawValue; // Ambil URL dari rawValue
this.UniqueID = this.qrCodeResult.split('/').pop(); // Ambil UniqueID dari URL
this.fetchItem(this.UniqueID);
}
},
//Showing Qr Error
onError(err) {
let message = `[${err.name}]: `;
if (err.name === "NotAllowedError") {
message += "You have to allow camera accecss.";
} else if (err.name === "NotFoundError") {
message += "There's no camera detect.";
} else if (err.name === "NotReadableError") {
message += "You are using camera on the other application.";
} else {
message += err.message;
}
this.error = message;
},
//Setting Camera to know that camera are turn on or not
async onCameraReady(videoElement) {
const video = document.querySelector("video");
const track = video.srcObject.getVideoTracks()[0];
if (track && track.getCapabilities) {
const capabilities = track.getCapabilities(); // Get camera capabilities
if (capabilities.sharpness) {
track.applyConstraints({
advanced: [{ width: 1280, height: 720 }]
}).then(() => {
// Step 2: Apply sharpness separately
return track.applyConstraints({ advanced: [{ sharpness: 10 }] });
}).then(() => {
return track.applyConstraints({ advanced: [{ exposureMode: 'continuous' }] });
})
.then(() => {
}).catch(err => console.error("Failed to apply constraints:", err));
} else {
console.warn("⚠️ Sharpness not supported on this camera");
}
}
try {
const devices = await navigator.mediaDevices.enumerateDevices();
this.videoInputDevices = devices.filter(device => device.kind === 'videoinput');
if (this.videoInputDevices.length > 0) {
// Keep the selected camera if already chosen
if (!this.selectedCameraId) {
this.selectedCameraId = this.videoInputDevices[0].deviceId;
}
this.selectedConstraints = { deviceId: { exact: this.selectedCameraId } };
} else {
this.error = "No camera detected.";
}
} catch (err) {
this.error = "Error accessing camera: " + err.message;
}
},
//Update Camera Category
updateCamera() {
this.selectedConstraints = { deviceId: { exact: this.selectedCameraId } };
},
//Red box if QR Detect
paintOutline(detectedCodes, ctx) {
for (const detectedCode of detectedCodes) {
const [firstPoint, ...otherPoints] = detectedCode.cornerPoints;
ctx.strokeStyle = 'red'; // Warna garis merah
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(firstPoint.x, firstPoint.y);
for (const { x, y } of otherPoints) {
ctx.lineTo(x, y);
}
ctx.lineTo(firstPoint.x, firstPoint.y);
ctx.closePath();
ctx.stroke();
}
},
//Ni return message
receiveReturnMessage() {
$("#returnModal").modal("show");
},
closeModal() {
$('#returnModal').modal('hide'); // Manually hide the modal
},
ReturnMessage() {
$("#returnMessage").modal("show");
},
closeMessageModal() {
$('#returnMessage').modal('hide'); // Manually hide the modal
},
StationMessage() {
$("#stationMessage").modal("show");
},
closeStationMessageModal() {
$('#stationMessage').modal('hide'); // Manually hide the modal
},
},
});
app.component("qrcode-stream", VueQrcodeReader.QrcodeStream);
app.mount('#registerItem');
$(function () {
// Attach event listener to show modal
$('#addItemBtn').on('click', function () {
$('#registerItemModal').modal('show');
});
// Close modals
$('.closeModal').on('click', function () {
$('.modal').modal('hide');
});
});
</script>
}

View File

@ -0,0 +1,90 @@
@{
ViewData["Title"] = "Dashboard";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div class="container" id="invUser">
<div class="row">
<div class="text-center">
<p><h1 class="display-4">Inventory User Dashboard</h1></p>
<p v-show="currentUserCompanyDept.departmentName"><h2 class="display-6">Store: {{ currentUserCompanyDept.departmentName }}</h2></p>
</div>
</div>
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartialUser.cshtml")
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
$(function () {
app.mount('#invUser');
$('.closeModal').on('click', function () {
// Show the modal with the ID 'addManufacturerModal'.
$('.modal').modal('hide');
});
});
const app = Vue.createApp({
data() {
return {
currentUser: null,
currentUserCompanyDept: {
departmentName: null,
departmentId: null
},
reportData: null,
}
},
mounted() {
this.fetchUser();
},
methods: {
async fetchUser() {
try {
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
this.currentUser = data?.userInfo || null;
const companyDeptData = await this.currentUser.department;
const userRole = this.currentUser.role;
if(userRole == "SuperAdmin" || userRole == "SystemAdmin"){
this.currentUserCompanyDept = {departmentId : 0, departmentName : "All"}
this.fetchInventoryReport(0);
}
else{
this.currentUserCompanyDept = companyDeptData;
this.fetchInventoryReport(companyDeptData.departmentId);
}
}
else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
async fetchInventoryReport(deptId){
try {
const response = await fetch(`/InvMainAPI/GetInventoryReport/` + deptId, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
this.reportData = data;
}
else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
},
});
</script>
}

View File

@ -0,0 +1,11 @@
@{
ViewData["Title"] = "PSTW Centralized System";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row">
<div class="text-center">
<h1 class="display-4">Welcome To Invetory Module</h1>
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
</div>

View File

@ -0,0 +1,102 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div class="row">
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="Inventory" asp-controller="InventoryMaster" asp-action="SupplierRegistration">
<div class="box bg-info text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-truck"></i>
</h1>
<h6 class="text-white">Supplier</h6>
</div>
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="Inventory" asp-controller="InventoryMaster" asp-action="ManifacturerRegistration">
<div class="box bg-warning text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-factory"></i>
</h1>
<h6 class="text-white">Manufacturer</h6>
</div>
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<a href="@Url.Action("ProductRegistration", "InventoryMaster", new { area = "Inventory" })">
<div class="card card-hover">
<div class="box bg-cyan text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-checkbox-multiple-blank"></i>
</h1>
<h6 class="text-white">Product</h6>
</div>
</div>
</a>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="Inventory" asp-controller="InventoryMaster" asp-action="ItemRegistration">
<div class="box bg-success text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-checkbox-multiple-blank-circle"></i>
</h1>
<h6 class="text-white">Item</h6>
</div>
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="Inventory" asp-controller="InventoryMaster" asp-action="StationRegistration">
<div class="box bg-warning text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-truck"></i>
</h1>
<h6 class="text-white">Station</h6>
</div>
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="Inventory" asp-controller="InventoryMaster" asp-action="ItemMovement">
<div class="box bg-cyan text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-factory"></i>
</h1>
<h6 class="text-white">Item Movement</h6>
</div>
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="Inventory" asp-controller="InventoryMaster" asp-action="ItemRequestMaster">
<div class="box bg-info text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-checkbox-multiple-blank"></i>
</h1>
<h6 class="text-white">Product Request</h6>
</div>
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="Inventory" asp-controller="InventoryMaster" asp-action="QrMaster">
<div class="box bg-success text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-checkbox-multiple-blank-circle"></i>
</h1>
<h6 class="text-white">Scan Items</h6>
</div>
</a>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More