deploy
This commit is contained in:
parent
18a019fbc2
commit
1befef102f
@ -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
|
||||||
|
|||||||
@ -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 . .
|
||||||
|
|||||||
@ -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
160
combined.yaml
Normal 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
26
docker-compose.yml
Normal 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
|
||||||
@ -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"
|
||||||
|
|||||||
@ -16,7 +16,6 @@ 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
|
||||||
@ -29,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:
|
||||||
|
|||||||
@ -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
|
||||||
@ -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>© 2024 knck.pl. All rights reserved.</p>
|
<p>© 2025 knck.pl. All rights reserved.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 = {};
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user