Skip to content

Commit

Permalink
v1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxWaldorf committed Jan 3, 2025
2 parents fe7265c + 21bad33 commit df96bc5
Show file tree
Hide file tree
Showing 28 changed files with 2,719 additions and 1,118 deletions.
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#Ignore npm files
**/.env
**/.parcel-cache/*
**.lock
**/dist/*
**/node_modules/*
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/docker-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: Build Dev Docker Image

# Controls when the workflow will run
on:
schedule:
- cron: '0 0 * * *'
push:
branches: [ dev ]
pull_request:
Expand Down
32 changes: 22 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Specific
**/.env
**/.parcel-cache/*
**/dist/*
**/node_modules/*

# Logs
Expand Down Expand Up @@ -78,6 +76,16 @@ web_modules/
# Yarn Integrity file
.yarn-integrity

# Yarn lock file
yarn.lock

# Yarn cache directory
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# dotenv environment variable files
.env
.env.development.local
Expand Down Expand Up @@ -125,12 +133,16 @@ dist
# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# VS Code directories
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# SvelteKit build / generate output
.svelte-kit

# Temporary folders
tmp/
temp/
51 changes: 29 additions & 22 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# Base image for Node.js applications
ARG NODE_BASE_IMAGE=stable-alpine
FROM nginx:${NODE_BASE_IMAGE}
FROM node:lts-alpine

ARG APPLICATION="keygen-sh-ssp"
ARG BUILD_RFC3339="2024-08-30T20:00:00Z"
ARG BUILD_RFC3339="2024-12-29T16:00:00Z"
ARG REVISION="local"
ARG DESCRIPTION="Fully Packaged Self-Service Portal for Keygen.sh with keycloak SSO"
ARG PACKAGE="flcontainers/keygen-sh-ssp"
ARG VERSION="0.1.0"
ARG VERSION="1.0.0"

LABEL org.opencontainers.image.ref.name="${PACKAGE}" \
org.opencontainers.image.created=$BUILD_RFC3339 \
Expand All @@ -24,30 +23,38 @@ LABEL org.opencontainers.image.ref.name="${PACKAGE}" \
ENV NODE_ENV=production

# node app directory
RUN mkdir -p /app/node/portal
RUN mkdir -p /app/node
RUN mkdir -p /app/node/sessions

# Install nginx
RUN apk add --no-cache nodejs yarn
# Install required packages
RUN apk add --no-cache yarn netcat-openbsd sqlite sqlite-dev

# Build Portal
WORKDIR /app/node/portal
COPY portal/ .
RUN yarn
WORKDIR /app/node
COPY app/ .

# Change rights
#RUN chown www-data:www-data -R /app/node
# Production build with error logging
RUN yarn install --production \
&& yarn cache clean

# Copy Nginx configuration file
COPY nginx/proxy.conf /etc/nginx/conf.d/default.conf
# Add healthcheck with proper logging
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD nc -z localhost 3000 || (echo "Health check failed" && exit 1)

# Copy a custom startup script
COPY script/startup.sh /startup.sh
COPY script/cloudflare_nginx.sh /cloudflare_nginx.sh
RUN chmod +x /*.sh
# Add startup script with error handling first
COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh

# Create non-root user and set permissions after chmod
RUN adduser -D nodeuser && \
chown -R nodeuser:nodeuser /app/node && \
chown nodeuser:nodeuser /docker-entrypoint.sh && \
chown -R nodeuser:nodeuser /app/node/sessions

WORKDIR /app/node/portal
#USER www-data
# Switch to non-root user
USER nodeuser

EXPOSE 80
CMD ["/startup.sh"]
EXPOSE 3000

ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["yarn", "start"]
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ The portal offers the following functionality:
- License information e.g. expiration date.
- Machine deactivation.
- Protection behind SSO Portal
- Admin portal
- Env variables

## Env Variables
_Note: Mandatory varibales marked with *_

__Keycloak data:__
- KEYCLOAK_ID * (Client ID. Note: client must be public)
- KEYCLOAK_SECRET * (Client secret)
- KEYCLOAK_REALM * (Realm name)
- KEYCLOAK_URL * (URL of the server) / e.g. https://kc.myserver.org/

Expand All @@ -24,8 +26,6 @@ __Keygen data:__
- KEYGEN_TOKEN * (admin -bearer- token)

__Other data:__
- DOMAIN * (domain of the ssp portal) / e.g. my-domain.com:port (port if required)
- REQUEST_EMAIL (Support Email for license request)
- CLOUDFLARE (0 or 1 to use real IP from cloudflare)
- SESSION * (Session secret for your portal)

Note: You can use a local .env at root of the application or container variables...
122 changes: 122 additions & 0 deletions app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const Keycloak = require('keycloak-connect');
const path = require('path');
const SQLiteStore = require('connect-sqlite3')(session);

const app = express();

// Set trust proxy for production environment
if (process.env.NODE_ENV === 'production') {
// Enable trust proxy in production
app.set('trust proxy', true);

// Force HTTPS in production
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
});
} else {
// Default trust proxy setting for development
app.set('trust proxy', false);
}

// Set up EJS as the view engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// Create store based on environment
let store;
if (process.env.NODE_ENV === 'production') {
store = new SQLiteStore({
dir: './sessions', // Directory where SQLite db will be saved
db: 'sessions.db', // Database filename
table: 'sessions', // Table name to use
});
console.log('Using SQLite session store for production');
} else {
store = new session.MemoryStore();
console.log('Using Memory session store for development');
}

// Session setup for Keycloak
app.use(
session({
store: store,
secret: process.env.SESSION,
resave: false,
saveUninitialized: true,
cookie: {
maxAge: 24 * 60 * 60 * 1000, // 24 hours in milliseconds
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
sameSite: 'strict'
},
rolling: true // Resets the cookie expiration on every response
})
);

// Serve static files from public directory
app.use(express.static(path.join(__dirname, 'public')));

// Keycloak config
const keycloakConfig = {
'realm': process.env.KEYCLOAK_REALM,
'auth-server-url': process.env.KEYCLOAK_URL,
'ssl-required': 'external',
'resource': process.env.KEYCLOAK_ID,
"credentials": {
"secret": process.env.KEYCLOAK_SECRET
},
"confidential-port": 0
};

// Initialize Keycloak with store
const keycloak = new Keycloak({
store: store,
clearExpired: true, // Add this option
checkInterval: 300 // Check every 5 minutes
}, keycloakConfig);
app.use(keycloak.middleware());

// Middleware to check client role
function checkClientRole(role) {
return (req, res, next) => {
const clientId = process.env.KEYCLOAK_ID;
const clientRoles = req.kauth.grant.access_token.content.resource_access?.[clientId]?.roles || [];

if (clientRoles.includes(role)) {
return next();
}
res.status(403).send('Forbidden: You do not have access to this resource.');
};
}

// Route handlers
const userRoutes = require('./routes/user');
const adminRoutes = require('./routes/admin');
const licenseRoutes = require('./routes/licenses');

// Basic authentication for user routes - just needs to be logged in
app.use('/', keycloak.protect(), userRoutes);

// Admin routes require both authentication and admin role
app.use('/admin', keycloak.protect(), checkClientRole('admin'), adminRoutes);

// Redirect root to user dashboard
app.get('/', keycloak.protect(), (req, res) => {
res.redirect('/dashboard');
});

// Middleware to parse JSON bodies
app.use(express.json());

// Protected license routes
app.use('/api', keycloak.protect(), licenseRoutes);

const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
29 changes: 29 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "keygen-sh-ssp",
"version": "1.0.0",
"description": "Fully Packaged Self-Service Portal for Keygen.sh with keycloak SSO",
"main": "index.js",
"author": "MaxWaldorf",
"license": "MIT",
"private": true,
"browserslist": "last 2 versions, not dead",
"engines": {
"node": ">=20"
},
"scripts": {
"build": "yarn install",
"start": "node ./index.js"
},
"dependencies": {
"axios": "^1.7.9",
"connect-sqlite3": "^0.9.15",
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^4.21.2",
"express-session": "^1.18.1",
"keycloak-connect": "^26.0.7",
"path": "^0.12.7",
"sqlite3": "^5.1.7",
"winston": "^3.17.0"
}
}
Loading

0 comments on commit df96bc5

Please sign in to comment.