13 Commits

Author SHA1 Message Date
WorkClub Automation ad8bb2d320 Fix: Remove port 8081 hardcoding in OIDC internal URLs
CI Pipeline / Backend Build & Test (push) Successful in 53s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 36s
CI Pipeline / Infrastructure Validation (push) Successful in 3s
The auth.ts was hardcoding port 8081 for internal Keycloak communication

but the Kubernetes Keycloak service uses port 8080, causing auth failures

Changed: oidcInternal no longer replaces 8080 with 8081
2026-03-21 21:52:03 +01:00
WorkClub Automation b10c57bdb8 Fix: Admin club management 500 error - JWT clubs claim format
CI Pipeline / Backend Build & Test (push) Successful in 53s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 33s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
- Fix clubs attribute in Keycloak to contain only UUIDs (removed role names)

- Add defensive error handling in ClubService.GetMyClubsAsync()

- Add logging for debugging club retrieval issues

- Return empty list instead of 500 error on failures

Fixes: Admin users can now manage clubs without contact admin error
2026-03-21 20:27:38 +01:00
WorkClub Automation 9304db2391 Fix: Add API_INTERNAL_URL to Dockerfile build stage
CI Pipeline / Backend Build & Test (push) Successful in 59s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 30s
CI Pipeline / Infrastructure Validation (push) Successful in 3s
Next.js rewrites are evaluated at build time, not runtime.
The API_INTERNAL_URL was set in K8s deployment but not during
the Docker build, causing fallback to localhost:5001.

- Added ENV API_INTERNAL_URL=http://workclub-api:8080
- This ensures Next.js rewrites point to internal K8s service
2026-03-21 14:13:37 +01:00
WorkClub Automation 27f1ad5780 Add debug logging to auth-guard to trace isAdmin issue
CI Pipeline / Backend Build & Test (push) Successful in 1m5s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 36s
CI Pipeline / Infrastructure Validation (push) Successful in 3s
2026-03-21 13:57:27 +01:00
WorkClub Automation 4e52544c79 Add API_INTERNAL_URL to frontend deployment for K8s
CI Pipeline / Backend Build & Test (push) Successful in 1m3s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 34s
CI Pipeline / Infrastructure Validation (push) Successful in 3s
The Next.js rewrites were falling back to localhost:5001 because
API_INTERNAL_URL was not set. This caused API proxy errors.

- Added API_INTERNAL_URL=http://workclub-api:8080
- This allows Next.js to proxy /api/* calls to the internal backend service
2026-03-21 13:46:35 +01:00
WorkClub Automation e6e1112060 Add debug logging for admin status detection
CI Pipeline / Backend Build & Test (push) Successful in 1m1s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 37s
CI Pipeline / Infrastructure Validation (push) Successful in 3s
2026-03-21 13:32:53 +01:00
WorkClub Automation b5dd24b4c9 Fix: Always check admin status from access token in JWT callback
CI Pipeline / Backend Build & Test (push) Successful in 1m3s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 29s
CI Pipeline / Infrastructure Validation (push) Successful in 3s
The jwt callback was only checking isAdmin during initial login when
account was present, but not on subsequent session refreshes. This caused
the admin status to be lost after the initial login.

- Moved admin status check outside of the 'if (account)' block
- Now checks isAdmin on every JWT callback when accessToken is available
2026-03-21 13:11:01 +01:00
WorkClub Automation f8d698ba42 Fix KEYCLOAK_ISSUER_INTERNAL to include port 8080
CI Pipeline / Backend Build & Test (push) Successful in 50s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 34s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
The internal Keycloak URL was missing the port number, causing
the OIDC token exchange to fail. The code tries to replace
:8080 with :8081 but the port was missing entirely.

- Changed from: http://workclub-keycloak/realms/workclub
- Changed to: http://workclub-keycloak:8080/realms/workclub
2026-03-21 09:30:13 +01:00
WorkClub Automation 86c7b0d46d Fix Keycloak URL in K8s ConfigMap to use correct NodePort 30808
CI Pipeline / Backend Build & Test (push) Successful in 57s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 34s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
- Changed api-base-url from :5001 to :30501
- Changed keycloak-url from :8080 to :30808
- Changed keycloak-authority from :8080 to :30808

The frontend was trying to connect to port 8080 which is not exposed
externally. Keycloak is accessible via NodePort 30808.
2026-03-21 08:27:05 +01:00
WorkClub Automation fd2931e59c Fix Kubernetes NodePort range (30000-32767)
CI Pipeline / Backend Build & Test (push) Successful in 1m6s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 32s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
- Frontend: nodePort 3000 → 30080
- Backend: nodePort 5001 → 30501, service port 5001 → 8080
- Keycloak: nodePort 8080 → 30808

Kubernetes requires NodePort to be in range 30000-32767.
The service port (internal) and targetPort (container) remain
unchanged for compatibility with existing configurations.
2026-03-20 22:50:51 +01:00
WorkClub Automation a5ebecc8b5 Remove localhost:3000 from Keycloak redirect URIs and web origins
CI Pipeline / Backend Build & Test (push) Successful in 50s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 32s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
- Removed localhost:3000/* from redirectUris in realm-export.json
- Removed localhost:3000 from webOrigins in realm-export.json
- Removed localhost:3000/* from post.logout.redirect.uris
- Removed localhost:3000 from keycloak-realm-import-configmap.yaml
- Updated running Keycloak instance via kcadm.sh

Only port 30080 is now configured for OAuth redirects.
2026-03-20 22:39:15 +01:00
WorkClub Automation 956c3ead0c Fix YAML syntax error in frontend-deployment.yaml
CI Pipeline / Backend Build & Test (push) Successful in 52s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 34s
CI Pipeline / Infrastructure Validation (push) Successful in 3s
The file had malformed YAML with incorrect indentation on line 70,
causing validation to fail. Rewrote the file with correct indentation.
2026-03-20 20:50:14 +01:00
WorkClub Automation 0100def25a Align Kubernetes ports with Docker Compose configuration
CI Pipeline / Backend Build & Test (push) Successful in 58s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 43s
CI Pipeline / Infrastructure Validation (push) Failing after 4s
- Frontend: Changed NodePort from 30080 to 3000 (matches Docker port)
- Backend: Changed NodePort from 30081 to 5001 (matches Docker port)
- Keycloak: Changed NodePort from 30082 to 8080 (matches Docker port)
- Updated ConfigMap URLs to use new ports
- Updated NEXTAUTH_URL to use port 3000

This ensures Kubernetes deployment uses the same ports as Docker Compose
for consistency across environments.
2026-03-20 20:40:22 +01:00
16 changed files with 324 additions and 170 deletions
+130 -93
View File
@@ -12,135 +12,172 @@ public class ClubService
private readonly AppDbContext _context; private readonly AppDbContext _context;
private readonly ITenantProvider _tenantProvider; private readonly ITenantProvider _tenantProvider;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<ClubService> _logger;
public ClubService( public ClubService(
AppDbContext context, AppDbContext context,
ITenantProvider tenantProvider, ITenantProvider tenantProvider,
IHttpContextAccessor httpContextAccessor) IHttpContextAccessor httpContextAccessor,
ILogger<ClubService> logger)
{ {
_context = context; _context = context;
_tenantProvider = tenantProvider; _tenantProvider = tenantProvider;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_logger = logger;
} }
public async Task<List<ClubListDto>> GetMyClubsAsync() public async Task<List<ClubListDto>> GetMyClubsAsync()
{ {
var clubsClaim = _httpContextAccessor.HttpContext?.User.FindFirst("clubs")?.Value; try
if (string.IsNullOrEmpty(clubsClaim))
{ {
return new List<ClubListDto>(); var clubsClaim = _httpContextAccessor.HttpContext?.User.FindFirst("clubs")?.Value;
} _logger.LogInformation("GetMyClubsAsync: Clubs claim value: {ClubsClaim}", clubsClaim);
var tenantIds = clubsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries) if (string.IsNullOrEmpty(clubsClaim))
.Select(t => t.Trim())
.Where(t => !string.IsNullOrEmpty(t) && Guid.TryParse(t, out _))
.ToList();
if (tenantIds.Count == 0)
{
return new List<ClubListDto>();
}
var clubDtos = new List<ClubListDto>();
var connectionString = _context.Database.GetConnectionString();
foreach (var tenantId in tenantIds)
{
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
await using var transaction = await connection.BeginTransactionAsync();
// Set RLS context
using (var command = connection.CreateCommand())
{ {
command.Transaction = transaction; _logger.LogWarning("GetMyClubsAsync: No clubs claim found for user");
command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'"; return new List<ClubListDto>();
await command.ExecuteNonQueryAsync();
} }
Guid? clubId = null; // Parse UUIDs from comma-separated claim, filtering out non-UUID values (like role names)
string? clubName = null; var tenantIds = clubsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries)
int? sportTypeInt = null; .Select(t => t.Trim())
.Where(t => !string.IsNullOrEmpty(t) && Guid.TryParse(t, out _))
.ToList();
// Fetch club details _logger.LogInformation("GetMyClubsAsync: Parsed {Count} valid tenant IDs from claim", tenantIds.Count);
using (var command = connection.CreateCommand())
if (tenantIds.Count == 0)
{ {
command.Transaction = transaction; _logger.LogWarning("GetMyClubsAsync: No valid tenant IDs found in clubs claim: {ClubsClaim}", clubsClaim);
command.CommandText = @" return new List<ClubListDto>();
SELECT c.""Id"", c.""Name"", c.""SportType"" }
FROM clubs AS c
WHERE c.""TenantId"" = @tenantId";
var parameter = command.CreateParameter(); var clubDtos = new List<ClubListDto>();
parameter.ParameterName = "@tenantId"; var connectionString = _context.Database.GetConnectionString();
parameter.Value = tenantId;
command.Parameters.Add(parameter);
using (var reader = await command.ExecuteReaderAsync()) foreach (var tenantId in tenantIds)
{
try
{ {
if (await reader.ReadAsync()) await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
await using var transaction = await connection.BeginTransactionAsync();
// Set RLS context - tenantId is already validated as a valid GUID
// Use direct string since SET LOCAL doesn't support parameters
using (var command = connection.CreateCommand())
{ {
clubId = reader.GetGuid(0); command.Transaction = transaction;
clubName = reader.GetString(1); command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'";
sportTypeInt = reader.GetInt32(2); await command.ExecuteNonQueryAsync();
} }
}
}
// Fetch member count if club exists Guid? clubId = null;
if (clubId.HasValue && clubName != null && sportTypeInt.HasValue) string? clubName = null;
{ int? sportTypeInt = null;
using (var memberCommand = connection.CreateCommand())
// Fetch club details
using (var command = connection.CreateCommand())
{
command.Transaction = transaction;
command.CommandText = @"
SELECT c.""Id"", c.""Name"", c.""SportType""
FROM clubs AS c
WHERE c.""TenantId"" = @tenantId";
var parameter = command.CreateParameter();
parameter.ParameterName = "@tenantId";
parameter.Value = tenantId;
command.Parameters.Add(parameter);
using (var reader = await command.ExecuteReaderAsync())
{
if (await reader.ReadAsync())
{
clubId = reader.GetGuid(0);
clubName = reader.GetString(1);
sportTypeInt = reader.GetInt32(2);
}
}
}
// Fetch member count if club exists
if (clubId.HasValue && clubName != null && sportTypeInt.HasValue)
{
using (var memberCommand = connection.CreateCommand())
{
memberCommand.Transaction = transaction;
memberCommand.CommandText = @"
SELECT COUNT(*)
FROM members AS m
WHERE m.""ClubId"" = @clubId";
var param = memberCommand.CreateParameter();
param.ParameterName = "@clubId";
param.Value = clubId;
memberCommand.Parameters.Add(param);
var memberCountResult = await memberCommand.ExecuteScalarAsync();
var memberCount = memberCountResult != null ? Convert.ToInt32(memberCountResult) : 0;
var sportTypeEnum = ((SportType)sportTypeInt.Value).ToString();
clubDtos.Add(new ClubListDto(
clubId.Value,
clubName,
sportTypeEnum,
memberCount,
Guid.Parse(tenantId)
));
}
}
await transaction.CommitAsync();
}
catch (Exception ex)
{ {
memberCommand.Transaction = transaction; _logger.LogError(ex, "GetMyClubsAsync: Error processing tenant {TenantId}", tenantId);
memberCommand.CommandText = @" // Continue with next tenant instead of failing entirely
SELECT COUNT(*)
FROM members AS m
WHERE m.""ClubId"" = @clubId";
var param = memberCommand.CreateParameter();
param.ParameterName = "@clubId";
param.Value = clubId;
memberCommand.Parameters.Add(param);
var memberCountResult = await memberCommand.ExecuteScalarAsync();
var memberCount = memberCountResult != null ? Convert.ToInt32(memberCountResult) : 0;
var sportTypeEnum = ((SportType)sportTypeInt.Value).ToString();
clubDtos.Add(new ClubListDto(
clubId.Value,
clubName,
sportTypeEnum,
memberCount,
Guid.Parse(tenantId)
));
} }
} }
await transaction.CommitAsync(); _logger.LogInformation("GetMyClubsAsync: Returning {Count} clubs", clubDtos.Count);
return clubDtos;
}
catch (Exception ex)
{
_logger.LogError(ex, "GetMyClubsAsync: Unexpected error getting user clubs");
// Return empty list instead of throwing to prevent 500 error
return new List<ClubListDto>();
} }
return clubDtos;
} }
public async Task<ClubDetailDto?> GetCurrentClubAsync() public async Task<ClubDetailDto?> GetCurrentClubAsync()
{ {
var tenantId = _tenantProvider.GetTenantId(); try
{
var tenantId = _tenantProvider.GetTenantId();
var club = await _context.Clubs var club = await _context.Clubs
.FirstOrDefaultAsync(c => c.TenantId == tenantId); .FirstOrDefaultAsync(c => c.TenantId == tenantId);
if (club == null) if (club == null)
return null;
return new ClubDetailDto(
club.Id,
club.Name,
club.SportType.ToString(),
club.Description,
club.CreatedAt,
club.UpdatedAt
);
}
catch (Exception ex)
{
_logger.LogError(ex, "GetCurrentClubAsync: Error getting current club");
return null; return null;
}
return new ClubDetailDto(
club.Id,
club.Name,
club.SportType.ToString(),
club.Description,
club.CreatedAt,
club.UpdatedAt
);
} }
} }
@@ -9,7 +9,7 @@
"DefaultConnection": "Host=localhost;Port=5432;Database=workclub;Username=app;Password=apppass" "DefaultConnection": "Host=localhost;Port=5432;Database=workclub;Username=app;Password=apppass"
}, },
"Keycloak": { "Keycloak": {
"Authority": "http://localhost:8080/realms/workclub", "Authority": "http://localhost:30808/realms/workclub",
"Audience": "workclub-api" "Audience": "workclub-api"
} }
} }
+7 -7
View File
@@ -39,7 +39,7 @@ services:
KC_DB_PASSWORD: keycloakpass KC_DB_PASSWORD: keycloakpass
KC_HEALTH_ENABLED: "true" KC_HEALTH_ENABLED: "true"
KC_LOG_LEVEL: INFO KC_LOG_LEVEL: INFO
KC_HOSTNAME: "http://localhost:8080" KC_HOSTNAME: "http://localhost:30808"
KC_HOSTNAME_STRICT: "false" KC_HOSTNAME_STRICT: "false"
KC_PROXY: "edge" KC_PROXY: "edge"
KC_HTTP_PORT: "8081" KC_HTTP_PORT: "8081"
@@ -47,7 +47,7 @@ services:
KC_HOSTNAME_ADMIN: "http://keycloak:8081" KC_HOSTNAME_ADMIN: "http://keycloak:8081"
KC_SPI_HOSTNAME_DEFAULT_ADMIN: "keycloak:8081" KC_SPI_HOSTNAME_DEFAULT_ADMIN: "keycloak:8081"
ports: ports:
- "8080:8081" - "30808:8081"
volumes: volumes:
- ./infra/keycloak:/opt/keycloak/data/import - ./infra/keycloak:/opt/keycloak/data/import
depends_on: depends_on:
@@ -71,7 +71,7 @@ services:
Keycloak__Audience: "workclub-api" Keycloak__Audience: "workclub-api"
Keycloak__TokenValidationParameters__ValidateIssuer: "false" Keycloak__TokenValidationParameters__ValidateIssuer: "false"
ports: ports:
- "5001:8080" - "30501:8080"
extra_hosts: extra_hosts:
- "localhost:172.18.0.1" - "localhost:172.18.0.1"
- "127.0.0.1:172.18.0.1" - "127.0.0.1:172.18.0.1"
@@ -93,18 +93,18 @@ services:
extra_hosts: extra_hosts:
- "localhost:host-gateway" - "localhost:host-gateway"
environment: environment:
NEXT_PUBLIC_API_URL: "http://localhost:5001" NEXT_PUBLIC_API_URL: "http://localhost:30501"
API_INTERNAL_URL: "http://dotnet-api:8080" API_INTERNAL_URL: "http://dotnet-api:8080"
NEXTAUTH_SECRET: "dev-secret-change-in-production-use-openssl-rand-base64-32" NEXTAUTH_SECRET: "dev-secret-change-in-production-use-openssl-rand-base64-32"
AUTH_SECRET: "dev-secret-change-in-production-use-openssl-rand-base64-32" AUTH_SECRET: "dev-secret-change-in-production-use-openssl-rand-base64-32"
AUTH_TRUST_HOST: "true" AUTH_TRUST_HOST: "true"
KEYCLOAK_CLIENT_ID: "workclub-app" KEYCLOAK_CLIENT_ID: "workclub-app"
KEYCLOAK_CLIENT_SECRET: "dev-secret-workclub-api-change-in-production" KEYCLOAK_CLIENT_SECRET: "dev-secret-workclub-api-change-in-production"
KEYCLOAK_ISSUER: "http://localhost:8080/realms/workclub" KEYCLOAK_ISSUER: "http://localhost:30808/realms/workclub"
KEYCLOAK_ISSUER_INTERNAL: "http://keycloak:8081/realms/workclub" KEYCLOAK_ISSUER_INTERNAL: "http://keycloak:8081/realms/workclub"
NEXT_PUBLIC_KEYCLOAK_ISSUER: "http://localhost:8080/realms/workclub" NEXT_PUBLIC_KEYCLOAK_ISSUER: "http://localhost:30808/realms/workclub"
ports: ports:
- "3000:3000" - "30080:3000"
volumes: volumes:
- ./frontend:/app:cached - ./frontend:/app:cached
- /app/node_modules - /app/node_modules
+2
View File
@@ -16,6 +16,8 @@ COPY . .
# Set environment for build to ensure server binds to all interfaces # Set environment for build to ensure server binds to all interfaces
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"
ENV PORT="3000" ENV PORT="3000"
# Set API_INTERNAL_URL for build-time Next.js rewrites evaluation
ENV API_INTERNAL_URL="http://workclub-api:8080"
RUN bun run build RUN bun run build
# Stage 3: Runtime # Stage 3: Runtime
+1 -1
View File
@@ -48,7 +48,7 @@ function LoginContent() {
}; };
const handleSwitchAccount = () => { const handleSwitchAccount = () => {
const keycloakLogoutUrl = `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER || 'http://localhost:8080/realms/workclub'}/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent(window.location.origin + '/login')}`; const keycloakLogoutUrl = `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER || 'http://localhost:30808/realms/workclub'}/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent(window.location.origin + '/login')}`;
signOut({ redirect: false }).then(() => { signOut({ redirect: false }).then(() => {
window.location.href = keycloakLogoutUrl; window.location.href = keycloakLogoutUrl;
}); });
+16 -3
View File
@@ -24,10 +24,10 @@ declare module "next-auth" {
// In Docker, the Next.js server reaches Keycloak via internal hostname // In Docker, the Next.js server reaches Keycloak via internal hostname
// (keycloak:8080) but the browser uses localhost:8080. Explicit endpoint // (keycloak:8080) but the browser uses localhost:8080. Explicit endpoint
// URLs bypass OIDC discovery, avoiding issuer mismatch validation errors. // URLs bypass OIDC discovery, avoiding issuer mismatch validation errors.
const issuerPublic = process.env.KEYCLOAK_ISSUER || 'http://localhost:8080/realms/workclub' const issuerPublic = process.env.KEYCLOAK_ISSUER || 'http://localhost:30808/realms/workclub'
const issuerInternal = process.env.KEYCLOAK_ISSUER_INTERNAL || issuerPublic const issuerInternal = process.env.KEYCLOAK_ISSUER_INTERNAL || issuerPublic
const oidcPublic = `${issuerPublic}/protocol/openid-connect` const oidcPublic = `${issuerPublic}/protocol/openid-connect`
const oidcInternal = `${issuerInternal.replace(':8080', ':8081')}/protocol/openid-connect` const oidcInternal = `${issuerInternal}/protocol/openid-connect`
export const { handlers, signIn, signOut, auth } = NextAuth({ export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [ providers: [
@@ -71,14 +71,21 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
// Add clubs claim from Keycloak access token // Add clubs claim from Keycloak access token
token.clubs = (account as { clubs?: Record<string, string> }).clubs || {} token.clubs = (account as { clubs?: Record<string, string> }).clubs || {}
token.accessToken = account.access_token token.accessToken = account.access_token
}
// Always check admin status from the access token if available
if (token.accessToken) {
try { try {
const payload = JSON.parse(Buffer.from((token.accessToken as string).split('.')[1], 'base64').toString()); const payload = JSON.parse(Buffer.from((token.accessToken as string).split('.')[1], 'base64').toString());
const roles = (payload.realm_access?.roles as string[]) || []; const roles = (payload.realm_access?.roles as string[]) || [];
token.isAdmin = roles.includes('admin'); token.isAdmin = roles.includes('admin');
} catch { console.log('[Auth Debug] Checking admin status:', { roles, isAdmin: token.isAdmin });
} catch (e) {
console.error('[Auth Debug] Failed to check admin status:', e);
token.isAdmin = false; token.isAdmin = false;
} }
} else {
console.log('[Auth Debug] No access token available');
} }
return token return token
}, },
@@ -89,6 +96,12 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
session.user.isAdmin = token.isAdmin as boolean | undefined session.user.isAdmin = token.isAdmin as boolean | undefined
} }
session.accessToken = token.accessToken as string | undefined session.accessToken = token.accessToken as string | undefined
// Log session data for debugging
console.log('[Session Debug] Session user:', session.user);
console.log('[Session Debug] Token isAdmin:', token.isAdmin);
console.log('[Session Debug] Session isAdmin:', session.user?.isAdmin);
return session return session
} }
} }
+7
View File
@@ -60,6 +60,13 @@ export function AuthGuard({ children }: { children: ReactNode }) {
} }
const isAdmin = data?.user?.isAdmin; const isAdmin = data?.user?.isAdmin;
// Debug: Log auth state
console.log('[AuthGuard Debug] status:', status);
console.log('[AuthGuard Debug] isAdmin:', isAdmin);
console.log('[AuthGuard Debug] data?.user:', data?.user);
console.log('[AuthGuard Debug] clubs.length:', clubs.length);
if (clubs.length === 0 && status === 'authenticated' && !isAdmin) { if (clubs.length === 0 && status === 'authenticated' && !isAdmin) {
const handleSwitchAccount = () => { const handleSwitchAccount = () => {
const keycloakLogoutUrl = `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER || 'http://localhost:8080/realms/workclub'}/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent(window.location.origin + '/login')}`; const keycloakLogoutUrl = `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER || 'http://localhost:8080/realms/workclub'}/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent(window.location.origin + '/login')}`;
+5 -5
View File
@@ -10,8 +10,8 @@ spec:
selector: selector:
app: workclub-api app: workclub-api
ports: ports:
- name: http - name: http
port: 80 port: 8080
targetPort: 8080 targetPort: 8080
nodePort: 30081 nodePort: 30501
protocol: TCP protocol: TCP
+4 -4
View File
@@ -6,10 +6,10 @@ metadata:
app: workclub app: workclub
data: data:
log-level: "Information" log-level: "Information"
cors-origins: "http://localhost:3000,http://192.168.240.200:30080" cors-origins: "http://localhost:30080,http://192.168.240.200:30080,http://192.168.240.200:30808"
api-base-url: "http://192.168.240.200:30081" api-base-url: "http://192.168.240.200:30501"
keycloak-url: "http://192.168.240.200:30082" keycloak-url: "http://192.168.240.200:30808"
keycloak-authority: "http://192.168.240.200:30082/realms/workclub" keycloak-authority: "http://192.168.240.200:30808/realms/workclub"
keycloak-audience: "workclub-api" keycloak-audience: "workclub-api"
keycloak-realm: "workclub" keycloak-realm: "workclub"
+3 -3
View File
@@ -40,7 +40,6 @@ spec:
periodSeconds: 15 periodSeconds: 15
timeoutSeconds: 5 timeoutSeconds: 5
failureThreshold: 3 failureThreshold: 3
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
@@ -48,10 +47,11 @@ spec:
limits: limits:
cpu: 500m cpu: 500m
memory: 512Mi memory: 512Mi
env: env:
- name: NODE_ENV - name: NODE_ENV
value: "production" value: "production"
- name: API_INTERNAL_URL
value: "http://workclub-api:8080"
- name: NEXT_PUBLIC_API_URL - name: NEXT_PUBLIC_API_URL
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:
@@ -89,4 +89,4 @@ spec:
name: workclub-config name: workclub-config
key: keycloak-authority key: keycloak-authority
- name: KEYCLOAK_ISSUER_INTERNAL - name: KEYCLOAK_ISSUER_INTERNAL
value: "http://workclub-keycloak/realms/workclub" value: "http://workclub-keycloak:8080/realms/workclub"
@@ -0,0 +1,92 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: workclub-frontend
labels:
app: workclub-frontend
component: frontend
spec:
replicas: 1
selector:
matchLabels:
app: workclub-frontend
template:
metadata:
labels:
app: workclub-frontend
component: frontend
spec:
containers:
- name: frontend
image: 192.168.241.13:8080/workclub-frontend:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 3000
protocol: TCP
readinessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 2
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
env:
- name: NODE_ENV
value: "production"
- name: NEXT_PUBLIC_API_URL
valueFrom:
configMapKeyRef:
name: workclub-config
key: api-base-url
- name: NEXT_PUBLIC_KEYCLOAK_URL
valueFrom:
configMapKeyRef:
name: workclub-config
key: keycloak-url
- name: NEXT_PUBLIC_KEYCLOAK_ISSUER
valueFrom:
configMapKeyRef:
name: workclub-config
key: keycloak-authority
- name: NEXTAUTH_URL
value: "http://192.168.240.200:3000"
- name: AUTH_TRUST_HOST
value: "true"
- name: NEXTAUTH_SECRET
valueFrom:
secretKeyRef:
name: workclub-secrets
key: nextauth-secret
- name: KEYCLOAK_CLIENT_ID
value: "workclub-app"
- name: KEYCLOAK_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: workclub-secrets
key: keycloak-client-secret
- name: KEYCLOAK_ISSUER
valueFrom:
configMapKeyRef:
name: workclub-config
key: keycloak-authority
- name: KEYCLOAK_ISSUER_INTERNAL
value: "http://workclub-keycloak/realms/workclub"
+5 -5
View File
@@ -10,8 +10,8 @@ spec:
selector: selector:
app: workclub-frontend app: workclub-frontend
ports: ports:
- name: http - name: http
port: 80 port: 3000
targetPort: 3000 targetPort: 3000
nodePort: 30080 nodePort: 30080
protocol: TCP protocol: TCP
+1
View File
@@ -26,6 +26,7 @@ spec:
args: args:
- start-dev - start-dev
- --import-realm - --import-realm
- --import-realm-overwrite
ports: ports:
- name: http - name: http
containerPort: 8080 containerPort: 8080
@@ -68,18 +68,18 @@ data:
"enabled": true, "enabled": true,
"protocol": "openid-connect", "protocol": "openid-connect",
"publicClient": true, "publicClient": true,
"redirectUris": [ "redirectUris": [
"http://localhost:3000/*", "http://localhost:30080/*",
"http://localhost:3001/*", "http://localhost:30081/*",
"http://workclub-frontend/*", "http://workclub-frontend/*",
"http://192.168.240.200:30080/*" "http://192.168.240.200:30080/*"
], ],
"webOrigins": [ "webOrigins": [
"http://localhost:3000", "http://localhost:30080",
"http://localhost:3001", "http://localhost:30081",
"http://workclub-frontend", "http://workclub-frontend",
"http://192.168.240.200:30080" "http://192.168.240.200:30080"
], ],
"directAccessGrantsEnabled": true, "directAccessGrantsEnabled": true,
"standardFlowEnabled": true, "standardFlowEnabled": true,
"implicitFlowEnabled": false, "implicitFlowEnabled": false,
@@ -150,11 +150,11 @@ data:
"realmRoles": [ "realmRoles": [
"admin" "admin"
], ],
"attributes": { "attributes": {
"clubs": [ "clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,Admin,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda,Member" "64e05b5e-ef45-81d7-f2e8-3d14bd197383,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda"
] ]
} }
}, },
{ {
"username": "manager@test.com", "username": "manager@test.com",
@@ -172,11 +172,11 @@ data:
"realmRoles": [ "realmRoles": [
"manager" "manager"
], ],
"attributes": { "attributes": {
"clubs": [ "clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,Manager" "64e05b5e-ef45-81d7-f2e8-3d14bd197383"
] ]
} }
}, },
{ {
"username": "member1@test.com", "username": "member1@test.com",
@@ -194,11 +194,11 @@ data:
"realmRoles": [ "realmRoles": [
"member" "member"
], ],
"attributes": { "attributes": {
"clubs": [ "clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,Member,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda,Member" "64e05b5e-ef45-81d7-f2e8-3d14bd197383,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda"
] ]
} }
}, },
{ {
"username": "member2@test.com", "username": "member2@test.com",
@@ -216,11 +216,11 @@ data:
"realmRoles": [ "realmRoles": [
"member" "member"
], ],
"attributes": { "attributes": {
"clubs": [ "clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,Member" "64e05b5e-ef45-81d7-f2e8-3d14bd197383"
] ]
} }
}, },
{ {
"username": "viewer@test.com", "username": "viewer@test.com",
@@ -238,11 +238,11 @@ data:
"realmRoles": [ "realmRoles": [
"viewer" "viewer"
], ],
"attributes": { "attributes": {
"clubs": [ "clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,Viewer" "64e05b5e-ef45-81d7-f2e8-3d14bd197383"
] ]
} }
} }
] ]
} }
+5 -5
View File
@@ -10,8 +10,8 @@ spec:
selector: selector:
app: workclub-keycloak app: workclub-keycloak
ports: ports:
- name: http - name: http
port: 80 port: 8080
targetPort: 8080 targetPort: 8080
nodePort: 30082 nodePort: 30808
protocol: TCP protocol: TCP
+6 -4
View File
@@ -86,14 +86,14 @@
"authorizationServicesEnabled": false, "authorizationServicesEnabled": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"redirectUris": [ "redirectUris": [
"http://localhost:3000/*" "http://localhost:30080/*"
], ],
"webOrigins": [ "webOrigins": [
"http://localhost:3000" "http://localhost:30080"
], ],
"attributes": { "attributes": {
"pkce.code.challenge.method": "S256", "pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "http://localhost:3000/*", "post.logout.redirect.uris": "http://localhost:30080/*",
"access.token.lifespan": "3600" "access.token.lifespan": "3600"
}, },
"protocolMappers": [ "protocolMappers": [
@@ -162,7 +162,9 @@
"firstName": "Admin", "firstName": "Admin",
"lastName": "User", "lastName": "User",
"attributes": { "attributes": {
"clubs": [] "clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda"
]
}, },
"credentials": [ "credentials": [
{ {