Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbf58046e7 | ||
|
|
d238965c2c | ||
|
|
53ecda9f7a | ||
|
|
fc535a558f | ||
|
|
f7ac25f91f | ||
|
|
9d8b4a59fb | ||
|
|
60804c12b0 | ||
|
|
90f8170efb |
@@ -62,7 +62,7 @@ jobs:
|
||||
IMAGE="ghcr.io/${{ github.repository }}"
|
||||
|
||||
if [[ "${{ steps.check-release.outputs.is_release }}" == "true" ]]; then
|
||||
# This is a release: use semver tags
|
||||
# This is a release: use semver tags + latest
|
||||
TAG="${{ steps.check-release.outputs.release_tag }}"
|
||||
VERSION="${TAG#v}"
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
@@ -70,9 +70,9 @@ jobs:
|
||||
TAGS="${VERSION},${MAJOR}.${MINOR},${MAJOR},latest"
|
||||
echo "Building RELEASE with tags: $TAGS"
|
||||
else
|
||||
# Regular commit: use SHA tags
|
||||
# Regular commit: use SHA tags only (no latest)
|
||||
SHORT_SHA="${GITHUB_SHA:0:7}"
|
||||
TAGS="sha-${SHORT_SHA},latest"
|
||||
TAGS="sha-${SHORT_SHA}"
|
||||
echo "Building COMMIT with tags: $TAGS"
|
||||
fi
|
||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
|
||||
# Merge platform-specific images into multi-arch manifest
|
||||
merge:
|
||||
needs: [prepare, build]
|
||||
needs: [ prepare, build ]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.1.0"
|
||||
".": "0.1.1"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.1](https://github.com/useplunk/plunk/compare/v0.1.0...v0.1.1) (2025-12-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add additional verification in Oauth controllers ([53ecda9](https://github.com/useplunk/plunk/commit/53ecda9f7a34111785ac3ea3af18cb33460a44af))
|
||||
* add clear cache button on full-screen loader ([60804c1](https://github.com/useplunk/plunk/commit/60804c12b0b4c4c3ef97e730e4857d41fa1fd021))
|
||||
* Dedicated token name for next version ([fc535a5](https://github.com/useplunk/plunk/commit/fc535a558fab28045dae9ce978f483e58c72a24f))
|
||||
* only mark releases as latest ([9d8b4a5](https://github.com/useplunk/plunk/commit/9d8b4a59fbd42420bb595d2a44df93a29214121a))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* dynamic link to Docker Compose for self-hosting ([90f8170](https://github.com/useplunk/plunk/commit/90f8170efbad0cec981a24e7831840aac65d8d70))
|
||||
* Update AWS setup docs ([f7ac25f](https://github.com/useplunk/plunk/commit/f7ac25f91f4e1dcce301fb325801a67f2a9dee07))
|
||||
|
||||
## [0.1.0](https://github.com/useplunk/plunk/compare/v0.0.1...v0.1.0) (2025-12-08)
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ The easiest way to self-host Plunk is by using the `plunk` Docker image.
|
||||
You can pull the latest image from [Github](https://github.com/useplunk/plunk/pkgs/container/plunk).
|
||||
|
||||
A complete guide on how to deploy Plunk can be found in
|
||||
the [documentation](https://docs.useplunk.com/self-hosting/introduction).
|
||||
the [documentation](https://next-wiki.useplunk.com/self-hosting/introduction).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@ export class Github {
|
||||
}
|
||||
const {code} = req.query;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
return res.redirect(DASHBOARD_URI + '/auth/login?message=Invalid OAuth callback');
|
||||
}
|
||||
|
||||
const data = new URLSearchParams({
|
||||
client_id: GITHUB_OAUTH_CLIENT,
|
||||
client_secret: GITHUB_OAUTH_SECRET,
|
||||
@@ -47,19 +51,33 @@ export class Github {
|
||||
redirect_uri: `${API_URI}/oauth/github/callback`,
|
||||
});
|
||||
|
||||
const {access_token, token_type} = await fetch('https://github.com/login/oauth/access_token', {
|
||||
const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
headers: {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'},
|
||||
body: data,
|
||||
}).then(res => res.json());
|
||||
|
||||
if (!tokenResponse.access_token || !tokenResponse.token_type) {
|
||||
return res.redirect(DASHBOARD_URI + '/auth/login?message=Failed to authenticate with GitHub');
|
||||
}
|
||||
|
||||
const emails = await fetch(`https://api.github.com/user/emails`, {
|
||||
headers: {Authorization: `${token_type} ${access_token}`},
|
||||
headers: {Authorization: `${tokenResponse.token_type} ${tokenResponse.access_token}`},
|
||||
}).then(res => res.json());
|
||||
|
||||
const email = emails.find((e: {primary: boolean; email: string}) => e.primary).email;
|
||||
if (!Array.isArray(emails) || emails.length === 0) {
|
||||
return res.redirect(DASHBOARD_URI + '/auth/login?message=Failed to retrieve emails from GitHub');
|
||||
}
|
||||
|
||||
let user = await UserService.email(email as string);
|
||||
const primaryEmail = emails.find((e: {primary: boolean; email: string}) => e.primary);
|
||||
|
||||
if (!primaryEmail || !primaryEmail.email || typeof primaryEmail.email !== 'string') {
|
||||
return res.redirect(DASHBOARD_URI + '/auth/login?message=Failed to retrieve primary email from GitHub');
|
||||
}
|
||||
|
||||
const email = primaryEmail.email;
|
||||
|
||||
let user = await UserService.email(email);
|
||||
let isNewUser = false;
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -35,6 +35,10 @@ export class Google {
|
||||
}
|
||||
const {code} = req.query;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
return res.redirect(DASHBOARD_URI + '/auth/login?message=Invalid OAuth callback');
|
||||
}
|
||||
|
||||
const data = new URLSearchParams({
|
||||
client_id: GOOGLE_OAUTH_CLIENT,
|
||||
client_secret: GOOGLE_OAUTH_SECRET,
|
||||
@@ -43,15 +47,25 @@ export class Google {
|
||||
grant_type: 'authorization_code',
|
||||
});
|
||||
|
||||
const {access_token} = await fetch('https://oauth2.googleapis.com/token', {
|
||||
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: {'Content-type': 'application/x-www-form-urlencoded'},
|
||||
body: data,
|
||||
}).then(res => res.json());
|
||||
|
||||
const {email} = await fetch(`https://www.googleapis.com/oauth2/v3/userinfo?access_token=${access_token}`).then(
|
||||
res => res.json(),
|
||||
);
|
||||
if (!tokenResponse.access_token) {
|
||||
return res.redirect(DASHBOARD_URI + '/auth/login?message=Failed to authenticate with Google');
|
||||
}
|
||||
|
||||
const userInfoResponse = await fetch(
|
||||
`https://www.googleapis.com/oauth2/v3/userinfo?access_token=${tokenResponse.access_token}`,
|
||||
).then(res => res.json());
|
||||
|
||||
if (!userInfoResponse.email || typeof userInfoResponse.email !== 'string') {
|
||||
return res.redirect(DASHBOARD_URI + '/auth/login?message=Failed to retrieve email from Google');
|
||||
}
|
||||
|
||||
const email = userInfoResponse.email;
|
||||
|
||||
let user = await UserService.email(email);
|
||||
let isNewUser = false;
|
||||
|
||||
@@ -65,7 +65,7 @@ export const jwt = {
|
||||
* @param request The express request object
|
||||
*/
|
||||
export function parseJwt(request: Request): string {
|
||||
const token: string | undefined = request.cookies.token;
|
||||
const token: string | undefined = request.cookies.next_token;
|
||||
|
||||
if (!token) {
|
||||
throw new NotAuthenticated();
|
||||
|
||||
@@ -43,7 +43,7 @@ function getCookieDomain(): string | undefined {
|
||||
}
|
||||
|
||||
export class UserService {
|
||||
public static readonly COOKIE_NAME = 'token';
|
||||
public static readonly COOKIE_NAME = 'next_token';
|
||||
|
||||
public static async id(id: string) {
|
||||
return wrapRedis(Keys.User.id(id), async () => {
|
||||
@@ -52,6 +52,10 @@ export class UserService {
|
||||
}
|
||||
|
||||
public static async email(email: string) {
|
||||
if (!email) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return wrapRedis(Keys.User.email(email), async () => {
|
||||
return prisma.user.findFirst({
|
||||
where: {
|
||||
|
||||
@@ -5,15 +5,32 @@ description: Deploy with Docker Compose
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Download [docker-compose.yml](https://raw.githubusercontent.com/useplunk/plunk/next/docker-compose.yml)
|
||||
2. Configure environment variables
|
||||
3. Run `docker compose up -d`
|
||||
|
||||
## Minimal .env
|
||||
|
||||
```bash
|
||||
git clone https://github.com/useplunk/plunk.git
|
||||
cd plunk
|
||||
cp .env.self-host.example .env
|
||||
# Edit .env (see Environment Variables)
|
||||
docker compose up -d
|
||||
# Required
|
||||
JWT_SECRET="$(openssl rand -base64 32)"
|
||||
DB_PASSWORD="your-secure-password"
|
||||
|
||||
# Domains (point these to your server)
|
||||
API_DOMAIN="api.yourdomain.com"
|
||||
DASHBOARD_DOMAIN="app.yourdomain.com"
|
||||
LANDING_DOMAIN="www.yourdomain.com"
|
||||
WIKI_DOMAIN="docs.yourdomain.com"
|
||||
USE_HTTPS="true"
|
||||
|
||||
# AWS SES
|
||||
AWS_SES_REGION="us-east-1"
|
||||
AWS_SES_ACCESS_KEY_ID="your-key"
|
||||
AWS_SES_SECRET_ACCESS_KEY="your-secret"
|
||||
SES_CONFIGURATION_SET="plunk-tracking"
|
||||
```
|
||||
|
||||
See [Environment Variables](/self-hosting/environment-variables) for all configuration options.
|
||||
See [Environment Variables](/self-hosting/environment-variables) for all options.
|
||||
|
||||
## Services
|
||||
|
||||
@@ -33,7 +50,6 @@ See [Environment Variables](/self-hosting/environment-variables) for all configu
|
||||
| 465 | SMTP (implicit TLS) |
|
||||
| 587 | SMTP (STARTTLS) |
|
||||
| 9000 | Minio API |
|
||||
| 9001 | Minio Console |
|
||||
|
||||
## Running Individual Services
|
||||
|
||||
|
||||
@@ -33,28 +33,7 @@ description: Configure email delivery
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Request Production Access
|
||||
|
||||
SES starts in sandbox mode (verified addresses only).
|
||||
|
||||
1. Go to SES Console
|
||||
2. Click "Request production access"
|
||||
3. Wait for approval (24-48 hours)
|
||||
|
||||
## 3. Verify Domain
|
||||
|
||||
1. SES Console → Verified Identities → Create identity
|
||||
2. Choose "Domain" → Enter your domain
|
||||
3. Add DNS records provided by AWS
|
||||
4. Wait for verification
|
||||
|
||||
## 4. Enable DKIM (Recommended)
|
||||
|
||||
1. SES Console → Verified Identities → Your domain
|
||||
2. Enable "Easy DKIM"
|
||||
3. Add the 3 CNAME records to your DNS
|
||||
|
||||
## 5. Create SNS Topic
|
||||
## 2. Create SNS Topic
|
||||
|
||||
1. Go to SNS Console → Topics → Create topic
|
||||
2. Type: Standard
|
||||
@@ -65,7 +44,7 @@ SES starts in sandbox mode (verified addresses only).
|
||||
- Endpoint: `https://api.yourdomain.com/webhooks/sns`
|
||||
6. Plunk automatically confirms the subscription. If it fails, check your logs for the confirmation URL.
|
||||
|
||||
## 6. Create Configuration Sets
|
||||
## 3. Create Configuration Sets
|
||||
|
||||
### Tracking Configuration Set
|
||||
|
||||
@@ -81,7 +60,7 @@ SES starts in sandbox mode (verified addresses only).
|
||||
1. Create another set named `plunk-no-tracking`
|
||||
2. Add event destination with only: **Sends, Deliveries, Bounces, Complaints**
|
||||
|
||||
## 7. Configure Environment
|
||||
## 4. Configure Environment
|
||||
|
||||
```bash
|
||||
AWS_SES_REGION="us-east-1"
|
||||
@@ -90,3 +69,8 @@ AWS_SES_SECRET_ACCESS_KEY="your-secret-key"
|
||||
SES_CONFIGURATION_SET="plunk-tracking"
|
||||
SES_CONFIGURATION_SET_NO_TRACKING="plunk-no-tracking"
|
||||
```
|
||||
|
||||
## 5. Add Your Domain in Plunk
|
||||
|
||||
Once you have configured AWS SES with the above settings, you can add and verify your domain directly through the Plunk dashboard. Plunk will handle the domain verification and DKIM setup with AWS SES automatically and show you the right records to add to your DNS.
|
||||
|
||||
|
||||
@@ -7,26 +7,25 @@ description: Deploy Plunk on your own infrastructure
|
||||
|
||||
- Docker and Docker Compose
|
||||
- AWS SES account (for sending emails)
|
||||
- Domain name (for production)
|
||||
- Domain name with subdomains for each service
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/useplunk/plunk.git
|
||||
cd plunk
|
||||
cp .env.self-host.example .env
|
||||
# Edit .env with your settings
|
||||
docker compose up -d
|
||||
```
|
||||
1. Download the [docker-compose.yml](https://raw.githubusercontent.com/useplunk/plunk/next/docker-compose.yml)
|
||||
2. Create environment variables with your configuration (see [Environment Variables](/self-hosting/environment-variables))
|
||||
3. Run `docker compose up -d`
|
||||
|
||||
## Access
|
||||
## Domain Setup
|
||||
|
||||
| Service | URL |
|
||||
|---------|-----|
|
||||
| Dashboard | http://app.localhost |
|
||||
| API | http://api.localhost |
|
||||
| Docs | http://docs.localhost |
|
||||
| Minio Console | http://localhost:9001 |
|
||||
Configure these subdomains pointing to your server:
|
||||
|
||||
| Subdomain | Service |
|
||||
|-----------|---------|
|
||||
| `app.yourdomain.com` | Dashboard |
|
||||
| `api.yourdomain.com` | API |
|
||||
| `docs.yourdomain.com` | Documentation |
|
||||
| `www.yourdomain.com` | Landing page |
|
||||
| `smtp.yourdomain.com` | SMTP relay (optional) |
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plunk",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "turbo build",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import Image from 'next/image';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useRouter} from 'next/router';
|
||||
|
||||
export interface LoaderProps {
|
||||
message?: string;
|
||||
@@ -9,6 +11,28 @@ export interface LoaderProps {
|
||||
* Full-screen loader component with animated logo and spinner
|
||||
*/
|
||||
export function Loader({message = 'Loading...', showLogo = true}: LoaderProps) {
|
||||
const [showClearCache, setShowClearCache] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setShowClearCache(true);
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const handleClearCache = () => {
|
||||
document.cookie.split(';').forEach(c => {
|
||||
document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
|
||||
});
|
||||
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
void router.push('/auth/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-white">
|
||||
<div className="text-center animate-in fade-in duration-500">
|
||||
@@ -48,6 +72,19 @@ export function Loader({message = 'Loading...', showLogo = true}: LoaderProps) {
|
||||
|
||||
{/* Loading message */}
|
||||
{message && <p className="text-sm font-medium text-neutral-700 animate-in fade-in duration-1000">{message}</p>}
|
||||
|
||||
{/* Clear cache button - appears after 5 seconds */}
|
||||
{showClearCache && (
|
||||
<div className="mt-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
||||
<p className="text-xs text-neutral-500 mb-3">Taking too long?</p>
|
||||
<button
|
||||
onClick={handleClearCache}
|
||||
className="px-4 py-2 text-sm font-medium text-neutral-700 bg-neutral-100 hover:bg-neutral-200 rounded-lg transition-colors duration-200"
|
||||
>
|
||||
Clear cache and reload
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user