Compare commits

...

4 Commits

Author SHA1 Message Date
Jakub Kaniecki
1befef102f deploy 2025-03-30 18:59:15 +02:00
Jakub Kaniecki
18a019fbc2 deploy
Some checks failed
continuous-integration/drone Build was killed
2025-03-30 15:13:15 +02:00
Jakub Kaniecki
a39c5dac4c deploy
Some checks failed
continuous-integration/drone Build was killed
2025-03-30 14:58:25 +02:00
Jakub Kaniecki
e5cf69f08f deploy 2025-03-30 14:55:31 +02:00
14 changed files with 357 additions and 31 deletions

37
.dockerignore Normal file
View File

@@ -0,0 +1,37 @@
# Dependencies
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
# Build output
dist
dist-ssr
build
# Version control
.git
.gitignore
# Environment files
.env
.env.local
.env.*.local
# IDE and editor files
.idea
.vscode
*.swp
*.swo
.DS_Store
# Test files
coverage
.nyc_output
# Misc
README.md
.drone.yaml
k8s/
backend/
*.log

34
.drone.yaml Normal file
View File

@@ -0,0 +1,34 @@
kind: pipeline
type: exec
name: default
steps:
- name: build-frontend
commands:
- docker build --no-cache -t knck-frontend:latest .
- docker tag knck-frontend:latest registry.knck.pl:5000/knck-frontend:latest
- docker push registry.knck.pl:5000/knck-frontend:latest
- name: build-backend
commands:
- docker build --no-cache -t knck-backend:latest ./backend
- docker tag knck-backend:latest registry.knck.pl:5000/knck-backend:latest
- docker push registry.knck.pl:5000/knck-backend:latest
- name: delete
environment:
KUBECONFIG: /home/drone-runner/drone-kubeconfig
commands:
- kubectl delete deployment knck-app || true
- name: deploy
environment:
KUBECONFIG: /home/drone-runner/drone-kubeconfig
commands:
- kubectl apply -f k8s/combined-deployment.yaml --insecure-skip-tls-verify
- kubectl apply -f k8s/combined-service.yaml --insecure-skip-tls-verify
- kubectl apply -f k8s/combined-ingress.yaml --insecure-skip-tls-verify
trigger:
branch:
- main

View File

@@ -4,7 +4,7 @@ FROM node:20-alpine as build
WORKDIR /app WORKDIR /app
# Copy package files # Copy package files
COPY package*.json ./ COPY package.json package-lock.json ./
# Install dependencies # Install dependencies
RUN npm ci RUN npm ci

34
backend/.dockerignore Normal file
View File

@@ -0,0 +1,34 @@
# Dependencies
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
# Environment files
.env
.env.local
.env.*.local
# Version control
.git
.gitignore
# IDE and editor files
.idea
.vscode
*.swp
*.swo
.DS_Store
# Test files
coverage
.nyc_output
# Logs
logs
*.log
# Misc
README.md
.drone.yaml
k8s/

View File

@@ -6,7 +6,7 @@ WORKDIR /app
COPY package*.json ./ COPY package*.json ./
# Install dependencies # Install dependencies
RUN npm ci --only=production RUN npm install --only=production
# Copy source code # Copy source code
COPY . . COPY . .

View File

@@ -11,18 +11,26 @@ dotenv.config();
const app = express(); const app = express();
const port = process.env.PORT || 3001; const port = process.env.PORT || 3001;
// Trust proxy for rate limiting behind Traefik
app.set('trust proxy', 1);
// Middleware // Middleware
app.use(helmet()); app.use(helmet());
app.use(express.json()); app.use(express.json());
app.use(cors({ app.use(cors({
origin: process.env.CORS_ORIGIN, origin: process.env.CORS_ORIGIN || 'https://knck.pl',
methods: ['POST'] methods: ['POST']
})); }));
// Rate limiting // Rate limiting
const limiter = rateLimit({ const limiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 900000, // 15 minutes windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 900000, // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 5 // 5 requests per window max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 5, // 5 requests per window
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
keyGenerator: (req) => {
return req.ip || req.socket.remoteAddress;
}
}); });
app.use('/api/contact', limiter); app.use('/api/contact', limiter);
@@ -30,10 +38,34 @@ app.use('/api/contact', limiter);
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT), port: parseInt(process.env.SMTP_PORT),
secure: false, secure: false, // true for 465, false for other ports like 587
auth: { auth: {
type: 'login',
user: process.env.SMTP_USER, user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS pass: process.env.SMTP_PASS
},
tls: {
rejectUnauthorized: false // Required for some SMTP servers
},
debug: true, // Enable debug logging
logger: true // Enable logger
});
// Verify SMTP connection configuration
transporter.verify(function(error, success) {
if (error) {
console.error('SMTP connection error:', {
message: error.message,
code: error.code,
response: error.response,
command: error.command,
stack: error.stack,
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
user: process.env.SMTP_USER
});
} else {
console.log('SMTP server is ready to take our messages');
} }
}); });
@@ -73,7 +105,13 @@ ${message}
res.status(200).json({ message: 'Email sent successfully' }); res.status(200).json({ message: 'Email sent successfully' });
} catch (error) { } catch (error) {
console.error('Error sending email:', error); console.error('Error sending email:', {
message: error.message,
code: error.code,
response: error.response,
command: error.command,
stack: error.stack
});
res.status(500).json({ error: 'Failed to send email' }); res.status(500).json({ error: 'Failed to send email' });
} }
}); });

160
combined.yaml Normal file
View File

@@ -0,0 +1,160 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: knck-app
labels:
app: knck-app
spec:
replicas: 2
selector:
matchLabels:
app: knck-app
template:
metadata:
labels:
app: knck-app
spec:
containers:
- name: knck-frontend
image: knck-frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 80
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "100m"
memory: "128Mi"
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20
- name: knck-backend
image: knck-backend:latest
imagePullPolicy: Always
ports:
- containerPort: 3001
env:
- name: PORT
value: "3001"
- name: NODE_ENV
value: "production"
- name: SMTP_HOST
valueFrom:
secretKeyRef:
name: knck-secrets
key: smtp-host
- name: SMTP_PORT
valueFrom:
secretKeyRef:
name: knck-secrets
key: smtp-port
- name: SMTP_USER
valueFrom:
secretKeyRef:
name: knck-secrets
key: smtp-user
- name: SMTP_PASS
valueFrom:
secretKeyRef:
name: knck-secrets
key: smtp-pass
- name: SMTP_FROM
valueFrom:
secretKeyRef:
name: knck-secrets
key: smtp-from
- name: SMTP_TO
valueFrom:
secretKeyRef:
name: knck-secrets
key: smtp-to
- name: CORS_ORIGIN
value: "https://knck.pl"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "256Mi"
readinessProbe:
httpGet:
path: /health
port: 3001
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3001
initialDelaySeconds: 15
periodSeconds: 20
---
kind: Ingress
metadata:
name: knck-ingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
traefik.ingress.kubernetes.io/service.serversscheme: http
traefik.ingress.kubernetes.io/service.passhostheader: "true"
traefik.ingress.kubernetes.io/router.priority: "1"
cert-manager.io/cluster-issuer: letsencrypt-prod
acme.cert-manager.io/http01-edit-in-place: "true"
acme.cert-manager.io/http01-ingress-class: traefik
spec:
tls:
- hosts:
- knck.pl
- api.knck.pl
secretName: knck-tls
rules:
- host: knck.pl
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: knck-app
port:
number: 80
- host: api.knck.pl
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: knck-app
port:
number: 3001
---
kind: Service
metadata:
name: knck-app
spec:
selector:
app: knck-app
ports:
- name: frontend
protocol: TCP
port: 80
targetPort: 80
- name: backend
protocol: TCP
port: 3001
targetPort: 3001
type: ClusterIP

26
docker-compose.yml Normal file
View File

@@ -0,0 +1,26 @@
version: '3.8'
services:
frontend:
build:
context: .
dockerfile: Dockerfile
ports:
- "80:80"
depends_on:
- backend
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "3001:3001"
environment:
- SMTP_HOST=smtp.mail.me.com
- SMTP_PORT=587
- SMTP_USER=your-email@me.com
- SMTP_PASS=your-app-specific-password
- SMTP_FROM=your-email@me.com
- SMTP_TO=your-email@me.com
- CORS_ORIGIN=http://localhost

View File

@@ -79,8 +79,6 @@ spec:
secretKeyRef: secretKeyRef:
name: knck-secrets name: knck-secrets
key: smtp-to key: smtp-to
- name: CORS_ORIGIN
value: "https://knck.pl"
resources: resources:
requests: requests:
cpu: "100m" cpu: "100m"

View File

@@ -3,15 +3,19 @@ kind: Ingress
metadata: metadata:
name: knck-ingress name: knck-ingress
annotations: annotations:
kubernetes.io/ingress.class: nginx traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
traefik.ingress.kubernetes.io/service.serversscheme: http
traefik.ingress.kubernetes.io/service.passhostheader: "true"
traefik.ingress.kubernetes.io/router.priority: "1"
cert-manager.io/cluster-issuer: letsencrypt-prod cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true" acme.cert-manager.io/http01-edit-in-place: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "8m" acme.cert-manager.io/http01-ingress-class: traefik
spec: spec:
tls: tls:
- hosts: - hosts:
- knck.pl - knck.pl
- api.knck.pl
secretName: knck-tls secretName: knck-tls
rules: rules:
- host: knck.pl - host: knck.pl
@@ -24,10 +28,7 @@ spec:
name: knck-app name: knck-app
port: port:
number: 80 number: 80
- host: api.knck.pl - path: /api
http:
paths:
- path: /
pathType: Prefix pathType: Prefix
backend: backend:
service: service:

View File

@@ -4,10 +4,11 @@ metadata:
name: knck-secrets name: knck-secrets
type: Opaque type: Opaque
data: data:
# These values should be base64 encoded # Replace these values with your base64 encoded actual values
smtp-host: c210cC5tYWlsLm1lLmNvbQ== # smtp.mail.me.com # Example: echo -n "your-actual-value" | base64
smtp-port: NTg3 # 587 smtp-host: REPLACE_WITH_YOUR_BASE64_ENCODED_SMTP_HOST
smtp-user: eW91ci1lbWFpbEBtZS5jb20= # your-email@me.com smtp-port: REPLACE_WITH_YOUR_BASE64_ENCODED_SMTP_PORT
smtp-pass: eW91ci1hcHAtc3BlY2lmaWMtcGFzc3dvcmQ= # your-app-specific-password smtp-user: REPLACE_WITH_YOUR_BASE64_ENCODED_SMTP_USER
smtp-from: eW91ci1lbWFpbEBtZS5jb20= # your-email@me.com smtp-pass: REPLACE_WITH_YOUR_BASE64_ENCODED_SMTP_PASSWORD
smtp-to: eW91ci1lbWFpbEBtZS5jb20= # your-email@me.com smtp-from: REPLACE_WITH_YOUR_BASE64_ENCODED_SMTP_FROM
smtp-to: REPLACE_WITH_YOUR_BASE64_ENCODED_SMTP_TO

View File

@@ -23,13 +23,13 @@ const LandingPage: FC = () => {
<img src="https://picsum.photos/400/400" alt="Jakub Kaniecki" /> <img src="https://picsum.photos/400/400" alt="Jakub Kaniecki" />
</div> </div>
<div className={styles.socialIcons}> <div className={styles.socialIcons}>
<a href="https://github.com/yourusername" target="_blank" rel="noopener noreferrer" className={styles.iconLink}> <a href="https://github.com/knckj" target="_blank" rel="noopener noreferrer" className={styles.iconLink}>
<FaGithub /> <FaGithub />
</a> </a>
<a href="https://linkedin.com/in/yourusername" target="_blank" rel="noopener noreferrer" className={styles.iconLink}> <a href="https://linkedin.com/in/jakub-kaniecki" target="_blank" rel="noopener noreferrer" className={styles.iconLink}>
<FaLinkedin /> <FaLinkedin />
</a> </a>
<a href="https://instagram.com/yourusername" target="_blank" rel="noopener noreferrer" className={styles.iconLink}> <a href="https://instagram.com/knck_jkb" target="_blank" rel="noopener noreferrer" className={styles.iconLink}>
<FaInstagram /> <FaInstagram />
</a> </a>
</div> </div>
@@ -108,7 +108,7 @@ const LandingPage: FC = () => {
</main> </main>
<footer className={styles.footer}> <footer className={styles.footer}>
<p>&copy; 2024 knck.pl. All rights reserved.</p> <p>&copy; 2025 knck.pl. All rights reserved.</p>
</footer> </footer>
</div> </div>
); );

View File

@@ -25,7 +25,6 @@ interface GroupedPosts {
const Blog: FC = () => { const Blog: FC = () => {
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const [posts, setPosts] = useState<BlogPost[]>([]);
const [groupedPosts, setGroupedPosts] = useState<GroupedPosts>({}); const [groupedPosts, setGroupedPosts] = useState<GroupedPosts>({});
const [currentPost, setCurrentPost] = useState<BlogPost | null>(null); const [currentPost, setCurrentPost] = useState<BlogPost | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -66,8 +65,6 @@ const Blog: FC = () => {
const sortedPosts = loadedPosts.sort( const sortedPosts = loadedPosts.sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
); );
setPosts(sortedPosts);
// Group posts by year and month // Group posts by year and month
const grouped: GroupedPosts = {}; const grouped: GroupedPosts = {};

View File

@@ -19,7 +19,7 @@ const Contact: FC = () => {
setStatus({ type: null, message: '' }); setStatus({ type: null, message: '' });
try { try {
const response = await fetch('https://api.knck.pl/api/contact', { const response = await fetch('/api/contact', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',