nexus-shared 1.1.17 → 1.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/src/api-services/system-service.tsx +1 -2
  3. package/src/client.ts +3 -2
  4. package/src/components/documents/button.tsx +5 -1
  5. package/src/components/inputs/input-box.tsx +4 -2
  6. package/src/components/inputs/input-form.tsx +126 -14
  7. package/src/components/layouts/global-layout.tsx +4 -2
  8. package/src/components/layouts/layout-helpers.tsx +23 -23
  9. package/src/helpers/browser-helpers.tsx +41 -1
  10. package/src/helpers/input-helpers.tsx +34 -4
  11. package/src/helpers/utility-helpers.tsx +1 -0
  12. package/src/helpers/validation-helpers.tsx +302 -0
  13. package/src/index.ts +3 -18
  14. package/src/interface.ts +4 -1
  15. package/src/interfaces/button-interfaces.tsx +2 -2
  16. package/src/interfaces/exception-interfaces.tsx +99 -0
  17. package/src/interfaces/http-interfaces.tsx +120 -3
  18. package/src/interfaces/input-interfaces.tsx +61 -11
  19. package/src/interfaces/message-interfaces.tsx +32 -0
  20. package/src/interfaces/permission-interfaces.tsx +4 -4
  21. package/src/interfaces/storage-interfaces.tsx +1 -0
  22. package/src/interfaces/system-interfaces.tsx +2 -2
  23. package/src/interfaces/type-interfaces.tsx +8 -2
  24. package/src/interfaces/user-interfaces.tsx +17 -0
  25. package/src/nexus.environments.tsx +27 -17
  26. package/src/proxy-api/proxy-backend.tsx +60 -0
  27. package/src/proxy-api/proxy-constants.tsx +28 -0
  28. package/src/proxy-api/proxy-helpers.tsx +91 -0
  29. package/src/proxy-api-client.tsx +1 -0
  30. package/src/proxy-api-server.tsx +2 -0
  31. package/src/services/http-client-services.tsx +75 -0
  32. package/src/services/http-services.tsx +158 -0
  33. package/src/services/loader-service.tsx +5 -4
  34. package/src/services/localstorage-service.tsx +59 -0
  35. package/src/services/message-services.tsx +383 -0
  36. package/src/sso-config/auth-token-validation.ts +20 -0
  37. package/src/sso-config/callback-route.tsx +34 -0
  38. package/src/sso-config/cookie-encryption.ts +51 -0
  39. package/src/sso-config/cookie-helpers.tsx +77 -0
  40. package/src/sso-config/forgot-password-route.tsx +36 -0
  41. package/src/sso-config/oauth-callback-state.ts +58 -0
  42. package/src/sso-config/pkce-helpers.ts +16 -0
  43. package/src/sso-config/provider-complete-route.tsx +15 -0
  44. package/src/sso-config/redirect-context-actions.tsx +46 -0
  45. package/src/sso-config/redirect-context-helpers.tsx +144 -0
  46. package/src/sso-config/redirect-context-persist.tsx +18 -0
  47. package/src/sso-config/redirect-context-route.tsx +45 -0
  48. package/src/sso-config/refresh-route.tsx +22 -0
  49. package/src/sso-config/sign-in-route.tsx +69 -0
  50. package/src/sso-config/sign-out-route.tsx +53 -0
  51. package/src/sso-config/sign-up-route.tsx +46 -0
  52. package/src/sso-config/sso-backend-flow.ts +45 -0
  53. package/src/sso-config/sso-complete-route.tsx +46 -0
  54. package/src/sso-config/sso-config.ts +57 -0
  55. package/src/sso-config/sso-interfaces.tsx +68 -0
  56. package/src/sso-config/sso-response-helpers.tsx +20 -0
  57. package/src/sso-server.tsx +21 -1
  58. package/src/styles/nexus.dialog.css +3 -0
@@ -0,0 +1,53 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { NEXUS_INFO, URLS } from "../nexus.environments";
3
+ import { HttpPost } from "../services/http-services";
4
+ import { AUTH_COOKIES, ClearAllAuthCookies, GetDecryptedRequestCookie } from "./cookie-helpers";
5
+ import { SanitizeReturnPath } from "./redirect-context-helpers";
6
+ import { SSO_CONFIG } from "./sso-config";
7
+
8
+ function hasAuthSession(req: NextRequest): boolean {
9
+ return !!(req.cookies.get(AUTH_COOKIES.ssoSession)?.value || req.cookies.get(AUTH_COOKIES.accessToken)?.value || req.cookies.get(AUTH_COOKIES.refreshToken)?.value);
10
+ }
11
+
12
+ function resolveReturnUrl(req: NextRequest): URL {
13
+ if (NEXUS_INFO.IS_ADMIN_MODULE && !hasAuthSession(req)) {
14
+ return new URL(URLS.SIGN_IN);
15
+ }
16
+
17
+ const appBase = req.nextUrl.origin ?? URLS.CURRENT!;
18
+ const returnPath = req.nextUrl.searchParams.get("returnUrl");
19
+ if (returnPath) {
20
+ return new URL(SanitizeReturnPath(returnPath), appBase);
21
+ }
22
+
23
+ return new URL(appBase);
24
+ }
25
+
26
+ async function revokeServerSession(req: NextRequest) {
27
+ const ssoSession = GetDecryptedRequestCookie(req, AUTH_COOKIES.ssoSession);
28
+ if (!ssoSession) return;
29
+
30
+ const response = await HttpPost(SSO_CONFIG.BACKEND_ENDPOINTS.SIGN_OUT, { ssoSession });
31
+
32
+ return response.isSuccess;
33
+ }
34
+
35
+ /** GET — clear cookies and redirect back to the originating app page. */
36
+ export async function SignOutGetRoute(req: NextRequest) {
37
+ await revokeServerSession(req);
38
+ const response = NextResponse.redirect(resolveReturnUrl(req));
39
+ ClearAllAuthCookies(response);
40
+ return response;
41
+ }
42
+
43
+ /** POST — clear cookies and return JSON (API clients). */
44
+ export async function SignOutPostRoute(req: NextRequest) {
45
+ if (NEXUS_INFO.IS_ADMIN_MODULE && !hasAuthSession(req)) {
46
+ return NextResponse.json({ ok: false, redirect: URLS.SIGN_IN }, { status: 401 });
47
+ }
48
+
49
+ await revokeServerSession(req);
50
+ const response = NextResponse.json({ ok: true });
51
+ ClearAllAuthCookies(response);
52
+ return response;
53
+ }
@@ -0,0 +1,46 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { HttpPost } from "../services/http-services";
3
+ import { SetSsoSessionCookie } from "./cookie-helpers";
4
+ import { SSO_CONFIG } from "./sso-config";
5
+ import { ISignUpRequest } from "./sso-interfaces";
6
+ import { ParseWrappedResult } from "./sso-response-helpers";
7
+
8
+ const mapGender = (gender?: string | null) => {
9
+ const normalized = gender?.trim().toLowerCase();
10
+ if (normalized === "male") return 0;
11
+ if (normalized === "female") return 1;
12
+ return 2;
13
+ };
14
+
15
+ /** POST — register a new account (identity provider app). */
16
+ export async function SignUpPostRoute(req: NextRequest) {
17
+ const body = (await req.json()) as ISignUpRequest & { confirmPassword?: string };
18
+
19
+ if (!body.fullName?.trim() || !body.username?.trim() || !body.email?.trim() || !body.password || !body.confirmPassword) {
20
+ return NextResponse.json({ error: "All required fields must be provided." }, { status: 400 });
21
+ }
22
+
23
+ if (body.password !== body.confirmPassword) {
24
+ return NextResponse.json({ error: "Passwords do not match." }, { status: 400 });
25
+ }
26
+
27
+ const signUpRes = await HttpPost(SSO_CONFIG.BACKEND_ENDPOINTS.SIGN_UP, {
28
+ fullname: body.fullName.trim(),
29
+ username: body.username.trim(),
30
+ password: body.password,
31
+ confirmPassword: body.confirmPassword,
32
+ genderType: mapGender(body.gender),
33
+ emailAddress: body.email.trim(),
34
+ contactNumber: null,
35
+ clientSystemName: SSO_CONFIG.BACKEND_CONFIG.CLIENT_SYSTEM_NAME,
36
+ });
37
+
38
+ const signUpResult = ParseWrappedResult<{ ssoSession: string }>(signUpRes);
39
+ if (!signUpRes.isSuccess || !signUpResult?.ssoSession) {
40
+ return NextResponse.json(signUpRes, { status: signUpRes.code ?? 400 });
41
+ }
42
+
43
+ const response = NextResponse.json(signUpRes);
44
+ SetSsoSessionCookie(response, signUpResult.ssoSession, SSO_CONFIG.BACKEND_CONFIG.SSO_SESSION_MAX_AGE);
45
+ return response;
46
+ }
@@ -0,0 +1,45 @@
1
+ import { HttpPost } from "../services/http-services";
2
+ import { PKCE_METHOD_S256, PkcePair } from "./pkce-helpers";
3
+ import { SSO_CONFIG } from "./sso-config";
4
+ import { SSOAuthResponse } from "./sso-interfaces";
5
+ import { ParseSsoCodeResponse, ParseTokenResponse } from "./sso-response-helpers";
6
+
7
+ export type SsoExchangeInput = {
8
+ ssoSession: string;
9
+ redirectUri: string;
10
+ state?: string;
11
+ returnPath?: string;
12
+ pkce: PkcePair;
13
+ };
14
+
15
+ export type TokenExchangeInput = {
16
+ code: string;
17
+ redirectUri: string;
18
+ codeVerifier: string;
19
+ };
20
+
21
+ /** Server-only: request an authorization code using client policy + PKCE. */
22
+ export async function RequestAuthorizationCode(input: SsoExchangeInput): Promise<string | null> {
23
+ const ssoRes = await HttpPost(SSO_CONFIG.BACKEND_ENDPOINTS.SSO, {
24
+ ssoSession: input.ssoSession,
25
+ redirectUri: input.redirectUri,
26
+ state: input.state,
27
+ returnPath: input.returnPath,
28
+ codeChallenge: input.pkce.challenge,
29
+ codeChallengeMethod: input.pkce.method ?? PKCE_METHOD_S256,
30
+ });
31
+
32
+ return ParseSsoCodeResponse(ssoRes);
33
+ }
34
+
35
+ /** Server-only: exchange authorization code for tokens. */
36
+ export async function ExchangeAuthorizationCode(input: TokenExchangeInput): Promise<SSOAuthResponse | null> {
37
+ const tokenRes = await HttpPost(SSO_CONFIG.BACKEND_ENDPOINTS.TOKEN, {
38
+ code: input.code,
39
+ redirectUri: input.redirectUri,
40
+ clientSystemName: SSO_CONFIG.BACKEND_CONFIG.CLIENT_SYSTEM_NAME,
41
+ codeVerifier: input.codeVerifier,
42
+ });
43
+
44
+ return ParseTokenResponse(tokenRes);
45
+ }
@@ -0,0 +1,46 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { AUTH_COOKIES, GetDecryptedRequestCookie } from "./cookie-helpers";
3
+ import { EncodeOAuthCallbackState } from "./oauth-callback-state";
4
+ import { BuildAuthenticationSignInUrl } from "./sso-config";
5
+ import { ClearRedirectContextCookie, DecodeRedirectContext } from "./redirect-context-helpers";
6
+ import { PKCE_METHOD_S256, PkcePair } from "./pkce-helpers";
7
+ import { RequestAuthorizationCode } from "./sso-backend-flow";
8
+
9
+ function buildSignInRedirect(redirectContext: ReturnType<typeof DecodeRedirectContext>) {
10
+ return new URL(BuildAuthenticationSignInUrl(redirectContext?.returnPath));
11
+ }
12
+
13
+ /** Completes SSO after credentials sign-in: exchange session for code and redirect to the client callback. */
14
+ export async function SsoCompleteRoute(req: NextRequest) {
15
+ const ssoSession = GetDecryptedRequestCookie(req, AUTH_COOKIES.ssoSession);
16
+ const redirectContext = DecodeRedirectContext(req.cookies.get(AUTH_COOKIES.redirectContext)?.value);
17
+
18
+ if (!ssoSession || !redirectContext?.redirectUri) {
19
+ return NextResponse.redirect(buildSignInRedirect(redirectContext));
20
+ }
21
+
22
+ const pkce: PkcePair = {
23
+ verifier: redirectContext.pkceVerifier,
24
+ challenge: redirectContext.codeChallenge,
25
+ method: (redirectContext.codeChallengeMethod as PkcePair["method"]) ?? PKCE_METHOD_S256,
26
+ };
27
+
28
+ try {
29
+ const code = await RequestAuthorizationCode({
30
+ ssoSession,
31
+ redirectUri: redirectContext.redirectUri,
32
+ state: redirectContext.state,
33
+ returnPath: redirectContext.returnPath,
34
+ pkce,
35
+ });
36
+ if (!code) return NextResponse.redirect(buildSignInRedirect(redirectContext));
37
+
38
+ const callbackState = EncodeOAuthCallbackState(redirectContext.state, redirectContext.returnPath, pkce.verifier);
39
+ const location = `${redirectContext.redirectUri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(callbackState)}`;
40
+ const response = NextResponse.redirect(location);
41
+ ClearRedirectContextCookie(response);
42
+ return response;
43
+ } catch {
44
+ return NextResponse.redirect(buildSignInRedirect(redirectContext));
45
+ }
46
+ }
@@ -0,0 +1,57 @@
1
+ import { APIS, URLS } from "nexus-shared";
2
+
3
+ const _callbackPath = `${URLS.CURRENT}/api/auth/callback`;
4
+
5
+ export const SSO_CONFIG = {
6
+ COOKIE_CONFIG: {
7
+ COOKIE_SECRET: process.env.SSO_COOKIE_SECRET as string,
8
+ },
9
+ BACKEND_ENDPOINTS: {
10
+ SIGN_IN: `${APIS.AUTHENTICATION}/Authentication/SignIn`,
11
+ SIGN_UP: `${APIS.AUTHENTICATION}/Authentication/SignUp`,
12
+ SSO: `${APIS.AUTHENTICATION}/Authentication/SSO`,
13
+ TOKEN: `${APIS.AUTHENTICATION}/Authentication/Token`,
14
+ REFRESH: `${APIS.AUTHENTICATION}/Authentication/Refresh`,
15
+ SIGN_OUT: `${APIS.AUTHENTICATION}/Authentication/SignOut`,
16
+ PROVIDERS: `${APIS.AUTHENTICATION}/Authentication/Providers`,
17
+ FORGOT_PASSWORD: `${APIS.AUTHENTICATION}/Authentication/ForgotPassword`,
18
+ RESET_PASSWORD: `${APIS.AUTHENTICATION}/Authentication/ResetPassword`,
19
+ /** Used by VerifyAccessToken — returns 200 for a valid Bearer token, 401 otherwise. */
20
+ VERIFY: `${APIS.AUTHENTICATION}/Authentication/Verify`,
21
+ PROVIDER_CHALLENGE: (provider: string, returnUrl: string, clientSystemName: string) => `${APIS.AUTHENTICATION}/Provider/${provider}/Challenge?returnUrl=${encodeURIComponent(returnUrl)}&clientSystemName=${encodeURIComponent(clientSystemName)}`,
22
+ },
23
+ BACKEND_CONFIG: {
24
+ /** Matches AuthenticationOptions.DefaultClientSystemName (NEXUS_WEB_APPLICATION_API). */
25
+ CLIENT_SYSTEM_NAME: process.env.AUTH_CLIENT_SYSTEM_NAME as string,
26
+ /** Matches ClientApplications.SsoSessionLifetime (30 minutes). */
27
+ SSO_SESSION_MAX_AGE: process.env.SSO_SESSION_MAX_AGE as string,
28
+ /** How long redirect_uri/state survive in a secure httpOnly cookie on the auth app (seconds). */
29
+ REDIRECT_CONTEXT_MAX_AGE: process.env.REDIRECT_CONTEXT_MAX_AGE as string,
30
+ },
31
+ FRONTEND_ENDPOINTS: {
32
+ CALLBACK: _callbackPath,
33
+ SIGN_IN: `${URLS.CURRENT}/api/auth/sign-in`,
34
+ SIGN_UP: `${URLS.CURRENT}/api/auth/sign-up`,
35
+ SIGN_OUT: `${URLS.CURRENT}/api/auth/sign-out`,
36
+ REFRESH: `${URLS.CURRENT}/api/auth/refresh`,
37
+ SSO_COMPLETE: `${URLS.CURRENT}/api/auth/sso-complete`,
38
+ PROVIDER_COMPLETE: `${URLS.CURRENT}/api/auth/provider-complete`,
39
+ FORGOT_PASSWORD: `${URLS.CURRENT}/api/auth/forgot-password`,
40
+ RESET_PASSWORD: `${URLS.CURRENT}/api/auth/reset-password`,
41
+ },
42
+ } as const;
43
+
44
+ /** Client-app entry URL for centralized sign-in (includes redirect_uri + optional return path). */
45
+ export function BuildAuthenticationSignInUrl(returnPath?: string) {
46
+ const entryUrl = new URL(`${URLS.AUTHENTICATION}/api/auth/redirect-context`);
47
+ entryUrl.searchParams.set("redirect_uri", SSO_CONFIG.FRONTEND_ENDPOINTS.CALLBACK);
48
+ if (returnPath) entryUrl.searchParams.set("state", returnPath);
49
+ return entryUrl.toString();
50
+ }
51
+
52
+ /** Sign-out URL that returns the user to a page on this app after cookies are cleared. */
53
+ export function BuildSignOutUrl(returnPath?: string) {
54
+ const signOutUrl = new URL(SSO_CONFIG.FRONTEND_ENDPOINTS.SIGN_OUT);
55
+ if (returnPath) signOutUrl.searchParams.set("returnUrl", returnPath);
56
+ return signOutUrl.toString();
57
+ }
@@ -0,0 +1,68 @@
1
+ import { UN } from "../interfaces/type-interfaces";
2
+ export interface SSOAuthResponse {
3
+ accessToken: string;
4
+ refreshToken: string;
5
+ expiresTokenIn: number;
6
+ expiresRefreshTokenIn: number;
7
+ returnPath?: string;
8
+ }
9
+
10
+ export interface CookieInfo {
11
+ ssoSession: string;
12
+ accessToken: string | UN;
13
+ refreshToken: string | UN;
14
+ }
15
+
16
+ export interface IRedirectContext {
17
+ redirectUri: string;
18
+ /** OAuth CSRF nonce sent to the client callback. */
19
+ state: string;
20
+ /** Relative path to return to after sign-in completes. */
21
+ returnPath: string;
22
+ /** PKCE code challenge (stored server-side until SSO). */
23
+ codeChallenge: string;
24
+ codeChallengeMethod: string;
25
+ /** PKCE verifier (server-side only; also embedded in signed callback state). */
26
+ pkceVerifier: string;
27
+ }
28
+
29
+ export interface ISignInParams {
30
+ redirect_uri: string | UN;
31
+ state: string | UN;
32
+ }
33
+
34
+ export interface ISsoSessionResponse {
35
+ code: string;
36
+ }
37
+
38
+ export interface ILoginRequest {
39
+ username: string;
40
+ password: string;
41
+ rememberMe: boolean;
42
+ clientSystemName: string;
43
+ }
44
+
45
+ export interface ILoginResponse {
46
+ ssoSession: string;
47
+ displayName: string;
48
+ role: string;
49
+ userId: number;
50
+ redirectUrl?: string;
51
+ }
52
+
53
+ export interface ISignUpRequest {
54
+ fullName: string;
55
+ gender?: string | UN;
56
+ username: string;
57
+ email: string;
58
+ password: string;
59
+ }
60
+
61
+ export interface IDemoUserInfo {
62
+ id: number;
63
+ username: string;
64
+ email: string;
65
+ displayName: string;
66
+ role: string;
67
+ passwordHint: string;
68
+ }
@@ -0,0 +1,20 @@
1
+ import { IErrorRespose, ISuccessResponse } from "../interfaces/http-interfaces";
2
+ import { ISsoSessionResponse, SSOAuthResponse } from "./sso-interfaces";
3
+
4
+ type ApiResponse = ISuccessResponse | IErrorRespose;
5
+
6
+ export function ParseWrappedResult<T>(response: ApiResponse): T | null {
7
+ if (!response.isSuccess) return null;
8
+ return (response.result ?? null) as T | null;
9
+ }
10
+
11
+ export function ParseSsoCodeResponse(response: ApiResponse): string | null {
12
+ const result = ParseWrappedResult<ISsoSessionResponse | { code?: string }>(response);
13
+ if (!result) return null;
14
+ if ("code" in result && result.code) return result.code;
15
+ return null;
16
+ }
17
+
18
+ export function ParseTokenResponse(response: ApiResponse): SSOAuthResponse | null {
19
+ return ParseWrappedResult<SSOAuthResponse>(response);
20
+ }
@@ -1 +1,21 @@
1
- export {};
1
+ export * from "./sso-config/oauth-callback-state";
2
+ export * from "./sso-config/pkce-helpers";
3
+ export * from "./sso-config/sso-backend-flow";
4
+ export * from "./sso-config/auth-token-validation";
5
+ export * from "./sso-config/callback-route";
6
+ export * from "./sso-config/cookie-helpers";
7
+ export * from "./sso-config/redirect-context-actions";
8
+ export * from "./sso-config/redirect-context-helpers";
9
+ export * from "./sso-config/redirect-context-route";
10
+ export * from "./sso-config/redirect-context-persist";
11
+ export * from "./sso-config/refresh-route";
12
+ export * from "./sso-config/sign-in-route";
13
+ export * from "./sso-config/sign-out-route";
14
+ export * from "./sso-config/sign-up-route";
15
+ export * from "./sso-config/sso-response-helpers";
16
+ export * from "./sso-config/forgot-password-route";
17
+ export * from "./sso-config/provider-complete-route";
18
+ export * from "./sso-config/sso-complete-route";
19
+ export * from "./sso-config/sso-config";
20
+ export * from "./sso-config/sso-interfaces";
21
+ export * from "./sso-config/sso-response-helpers";
@@ -8,6 +8,9 @@
8
8
  z-index: 100 !important;
9
9
  cursor: default;
10
10
  }
11
+ .nexus-message {
12
+ z-index: 100000 !important;
13
+ }
11
14
 
12
15
  .dialog.left::before,
13
16
  .dialog.right::before,