Initialize Nuxt.js project with Docker deployment setup
- Add core Nuxt.js application structure with TypeScript - Include Docker configuration and deployment guide - Set up project scaffolding with pages, composables, and middleware - Add environment configuration and Git ignore rules
This commit is contained in:
65
server/api/auth/callback.get.ts
Normal file
65
server/api/auth/callback.get.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const { code, state } = query;
|
||||
|
||||
if (!code || !state) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Missing authorization code or state',
|
||||
});
|
||||
}
|
||||
|
||||
// Verify state
|
||||
const storedState = getCookie(event, 'oauth-state');
|
||||
if (state !== storedState) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid state parameter',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const keycloak = createKeycloakClient();
|
||||
const sessionManager = createSessionManager();
|
||||
|
||||
// Exchange code for tokens
|
||||
const tokens = await keycloak.exchangeCodeForTokens(code as string);
|
||||
|
||||
// Get user info
|
||||
const userInfo = await keycloak.getUserInfo(tokens.access_token);
|
||||
|
||||
// Create session
|
||||
const sessionData = {
|
||||
user: {
|
||||
id: userInfo.sub,
|
||||
email: userInfo.email,
|
||||
name: userInfo.name || `${userInfo.given_name} ${userInfo.family_name}`.trim(),
|
||||
groups: userInfo.groups || [],
|
||||
tier: userInfo.tier,
|
||||
},
|
||||
tokens: {
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
expiresAt: Date.now() + (tokens.expires_in * 1000),
|
||||
},
|
||||
createdAt: Date.now(),
|
||||
lastActivity: Date.now(),
|
||||
};
|
||||
|
||||
const sessionCookie = sessionManager.createSession(sessionData);
|
||||
|
||||
// Set session cookie
|
||||
setHeader(event, 'Set-Cookie', sessionCookie);
|
||||
|
||||
// Clear state cookie
|
||||
deleteCookie(event, 'oauth-state');
|
||||
|
||||
return sendRedirect(event, '/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Auth callback error:', error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Authentication failed',
|
||||
});
|
||||
}
|
||||
});
|
||||
17
server/api/auth/login.get.ts
Normal file
17
server/api/auth/login.get.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const keycloak = createKeycloakClient();
|
||||
const state = randomBytes(32).toString('hex');
|
||||
|
||||
// Store state in session for verification
|
||||
setCookie(event, 'oauth-state', state, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
maxAge: 600, // 10 minutes
|
||||
});
|
||||
|
||||
const authUrl = keycloak.getAuthUrl(state);
|
||||
|
||||
return sendRedirect(event, authUrl);
|
||||
});
|
||||
8
server/api/auth/logout.post.ts
Normal file
8
server/api/auth/logout.post.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const sessionManager = createSessionManager();
|
||||
const destroyCookie = sessionManager.destroySession();
|
||||
|
||||
setHeader(event, 'Set-Cookie', destroyCookie);
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
19
server/api/auth/session.get.ts
Normal file
19
server/api/auth/session.get.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const sessionManager = createSessionManager();
|
||||
const cookieHeader = getHeader(event, 'cookie');
|
||||
const session = sessionManager.getSession(cookieHeader);
|
||||
|
||||
if (!session) {
|
||||
return {
|
||||
authenticated: false,
|
||||
user: null,
|
||||
groups: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
user: session.user,
|
||||
groups: session.user.groups || [],
|
||||
};
|
||||
});
|
||||
83
server/utils/keycloak.ts
Normal file
83
server/utils/keycloak.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { KeycloakConfig, TokenResponse, UserInfo } from '~/utils/types';
|
||||
|
||||
export class KeycloakClient {
|
||||
private config: KeycloakConfig;
|
||||
|
||||
constructor(config: KeycloakConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
getAuthUrl(state: string): string {
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.config.clientId,
|
||||
redirect_uri: this.config.callbackUrl,
|
||||
response_type: 'code',
|
||||
scope: 'openid email profile',
|
||||
state,
|
||||
});
|
||||
|
||||
return `${this.config.issuer}/protocol/openid-connect/auth?${params}`;
|
||||
}
|
||||
|
||||
async exchangeCodeForTokens(code: string): Promise<TokenResponse> {
|
||||
const response = await fetch(`${this.config.issuer}/protocol/openid-connect/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: this.config.clientId,
|
||||
client_secret: this.config.clientSecret,
|
||||
code,
|
||||
redirect_uri: this.config.callbackUrl,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Token exchange failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getUserInfo(accessToken: string): Promise<UserInfo> {
|
||||
const response = await fetch(`${this.config.issuer}/protocol/openid-connect/userinfo`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get user info: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<TokenResponse> {
|
||||
const response = await fetch(`${this.config.issuer}/protocol/openid-connect/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: this.config.clientId,
|
||||
client_secret: this.config.clientSecret,
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Token refresh failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
export function createKeycloakClient(): KeycloakClient {
|
||||
const config = useRuntimeConfig();
|
||||
return new KeycloakClient(config.keycloak);
|
||||
}
|
||||
81
server/utils/session.ts
Normal file
81
server/utils/session.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { serialize, parse } from 'cookie';
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
||||
import type { SessionData } from '~/utils/types';
|
||||
|
||||
export class SessionManager {
|
||||
private encryptionKey: Buffer;
|
||||
private cookieName = 'monacousa-session';
|
||||
|
||||
constructor(encryptionKey: string) {
|
||||
this.encryptionKey = Buffer.from(encryptionKey, 'hex');
|
||||
}
|
||||
|
||||
private encrypt(data: string): string {
|
||||
const iv = randomBytes(16);
|
||||
const cipher = createCipheriv('aes-256-cbc', this.encryptionKey, iv);
|
||||
let encrypted = cipher.update(data, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
return iv.toString('hex') + ':' + encrypted;
|
||||
}
|
||||
|
||||
private decrypt(encryptedData: string): string {
|
||||
const [ivHex, encrypted] = encryptedData.split(':');
|
||||
const iv = Buffer.from(ivHex, 'hex');
|
||||
const decipher = createDecipheriv('aes-256-cbc', this.encryptionKey, iv);
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
createSession(sessionData: SessionData): string {
|
||||
const data = JSON.stringify(sessionData);
|
||||
const encrypted = this.encrypt(data);
|
||||
|
||||
return serialize(this.cookieName, encrypted, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
getSession(cookieHeader?: string): SessionData | null {
|
||||
if (!cookieHeader) return null;
|
||||
|
||||
const cookies = parse(cookieHeader);
|
||||
const sessionCookie = cookies[this.cookieName];
|
||||
|
||||
if (!sessionCookie) return null;
|
||||
|
||||
try {
|
||||
const decrypted = this.decrypt(sessionCookie);
|
||||
const sessionData = JSON.parse(decrypted) as SessionData;
|
||||
|
||||
// Check if session is expired
|
||||
if (Date.now() > sessionData.tokens.expiresAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sessionData;
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt session:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
destroySession(): string {
|
||||
return serialize(this.cookieName, '', {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 0,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createSessionManager(): SessionManager {
|
||||
const config = useRuntimeConfig();
|
||||
return new SessionManager(config.encryptionKey);
|
||||
}
|
||||
Reference in New Issue
Block a user