@simitgroup/simpleapp-generator 2.0.2-t-alpha → 2.0.2-v-alpha
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/ReleaseNote.md +11 -0
- package/package.json +1 -1
- package/templates/nest/src/simple-app/_core/features/auth/keycloak/keycloak.guard.ts.eta +17 -6
- package/templates/nuxt/plugins/10.simpleapp-event.ts.eta +4 -2
- package/templates/nuxt/plugins/20.simpleapp-userstore.ts.eta +1 -1
- package/templates/nuxt/server/api/auth/[...].ts.eta +8 -0
- package/templates/nuxt/server/api/profile/[...].ts.eta +9 -2
- package/templates/nuxt/server/api/profile/index.ts.eta +11 -12
package/ReleaseNote.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
[2.0.2v-alpha]
|
|
2
|
+
|
|
3
|
+
1. Stability on startup: app initialization no longer fatals on 401/302 from profile/session checks. it triggers the normal relogin flow
|
|
4
|
+
3. Navigate to /login when XHR requests are transparently redirected, preventing retry loops
|
|
5
|
+
|
|
6
|
+
[2.0.2u-alpha]
|
|
7
|
+
|
|
8
|
+
1. Enforced strict Keycloak token validation on backend (via AuthGuard + UserContext), returning clear 401 for invalid/expired tokens
|
|
9
|
+
2. Blocked user creation when required JWT claims are missing (e.g. uid, email)
|
|
10
|
+
3. Improved frontend session handling: refresh-token failures and consistently redirect to login on 401
|
|
11
|
+
|
|
1
12
|
[2.0.2t-alpha]
|
|
2
13
|
|
|
3
14
|
1. Fix define type on service file
|
package/package.json
CHANGED
|
@@ -6,22 +6,26 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
|
8
8
|
import { Reflector } from '@nestjs/core';
|
|
9
|
-
import { ResourceGuard } from 'nest-keycloak-connect';
|
|
9
|
+
import { AuthGuard, ResourceGuard } from 'nest-keycloak-connect';
|
|
10
|
+
import type { Request } from 'express';
|
|
10
11
|
|
|
11
12
|
@Injectable()
|
|
12
13
|
export class CustomKeycloakGuard implements CanActivate {
|
|
13
14
|
constructor(
|
|
14
15
|
private reflector: Reflector,
|
|
16
|
+
private authGuard: AuthGuard,
|
|
15
17
|
private resourceGuard: ResourceGuard,
|
|
16
18
|
) {}
|
|
17
19
|
|
|
18
20
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
19
|
-
const request = context.switchToHttp().getRequest();
|
|
21
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
20
22
|
|
|
21
23
|
//graphql no http request, exclude from capability of x-apikey
|
|
22
24
|
if (request?.headers) {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
+
const apiKeyHeader = request.headers['x-apikey'];
|
|
26
|
+
const apiSecretHeader = request.headers['x-apisecret'];
|
|
27
|
+
const apiKey = Array.isArray(apiKeyHeader) ? apiKeyHeader[0] : apiKeyHeader;
|
|
28
|
+
const apiSecret = Array.isArray(apiSecretHeader) ? apiSecretHeader[0] : apiSecretHeader;
|
|
25
29
|
// validate apikey and apisecret at middleware level, reach here mean approved as robot
|
|
26
30
|
if (apiKey && apiSecret) {
|
|
27
31
|
return true;
|
|
@@ -30,8 +34,15 @@ export class CustomKeycloakGuard implements CanActivate {
|
|
|
30
34
|
|
|
31
35
|
// If API key is not present, fall back to Keycloak authentication
|
|
32
36
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
// validate and authenticate the token with keycloak
|
|
38
|
+
const authCanActivate = await this.authGuard.canActivate(context);
|
|
39
|
+
if (!authCanActivate) {
|
|
40
|
+
throw new UnauthorizedException('Invalid Keycloak token');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// then enforce resource and role based access control
|
|
44
|
+
const resourceCanActivate = await this.resourceGuard.canActivate(context);
|
|
45
|
+
return resourceCanActivate;
|
|
35
46
|
} catch (error) {
|
|
36
47
|
throw new UnauthorizedException('Invalid API key or Keycloak token');
|
|
37
48
|
}
|
|
@@ -34,7 +34,9 @@ export default defineNuxtPlugin( async(nuxtApp) => {
|
|
|
34
34
|
// console.log("error catch",error)
|
|
35
35
|
|
|
36
36
|
if(error?.code && error.code == 'ERR_BAD_REQUEST'){
|
|
37
|
-
if(error.response && error.response.status==
|
|
37
|
+
if (error.response && (error.response.status == 401 || error.response.status == 302)) {
|
|
38
|
+
return Promise.reject(error);
|
|
39
|
+
} else if(error.response && error.response.status==403){
|
|
38
40
|
console.warn("error status 403, redirect to external link /" )
|
|
39
41
|
navigateTo('/',{external:true})
|
|
40
42
|
}else{
|
|
@@ -74,7 +76,7 @@ export default defineNuxtPlugin( async(nuxtApp) => {
|
|
|
74
76
|
fatal:true
|
|
75
77
|
})
|
|
76
78
|
}
|
|
77
|
-
else if(error.response && error.response.status==302){
|
|
79
|
+
else if(error.response && (error.response.status==302 || error.response.status==401)){
|
|
78
80
|
console.error("axios 302 session expired, start login flow")
|
|
79
81
|
}else if(error.response && error.response.status){
|
|
80
82
|
let errmsg = error.response.message
|
|
@@ -357,7 +357,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
357
357
|
statusMessage: e.message,
|
|
358
358
|
fatal: true,
|
|
359
359
|
});
|
|
360
|
-
} else if (e?.response?.status == 302) {
|
|
360
|
+
} else if (e?.response?.status == 302 || e?.response?.status == 401) {
|
|
361
361
|
//no session
|
|
362
362
|
//need authentication, relogin
|
|
363
363
|
if (useRoute().meta.auth !== false) {
|
|
@@ -107,6 +107,7 @@ export default NuxtAuthHandler({
|
|
|
107
107
|
return {
|
|
108
108
|
...token,
|
|
109
109
|
accessToken: null,
|
|
110
|
+
refreshToken: null,
|
|
110
111
|
expiresAt: 0,
|
|
111
112
|
error: "RefreshAccessTokenError"
|
|
112
113
|
};
|
|
@@ -126,6 +127,13 @@ export default NuxtAuthHandler({
|
|
|
126
127
|
// console.log("session", session);
|
|
127
128
|
// Send properties to the client, like an access_token from a provider.
|
|
128
129
|
const sessiondata:any = {...session}
|
|
130
|
+
|
|
131
|
+
if (!token.accessToken || token['error'] === 'RefreshAccessTokenError') {
|
|
132
|
+
sessiondata.accessToken = null;
|
|
133
|
+
sessiondata.error = token['error'] ?? 'MissingAccessToken';
|
|
134
|
+
return sessiondata;
|
|
135
|
+
}
|
|
136
|
+
|
|
129
137
|
sessiondata.accessToken = <string>token.accessToken;
|
|
130
138
|
return sessiondata;
|
|
131
139
|
},
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import axios from 'axios';
|
|
9
|
+
import fs from "node:fs";
|
|
9
10
|
import { getServerSession } from '#auth'
|
|
10
11
|
import type { Session } from 'next-auth';
|
|
11
12
|
import { pathJoin } from '~/server/utils/path';
|
|
@@ -22,7 +23,7 @@ export default defineEventHandler(async (event:any) => {
|
|
|
22
23
|
return sendRedirect(event, '/login', 401)
|
|
23
24
|
}
|
|
24
25
|
if(!session) {
|
|
25
|
-
|
|
26
|
+
return sendRedirect(event, "/login", 302);
|
|
26
27
|
}
|
|
27
28
|
return new Promise<any>(async (resolve, reject) => {
|
|
28
29
|
|
|
@@ -33,6 +34,10 @@ export default defineEventHandler(async (event:any) => {
|
|
|
33
34
|
|
|
34
35
|
const req = event.node.req;
|
|
35
36
|
|
|
37
|
+
if (!accessToken || typeof accessToken !== "string") {
|
|
38
|
+
return sendRedirect(event, "/login", 302);
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
if(req.method == 'POST' || req.method == 'PUT' || req.method == 'PATCH') {
|
|
37
42
|
|
|
38
43
|
forwardData = await readBody(event);
|
|
@@ -135,7 +140,9 @@ export default defineEventHandler(async (event:any) => {
|
|
|
135
140
|
}else{
|
|
136
141
|
|
|
137
142
|
if (error.response?.status && error.response.status == 401) {
|
|
138
|
-
|
|
143
|
+
// rejecting would bubble a 401 into the client and often renders the Nuxt error page.
|
|
144
|
+
// Instead, end the request with a redirect so the browser can enter the login flow.
|
|
145
|
+
return sendRedirect(event, "/login", 401);
|
|
139
146
|
}
|
|
140
147
|
reject({
|
|
141
148
|
statusMessage: error.response.statusText,
|
|
@@ -16,15 +16,13 @@ export default defineEventHandler(async (event:any) => {
|
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
18
|
session = await getServerSession(event)
|
|
19
|
-
|
|
20
19
|
} catch (error) {
|
|
21
|
-
return sendRedirect(event, '/login',
|
|
20
|
+
return sendRedirect(event, '/login', 302)
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
return new Promise<any>(async (resolve, reject) => {
|
|
25
24
|
if(!session) {
|
|
26
|
-
|
|
27
|
-
throw createError({ statusMessage: 'Unauthorized', statusCode: 401 })
|
|
25
|
+
return sendRedirect(event, '/login', 302)
|
|
28
26
|
}
|
|
29
27
|
const seperateSymbol = '.';
|
|
30
28
|
const accessToken = session?.accessToken;
|
|
@@ -33,6 +31,10 @@ export default defineEventHandler(async (event:any) => {
|
|
|
33
31
|
|
|
34
32
|
const req = event.node.req;
|
|
35
33
|
|
|
34
|
+
if (!accessToken || typeof accessToken !== 'string') {
|
|
35
|
+
return sendRedirect(event, '/login', 302);
|
|
36
|
+
}
|
|
37
|
+
|
|
36
38
|
if(req.method == 'POST' || req.method == 'PUT' || req.method == 'PATCH') {
|
|
37
39
|
|
|
38
40
|
forwardData = await readBody(event);
|
|
@@ -78,16 +80,13 @@ export default defineEventHandler(async (event:any) => {
|
|
|
78
80
|
// console.log('#####################################')
|
|
79
81
|
// console.log(axiosConfig);
|
|
80
82
|
// console.log('#####################################')
|
|
81
|
-
if (error.response?.status && error.response.status ==
|
|
82
|
-
|
|
83
|
-
return sendRedirect(event, '/login', 401)
|
|
84
|
-
// throw createError({ statusMessage: 'Unauthorized', statusCode: 401 })
|
|
83
|
+
if (error.response?.status && error.response.status == 401) {
|
|
84
|
+
return sendRedirect(event, '/login', 302)
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
statusCode: error.response.status ,
|
|
87
|
+
reject({
|
|
88
|
+
statusMessage: error.response.statusText,
|
|
89
|
+
statusCode: error.response.status,
|
|
91
90
|
data: error.response.data
|
|
92
91
|
}); // resolve({ status: 'ok' })
|
|
93
92
|
// throw createError({ statusMessage: 'Bad Requests', statusCode: 404 })
|