@dexcode-packages/helper 0.2.0

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.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @dexcode-packages/helper
2
+
3
+ Funções utilitárias em TypeScript, sem dependências de terceiros.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ npm install @dexcode-packages/helper
9
+ ```
10
+
11
+ ## Uso
12
+
13
+ ### `cn`
14
+
15
+ Combina classes condicionalmente (estilo `clsx`). Não resolve conflitos entre
16
+ utilitários Tailwind — para isso, garanta que a classe que deve prevalecer
17
+ seja declarada por último no CSS final.
18
+
19
+ ```ts
20
+ import { cn } from '@dexcode-packages/helper'
21
+
22
+ cn('px-4 py-2', isActive && 'bg-slate-900', { 'text-white': isActive })
23
+ ```
24
+
25
+ ### `formatDate`
26
+
27
+ Formata datas usando `Intl.DateTimeFormat`, com `pt-BR` como locale padrão.
28
+
29
+ ```ts
30
+ import { formatDate } from '@dexcode-packages/helper'
31
+
32
+ formatDate(new Date()) // "12/07/2026"
33
+ formatDate('2026-07-12', { locale: 'en-US', month: 'long' }) // "July 12, 2026"
34
+ ```
35
+
36
+ Documentação completa: ver o app `docs` deste monorepo.
package/dist/index.cjs ADDED
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ var server = require('next/server');
4
+
5
+ // src/cn.ts
6
+ function toClassList(value) {
7
+ if (!value) return [];
8
+ if (typeof value === "string" || typeof value === "number") return [String(value)];
9
+ if (Array.isArray(value)) return value.flatMap(toClassList);
10
+ return Object.entries(value).filter(([, enabled]) => Boolean(enabled)).map(([key]) => key);
11
+ }
12
+ function cn(...inputs) {
13
+ return Array.from(new Set(inputs.flatMap(toClassList))).join(" ");
14
+ }
15
+
16
+ // src/format-date.ts
17
+ function formatDate(date, options = {}) {
18
+ const { locale = "pt-BR", ...intlOptions } = options;
19
+ const parsedDate = date instanceof Date ? date : new Date(date);
20
+ return new Intl.DateTimeFormat(locale, {
21
+ day: "2-digit",
22
+ month: "2-digit",
23
+ year: "numeric",
24
+ ...intlOptions
25
+ }).format(parsedDate);
26
+ }
27
+ function decodeJwt(token) {
28
+ try {
29
+ return JSON.parse(
30
+ Buffer.from(token.split(".")[1], "base64url").toString()
31
+ );
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ function isTokenExpired(payload) {
37
+ return Date.now() / 1e3 >= payload.exp - 10;
38
+ }
39
+ function hasSystemAccess(payload, systemName, requiredRoles) {
40
+ const entry = payload.resource_access?.[systemName];
41
+ if (!entry || entry.roles.length === 0) return false;
42
+ if (!requiredRoles || requiredRoles.length === 0) return true;
43
+ return requiredRoles.some((role) => entry.roles.includes(role));
44
+ }
45
+ function isPublicPath(pathname, patterns) {
46
+ return patterns.some((pattern) => {
47
+ if (pattern.endsWith("/*")) {
48
+ const base = pattern.slice(0, -2);
49
+ return pathname === base || pathname.startsWith(base + "/");
50
+ }
51
+ return pathname === pattern;
52
+ });
53
+ }
54
+ async function refreshKeycloakToken(refreshToken) {
55
+ try {
56
+ const res = await fetch(
57
+ `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
58
+ {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
61
+ body: new URLSearchParams({
62
+ grant_type: "refresh_token",
63
+ client_id: process.env.KEYCLOAK_CLIENT_ID,
64
+ client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
65
+ refresh_token: refreshToken
66
+ }).toString()
67
+ }
68
+ );
69
+ if (!res.ok) return null;
70
+ return res.json();
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ async function handleKeycloak(request) {
76
+ const tokenName = process.env.TOKEN_NAME;
77
+ const refreshTokenName = process.env.REFRESH_TOKEN_NAME;
78
+ const systemName = process.env.SYSTEM_NAME;
79
+ const redirectUrl = process.env.DEXCODE_WEBSITE;
80
+ const isProd = process.env.NODE_ENV === "production";
81
+ const accessToken = request.cookies.get(tokenName)?.value;
82
+ const refreshToken = request.cookies.get(refreshTokenName)?.value;
83
+ const accessPayload = accessToken ? decodeJwt(accessToken) : null;
84
+ if (accessPayload && !isTokenExpired(accessPayload)) {
85
+ if (systemName && !hasSystemAccess(accessPayload, systemName)) {
86
+ return server.NextResponse.redirect(redirectUrl);
87
+ }
88
+ return server.NextResponse.next();
89
+ }
90
+ if (refreshToken) {
91
+ const refreshPayload = decodeJwt(refreshToken);
92
+ if (refreshPayload && !isTokenExpired(refreshPayload)) {
93
+ const tokens = await refreshKeycloakToken(refreshToken);
94
+ if (tokens) {
95
+ const newPayload = decodeJwt(tokens.access_token);
96
+ if (systemName && newPayload && !hasSystemAccess(newPayload, systemName)) {
97
+ return server.NextResponse.redirect(redirectUrl);
98
+ }
99
+ const response = server.NextResponse.next({
100
+ request: {
101
+ headers: patchCookieHeader(request.headers, tokenName, tokens.access_token)
102
+ }
103
+ });
104
+ response.cookies.set(tokenName, tokens.access_token, {
105
+ httpOnly: true,
106
+ secure: isProd,
107
+ sameSite: "lax",
108
+ maxAge: tokens.expires_in,
109
+ path: "/"
110
+ });
111
+ response.cookies.set(refreshTokenName, tokens.refresh_token, {
112
+ httpOnly: true,
113
+ secure: isProd,
114
+ sameSite: "lax",
115
+ maxAge: tokens.refresh_expires_in,
116
+ path: "/"
117
+ });
118
+ return response;
119
+ }
120
+ }
121
+ }
122
+ return server.NextResponse.redirect(redirectUrl);
123
+ }
124
+ function patchCookieHeader(headers, tokenName, newToken) {
125
+ const existing = headers.get("cookie") ?? "";
126
+ const pattern = new RegExp(`(^|;\\s*)${tokenName}=[^;]*`);
127
+ const newEntry = `${tokenName}=${newToken}`;
128
+ const updated = pattern.test(existing) ? existing.replace(pattern, (_, prefix) => `${prefix}${newEntry}`) : `${newEntry}; ${existing}`;
129
+ const patched = new Headers(headers);
130
+ patched.set("cookie", updated.trim());
131
+ return patched;
132
+ }
133
+ function AuthProxy(config) {
134
+ const { provider, publicPaths = [] } = config;
135
+ return async function proxy(request) {
136
+ const { pathname } = request.nextUrl;
137
+ if (isPublicPath(pathname, publicPaths)) {
138
+ return server.NextResponse.next();
139
+ }
140
+ switch (provider) {
141
+ case "keycloak":
142
+ return handleKeycloak(request);
143
+ case "none":
144
+ return server.NextResponse.next();
145
+ }
146
+ };
147
+ }
148
+
149
+ exports.AuthProxy = AuthProxy;
150
+ exports.cn = cn;
151
+ exports.decodeJwt = decodeJwt;
152
+ exports.formatDate = formatDate;
153
+ exports.hasSystemAccess = hasSystemAccess;
154
+ exports.isTokenExpired = isTokenExpired;
155
+ //# sourceMappingURL=index.cjs.map
156
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cn.ts","../src/format-date.ts","../src/auth-proxy.ts"],"names":["NextResponse"],"mappings":"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","file":"index.cjs","sourcesContent":["type ClassValue =\n | string\n | number\n | null\n | boolean\n | undefined\n | ClassValue[]\n | Record<string, boolean | null | undefined>\n\nfunction toClassList(value: ClassValue): string[] {\n if (!value) return []\n if (typeof value === 'string' || typeof value === 'number') return [String(value)]\n if (Array.isArray(value)) return value.flatMap(toClassList)\n return Object.entries(value)\n .filter(([, enabled]) => Boolean(enabled))\n .map(([key]) => key)\n}\n\n// Merge simples de classNames (estilo clsx). Nao resolve conflitos entre\n// utilitarios Tailwind (ex: \"p-2\" + \"p-4\") - a ultima classe declarada\n// vence pela ordem do CSS, nao pela ordem passada aqui.\nexport function cn(...inputs: ClassValue[]): string {\n return Array.from(new Set(inputs.flatMap(toClassList))).join(' ')\n}\n","export type FormatDateOptions = Intl.DateTimeFormatOptions & { locale?: string }\n\nexport function formatDate(date: Date | string | number, options: FormatDateOptions = {}): string {\n const { locale = 'pt-BR', ...intlOptions } = options\n const parsedDate = date instanceof Date ? date : new Date(date)\n\n return new Intl.DateTimeFormat(locale, {\n day: '2-digit',\n month: '2-digit',\n year: 'numeric',\n ...intlOptions\n }).format(parsedDate)\n}\n","import { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\n\n// ─── Config ───────────────────────────────────────────────────────────────────\n\nexport interface AuthProxyConfig {\n /**\n * Provider de autenticação.\n * 'keycloak' — valida JWT + resource_access via env vars.\n * 'none' — sem validação, todas as rotas são liberadas.\n */\n provider: 'keycloak' | 'none'\n /**\n * Rotas que ignoram todas as validações.\n * Suporta caminho exato ('/login') e wildcard ('/docs/*').\n */\n publicPaths?: string[]\n}\n\n// ─── JWT ──────────────────────────────────────────────────────────────────────\n\nexport interface JwtPayload {\n exp: number\n iat?: number\n sub?: string\n email?: string\n name?: string\n preferred_username?: string\n given_name?: string\n family_name?: string\n email_verified?: boolean\n resource_access?: Record<string, { roles: string[] }>\n realm_access?: { roles: string[] }\n [key: string]: unknown\n}\n\n// ─── Utilitários exportados ───────────────────────────────────────────────────\n\nexport function decodeJwt(token: string): JwtPayload | null {\n try {\n return JSON.parse(\n Buffer.from(token.split('.')[1], 'base64url').toString()\n ) as JwtPayload\n } catch {\n return null\n }\n}\n\nexport function isTokenExpired(payload: JwtPayload): boolean {\n return Date.now() / 1000 >= payload.exp - 10\n}\n\n/**\n * Verifica se o payload tem o systemName em resource_access.\n * Se requiredRoles for informado, o usuário precisa ter ao menos uma delas.\n */\nexport function hasSystemAccess(\n payload: JwtPayload,\n systemName: string,\n requiredRoles?: string[]\n): boolean {\n const entry = payload.resource_access?.[systemName]\n if (!entry || entry.roles.length === 0) return false\n if (!requiredRoles || requiredRoles.length === 0) return true\n return requiredRoles.some((role) => entry.roles.includes(role))\n}\n\n// ─── Matching de paths públicos ───────────────────────────────────────────────\n\nfunction isPublicPath(pathname: string, patterns: string[]): boolean {\n return patterns.some((pattern) => {\n if (pattern.endsWith('/*')) {\n const base = pattern.slice(0, -2)\n return pathname === base || pathname.startsWith(base + '/')\n }\n return pathname === pattern\n })\n}\n\n// ─── Provider: Keycloak ───────────────────────────────────────────────────────\n\ninterface KeycloakTokenResponse {\n access_token: string\n refresh_token: string\n expires_in: number\n refresh_expires_in: number\n}\n\nasync function refreshKeycloakToken(\n refreshToken: string\n): Promise<KeycloakTokenResponse | null> {\n try {\n const res = await fetch(\n `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: process.env.KEYCLOAK_CLIENT_ID!,\n client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,\n refresh_token: refreshToken\n }).toString()\n }\n )\n if (!res.ok) return null\n return res.json() as Promise<KeycloakTokenResponse>\n } catch {\n return null\n }\n}\n\nasync function handleKeycloak(request: NextRequest): Promise<NextResponse> {\n const tokenName = process.env.TOKEN_NAME!\n const refreshTokenName = process.env.REFRESH_TOKEN_NAME!\n const systemName = process.env.SYSTEM_NAME\n const redirectUrl = process.env.DEXCODE_WEBSITE!\n const isProd = process.env.NODE_ENV === 'production'\n\n const accessToken = request.cookies.get(tokenName)?.value\n const refreshToken = request.cookies.get(refreshTokenName)?.value\n const accessPayload = accessToken ? decodeJwt(accessToken) : null\n\n // ── 1. Token válido ────────────────────────────────────────────────────────\n if (accessPayload && !isTokenExpired(accessPayload)) {\n if (systemName && !hasSystemAccess(accessPayload, systemName)) {\n return NextResponse.redirect(redirectUrl)\n }\n return NextResponse.next()\n }\n\n // ── 2. Token expirado — tentar renovar com refresh token ──────────────────\n if (refreshToken) {\n const refreshPayload = decodeJwt(refreshToken)\n if (refreshPayload && !isTokenExpired(refreshPayload)) {\n const tokens = await refreshKeycloakToken(refreshToken)\n if (tokens) {\n const newPayload = decodeJwt(tokens.access_token)\n if (systemName && newPayload && !hasSystemAccess(newPayload, systemName)) {\n return NextResponse.redirect(redirectUrl)\n }\n\n const response = NextResponse.next({\n request: {\n headers: patchCookieHeader(request.headers, tokenName, tokens.access_token)\n }\n })\n\n response.cookies.set(tokenName, tokens.access_token, {\n httpOnly: true,\n secure: isProd,\n sameSite: 'lax',\n maxAge: tokens.expires_in,\n path: '/'\n })\n response.cookies.set(refreshTokenName, tokens.refresh_token, {\n httpOnly: true,\n secure: isProd,\n sameSite: 'lax',\n maxAge: tokens.refresh_expires_in,\n path: '/'\n })\n\n return response\n }\n }\n }\n\n // ── 3. Sem token válido — redirecionar ────────────────────────────────────\n return NextResponse.redirect(redirectUrl)\n}\n\nfunction patchCookieHeader(headers: Headers, tokenName: string, newToken: string): Headers {\n const existing = headers.get('cookie') ?? ''\n const pattern = new RegExp(`(^|;\\\\s*)${tokenName}=[^;]*`)\n const newEntry = `${tokenName}=${newToken}`\n const updated = pattern.test(existing)\n ? existing.replace(pattern, (_, prefix) => `${prefix}${newEntry}`)\n : `${newEntry}; ${existing}`\n\n const patched = new Headers(headers)\n patched.set('cookie', updated.trim())\n return patched\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────────\n\nexport function AuthProxy(config: AuthProxyConfig) {\n const { provider, publicPaths = [] } = config\n\n return async function proxy(request: NextRequest): Promise<NextResponse> {\n const { pathname } = request.nextUrl\n\n if (isPublicPath(pathname, publicPaths)) {\n return NextResponse.next()\n }\n\n switch (provider) {\n case 'keycloak':\n return handleKeycloak(request)\n case 'none':\n return NextResponse.next()\n }\n }\n}\n"]}
@@ -0,0 +1,51 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ type ClassValue = string | number | null | boolean | undefined | ClassValue[] | Record<string, boolean | null | undefined>;
4
+ declare function cn(...inputs: ClassValue[]): string;
5
+
6
+ type FormatDateOptions = Intl.DateTimeFormatOptions & {
7
+ locale?: string;
8
+ };
9
+ declare function formatDate(date: Date | string | number, options?: FormatDateOptions): string;
10
+
11
+ interface AuthProxyConfig {
12
+ /**
13
+ * Provider de autenticação.
14
+ * 'keycloak' — valida JWT + resource_access via env vars.
15
+ * 'none' — sem validação, todas as rotas são liberadas.
16
+ */
17
+ provider: 'keycloak' | 'none';
18
+ /**
19
+ * Rotas que ignoram todas as validações.
20
+ * Suporta caminho exato ('/login') e wildcard ('/docs/*').
21
+ */
22
+ publicPaths?: string[];
23
+ }
24
+ interface JwtPayload {
25
+ exp: number;
26
+ iat?: number;
27
+ sub?: string;
28
+ email?: string;
29
+ name?: string;
30
+ preferred_username?: string;
31
+ given_name?: string;
32
+ family_name?: string;
33
+ email_verified?: boolean;
34
+ resource_access?: Record<string, {
35
+ roles: string[];
36
+ }>;
37
+ realm_access?: {
38
+ roles: string[];
39
+ };
40
+ [key: string]: unknown;
41
+ }
42
+ declare function decodeJwt(token: string): JwtPayload | null;
43
+ declare function isTokenExpired(payload: JwtPayload): boolean;
44
+ /**
45
+ * Verifica se o payload tem o systemName em resource_access.
46
+ * Se requiredRoles for informado, o usuário precisa ter ao menos uma delas.
47
+ */
48
+ declare function hasSystemAccess(payload: JwtPayload, systemName: string, requiredRoles?: string[]): boolean;
49
+ declare function AuthProxy(config: AuthProxyConfig): (request: NextRequest) => Promise<NextResponse>;
50
+
51
+ export { AuthProxy, type AuthProxyConfig, type FormatDateOptions, type JwtPayload, cn, decodeJwt, formatDate, hasSystemAccess, isTokenExpired };
@@ -0,0 +1,51 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ type ClassValue = string | number | null | boolean | undefined | ClassValue[] | Record<string, boolean | null | undefined>;
4
+ declare function cn(...inputs: ClassValue[]): string;
5
+
6
+ type FormatDateOptions = Intl.DateTimeFormatOptions & {
7
+ locale?: string;
8
+ };
9
+ declare function formatDate(date: Date | string | number, options?: FormatDateOptions): string;
10
+
11
+ interface AuthProxyConfig {
12
+ /**
13
+ * Provider de autenticação.
14
+ * 'keycloak' — valida JWT + resource_access via env vars.
15
+ * 'none' — sem validação, todas as rotas são liberadas.
16
+ */
17
+ provider: 'keycloak' | 'none';
18
+ /**
19
+ * Rotas que ignoram todas as validações.
20
+ * Suporta caminho exato ('/login') e wildcard ('/docs/*').
21
+ */
22
+ publicPaths?: string[];
23
+ }
24
+ interface JwtPayload {
25
+ exp: number;
26
+ iat?: number;
27
+ sub?: string;
28
+ email?: string;
29
+ name?: string;
30
+ preferred_username?: string;
31
+ given_name?: string;
32
+ family_name?: string;
33
+ email_verified?: boolean;
34
+ resource_access?: Record<string, {
35
+ roles: string[];
36
+ }>;
37
+ realm_access?: {
38
+ roles: string[];
39
+ };
40
+ [key: string]: unknown;
41
+ }
42
+ declare function decodeJwt(token: string): JwtPayload | null;
43
+ declare function isTokenExpired(payload: JwtPayload): boolean;
44
+ /**
45
+ * Verifica se o payload tem o systemName em resource_access.
46
+ * Se requiredRoles for informado, o usuário precisa ter ao menos uma delas.
47
+ */
48
+ declare function hasSystemAccess(payload: JwtPayload, systemName: string, requiredRoles?: string[]): boolean;
49
+ declare function AuthProxy(config: AuthProxyConfig): (request: NextRequest) => Promise<NextResponse>;
50
+
51
+ export { AuthProxy, type AuthProxyConfig, type FormatDateOptions, type JwtPayload, cn, decodeJwt, formatDate, hasSystemAccess, isTokenExpired };
package/dist/index.js ADDED
@@ -0,0 +1,149 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ // src/cn.ts
4
+ function toClassList(value) {
5
+ if (!value) return [];
6
+ if (typeof value === "string" || typeof value === "number") return [String(value)];
7
+ if (Array.isArray(value)) return value.flatMap(toClassList);
8
+ return Object.entries(value).filter(([, enabled]) => Boolean(enabled)).map(([key]) => key);
9
+ }
10
+ function cn(...inputs) {
11
+ return Array.from(new Set(inputs.flatMap(toClassList))).join(" ");
12
+ }
13
+
14
+ // src/format-date.ts
15
+ function formatDate(date, options = {}) {
16
+ const { locale = "pt-BR", ...intlOptions } = options;
17
+ const parsedDate = date instanceof Date ? date : new Date(date);
18
+ return new Intl.DateTimeFormat(locale, {
19
+ day: "2-digit",
20
+ month: "2-digit",
21
+ year: "numeric",
22
+ ...intlOptions
23
+ }).format(parsedDate);
24
+ }
25
+ function decodeJwt(token) {
26
+ try {
27
+ return JSON.parse(
28
+ Buffer.from(token.split(".")[1], "base64url").toString()
29
+ );
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ function isTokenExpired(payload) {
35
+ return Date.now() / 1e3 >= payload.exp - 10;
36
+ }
37
+ function hasSystemAccess(payload, systemName, requiredRoles) {
38
+ const entry = payload.resource_access?.[systemName];
39
+ if (!entry || entry.roles.length === 0) return false;
40
+ if (!requiredRoles || requiredRoles.length === 0) return true;
41
+ return requiredRoles.some((role) => entry.roles.includes(role));
42
+ }
43
+ function isPublicPath(pathname, patterns) {
44
+ return patterns.some((pattern) => {
45
+ if (pattern.endsWith("/*")) {
46
+ const base = pattern.slice(0, -2);
47
+ return pathname === base || pathname.startsWith(base + "/");
48
+ }
49
+ return pathname === pattern;
50
+ });
51
+ }
52
+ async function refreshKeycloakToken(refreshToken) {
53
+ try {
54
+ const res = await fetch(
55
+ `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
56
+ {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
59
+ body: new URLSearchParams({
60
+ grant_type: "refresh_token",
61
+ client_id: process.env.KEYCLOAK_CLIENT_ID,
62
+ client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
63
+ refresh_token: refreshToken
64
+ }).toString()
65
+ }
66
+ );
67
+ if (!res.ok) return null;
68
+ return res.json();
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ async function handleKeycloak(request) {
74
+ const tokenName = process.env.TOKEN_NAME;
75
+ const refreshTokenName = process.env.REFRESH_TOKEN_NAME;
76
+ const systemName = process.env.SYSTEM_NAME;
77
+ const redirectUrl = process.env.DEXCODE_WEBSITE;
78
+ const isProd = process.env.NODE_ENV === "production";
79
+ const accessToken = request.cookies.get(tokenName)?.value;
80
+ const refreshToken = request.cookies.get(refreshTokenName)?.value;
81
+ const accessPayload = accessToken ? decodeJwt(accessToken) : null;
82
+ if (accessPayload && !isTokenExpired(accessPayload)) {
83
+ if (systemName && !hasSystemAccess(accessPayload, systemName)) {
84
+ return NextResponse.redirect(redirectUrl);
85
+ }
86
+ return NextResponse.next();
87
+ }
88
+ if (refreshToken) {
89
+ const refreshPayload = decodeJwt(refreshToken);
90
+ if (refreshPayload && !isTokenExpired(refreshPayload)) {
91
+ const tokens = await refreshKeycloakToken(refreshToken);
92
+ if (tokens) {
93
+ const newPayload = decodeJwt(tokens.access_token);
94
+ if (systemName && newPayload && !hasSystemAccess(newPayload, systemName)) {
95
+ return NextResponse.redirect(redirectUrl);
96
+ }
97
+ const response = NextResponse.next({
98
+ request: {
99
+ headers: patchCookieHeader(request.headers, tokenName, tokens.access_token)
100
+ }
101
+ });
102
+ response.cookies.set(tokenName, tokens.access_token, {
103
+ httpOnly: true,
104
+ secure: isProd,
105
+ sameSite: "lax",
106
+ maxAge: tokens.expires_in,
107
+ path: "/"
108
+ });
109
+ response.cookies.set(refreshTokenName, tokens.refresh_token, {
110
+ httpOnly: true,
111
+ secure: isProd,
112
+ sameSite: "lax",
113
+ maxAge: tokens.refresh_expires_in,
114
+ path: "/"
115
+ });
116
+ return response;
117
+ }
118
+ }
119
+ }
120
+ return NextResponse.redirect(redirectUrl);
121
+ }
122
+ function patchCookieHeader(headers, tokenName, newToken) {
123
+ const existing = headers.get("cookie") ?? "";
124
+ const pattern = new RegExp(`(^|;\\s*)${tokenName}=[^;]*`);
125
+ const newEntry = `${tokenName}=${newToken}`;
126
+ const updated = pattern.test(existing) ? existing.replace(pattern, (_, prefix) => `${prefix}${newEntry}`) : `${newEntry}; ${existing}`;
127
+ const patched = new Headers(headers);
128
+ patched.set("cookie", updated.trim());
129
+ return patched;
130
+ }
131
+ function AuthProxy(config) {
132
+ const { provider, publicPaths = [] } = config;
133
+ return async function proxy(request) {
134
+ const { pathname } = request.nextUrl;
135
+ if (isPublicPath(pathname, publicPaths)) {
136
+ return NextResponse.next();
137
+ }
138
+ switch (provider) {
139
+ case "keycloak":
140
+ return handleKeycloak(request);
141
+ case "none":
142
+ return NextResponse.next();
143
+ }
144
+ };
145
+ }
146
+
147
+ export { AuthProxy, cn, decodeJwt, formatDate, hasSystemAccess, isTokenExpired };
148
+ //# sourceMappingURL=index.js.map
149
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cn.ts","../src/format-date.ts","../src/auth-proxy.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["type ClassValue =\n | string\n | number\n | null\n | boolean\n | undefined\n | ClassValue[]\n | Record<string, boolean | null | undefined>\n\nfunction toClassList(value: ClassValue): string[] {\n if (!value) return []\n if (typeof value === 'string' || typeof value === 'number') return [String(value)]\n if (Array.isArray(value)) return value.flatMap(toClassList)\n return Object.entries(value)\n .filter(([, enabled]) => Boolean(enabled))\n .map(([key]) => key)\n}\n\n// Merge simples de classNames (estilo clsx). Nao resolve conflitos entre\n// utilitarios Tailwind (ex: \"p-2\" + \"p-4\") - a ultima classe declarada\n// vence pela ordem do CSS, nao pela ordem passada aqui.\nexport function cn(...inputs: ClassValue[]): string {\n return Array.from(new Set(inputs.flatMap(toClassList))).join(' ')\n}\n","export type FormatDateOptions = Intl.DateTimeFormatOptions & { locale?: string }\n\nexport function formatDate(date: Date | string | number, options: FormatDateOptions = {}): string {\n const { locale = 'pt-BR', ...intlOptions } = options\n const parsedDate = date instanceof Date ? date : new Date(date)\n\n return new Intl.DateTimeFormat(locale, {\n day: '2-digit',\n month: '2-digit',\n year: 'numeric',\n ...intlOptions\n }).format(parsedDate)\n}\n","import { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\n\n// ─── Config ───────────────────────────────────────────────────────────────────\n\nexport interface AuthProxyConfig {\n /**\n * Provider de autenticação.\n * 'keycloak' — valida JWT + resource_access via env vars.\n * 'none' — sem validação, todas as rotas são liberadas.\n */\n provider: 'keycloak' | 'none'\n /**\n * Rotas que ignoram todas as validações.\n * Suporta caminho exato ('/login') e wildcard ('/docs/*').\n */\n publicPaths?: string[]\n}\n\n// ─── JWT ──────────────────────────────────────────────────────────────────────\n\nexport interface JwtPayload {\n exp: number\n iat?: number\n sub?: string\n email?: string\n name?: string\n preferred_username?: string\n given_name?: string\n family_name?: string\n email_verified?: boolean\n resource_access?: Record<string, { roles: string[] }>\n realm_access?: { roles: string[] }\n [key: string]: unknown\n}\n\n// ─── Utilitários exportados ───────────────────────────────────────────────────\n\nexport function decodeJwt(token: string): JwtPayload | null {\n try {\n return JSON.parse(\n Buffer.from(token.split('.')[1], 'base64url').toString()\n ) as JwtPayload\n } catch {\n return null\n }\n}\n\nexport function isTokenExpired(payload: JwtPayload): boolean {\n return Date.now() / 1000 >= payload.exp - 10\n}\n\n/**\n * Verifica se o payload tem o systemName em resource_access.\n * Se requiredRoles for informado, o usuário precisa ter ao menos uma delas.\n */\nexport function hasSystemAccess(\n payload: JwtPayload,\n systemName: string,\n requiredRoles?: string[]\n): boolean {\n const entry = payload.resource_access?.[systemName]\n if (!entry || entry.roles.length === 0) return false\n if (!requiredRoles || requiredRoles.length === 0) return true\n return requiredRoles.some((role) => entry.roles.includes(role))\n}\n\n// ─── Matching de paths públicos ───────────────────────────────────────────────\n\nfunction isPublicPath(pathname: string, patterns: string[]): boolean {\n return patterns.some((pattern) => {\n if (pattern.endsWith('/*')) {\n const base = pattern.slice(0, -2)\n return pathname === base || pathname.startsWith(base + '/')\n }\n return pathname === pattern\n })\n}\n\n// ─── Provider: Keycloak ───────────────────────────────────────────────────────\n\ninterface KeycloakTokenResponse {\n access_token: string\n refresh_token: string\n expires_in: number\n refresh_expires_in: number\n}\n\nasync function refreshKeycloakToken(\n refreshToken: string\n): Promise<KeycloakTokenResponse | null> {\n try {\n const res = await fetch(\n `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: process.env.KEYCLOAK_CLIENT_ID!,\n client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,\n refresh_token: refreshToken\n }).toString()\n }\n )\n if (!res.ok) return null\n return res.json() as Promise<KeycloakTokenResponse>\n } catch {\n return null\n }\n}\n\nasync function handleKeycloak(request: NextRequest): Promise<NextResponse> {\n const tokenName = process.env.TOKEN_NAME!\n const refreshTokenName = process.env.REFRESH_TOKEN_NAME!\n const systemName = process.env.SYSTEM_NAME\n const redirectUrl = process.env.DEXCODE_WEBSITE!\n const isProd = process.env.NODE_ENV === 'production'\n\n const accessToken = request.cookies.get(tokenName)?.value\n const refreshToken = request.cookies.get(refreshTokenName)?.value\n const accessPayload = accessToken ? decodeJwt(accessToken) : null\n\n // ── 1. Token válido ────────────────────────────────────────────────────────\n if (accessPayload && !isTokenExpired(accessPayload)) {\n if (systemName && !hasSystemAccess(accessPayload, systemName)) {\n return NextResponse.redirect(redirectUrl)\n }\n return NextResponse.next()\n }\n\n // ── 2. Token expirado — tentar renovar com refresh token ──────────────────\n if (refreshToken) {\n const refreshPayload = decodeJwt(refreshToken)\n if (refreshPayload && !isTokenExpired(refreshPayload)) {\n const tokens = await refreshKeycloakToken(refreshToken)\n if (tokens) {\n const newPayload = decodeJwt(tokens.access_token)\n if (systemName && newPayload && !hasSystemAccess(newPayload, systemName)) {\n return NextResponse.redirect(redirectUrl)\n }\n\n const response = NextResponse.next({\n request: {\n headers: patchCookieHeader(request.headers, tokenName, tokens.access_token)\n }\n })\n\n response.cookies.set(tokenName, tokens.access_token, {\n httpOnly: true,\n secure: isProd,\n sameSite: 'lax',\n maxAge: tokens.expires_in,\n path: '/'\n })\n response.cookies.set(refreshTokenName, tokens.refresh_token, {\n httpOnly: true,\n secure: isProd,\n sameSite: 'lax',\n maxAge: tokens.refresh_expires_in,\n path: '/'\n })\n\n return response\n }\n }\n }\n\n // ── 3. Sem token válido — redirecionar ────────────────────────────────────\n return NextResponse.redirect(redirectUrl)\n}\n\nfunction patchCookieHeader(headers: Headers, tokenName: string, newToken: string): Headers {\n const existing = headers.get('cookie') ?? ''\n const pattern = new RegExp(`(^|;\\\\s*)${tokenName}=[^;]*`)\n const newEntry = `${tokenName}=${newToken}`\n const updated = pattern.test(existing)\n ? existing.replace(pattern, (_, prefix) => `${prefix}${newEntry}`)\n : `${newEntry}; ${existing}`\n\n const patched = new Headers(headers)\n patched.set('cookie', updated.trim())\n return patched\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────────\n\nexport function AuthProxy(config: AuthProxyConfig) {\n const { provider, publicPaths = [] } = config\n\n return async function proxy(request: NextRequest): Promise<NextResponse> {\n const { pathname } = request.nextUrl\n\n if (isPublicPath(pathname, publicPaths)) {\n return NextResponse.next()\n }\n\n switch (provider) {\n case 'keycloak':\n return handleKeycloak(request)\n case 'none':\n return NextResponse.next()\n }\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@dexcode-packages/helper",
3
+ "version": "0.2.0",
4
+ "description": "Funcoes utilitarias em TypeScript, sem dependencias de terceiros, para uso em projetos Next.js.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": {
14
+ "import": "./dist/index.d.ts",
15
+ "require": "./dist/index.d.cts"
16
+ },
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch"
30
+ },
31
+ "peerDependencies": {
32
+ "next": ">=14"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "next": {
36
+ "optional": true
37
+ }
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^26.1.1",
41
+ "next": "^16.2.10"
42
+ }
43
+ }