Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Configuration reference: http://docs.postiz.com/configuration/reference

# === Required Settings
# === Required Settings
DATABASE_URL="postgresql://postiz-user:postiz-password@localhost:5432/postiz-db-local"
REDIS_URL="redis://localhost:6379"
JWT_SECRET="random string for your JWT secret, make it long"
Expand All @@ -20,7 +20,6 @@ CLOUDFLARE_BUCKETNAME="your-bucket-name"
CLOUDFLARE_BUCKET_URL="https://your-bucket-url.r2.cloudflarestorage.com/"
CLOUDFLARE_REGION="auto"


# === Common optional Settings

## This is a dummy key, you must create your own from Resend.
Expand All @@ -32,15 +31,14 @@ CLOUDFLARE_REGION="auto"
#DISABLE_REGISTRATION=false

# Where will social media icons be saved - local or cloudflare.
STORAGE_PROVIDER="local"
STORAGE_PROVIDER="local"

# Your upload directory path if you host your files locally, otherwise Cloudflare will be used.
#UPLOAD_DIRECTORY=""

# Your upload directory path if you host your files locally, otherwise Cloudflare will be used.
#NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=""


# Social Media API Settings
X_API_KEY=""
X_API_SECRET=""
Expand Down Expand Up @@ -92,3 +90,13 @@ STRIPE_SIGNING_KEY_CONNECT=""
# Developer Settings
NX_ADD_PLUGINS=false
IS_GENERAL="true" # required for now
NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME="Authentik"
NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL="https://raw.githubusercontent.com/walkxcode/dashboard-icons/master/png/authentik.png"
POSTIZ_GENERIC_OAUTH="false"
POSTIZ_OAUTH_URL="https://auth.example.com"
POSTIZ_OAUTH_AUTH_URL="https://auth.example.com/application/o/authorize"
POSTIZ_OAUTH_TOKEN_URL="https://auth.example.com/application/o/token"
POSTIZ_OAUTH_USERINFO_URL="https://authentik.example.com/application/o/userinfo"
POSTIZ_OAUTH_CLIENT_ID=""
POSTIZ_OAUTH_CLIENT_SECRET=""
# POSTIZ_OAUTH_SCOPE="openid profile email" # default values
103 changes: 103 additions & 0 deletions apps/backend/src/services/auth/providers/oauth.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';

export class OauthProvider implements ProvidersInterface {
private readonly authUrl: string;
private readonly baseUrl: string;
private readonly clientId: string;
private readonly clientSecret: string;
private readonly frontendUrl: string;
private readonly tokenUrl: string;
private readonly userInfoUrl: string;

constructor() {
const {
POSTIZ_OAUTH_AUTH_URL,
POSTIZ_OAUTH_CLIENT_ID,
POSTIZ_OAUTH_CLIENT_SECRET,
POSTIZ_OAUTH_TOKEN_URL,
POSTIZ_OAUTH_URL,
POSTIZ_OAUTH_USERINFO_URL,
FRONTEND_URL,
} = process.env;

if (!POSTIZ_OAUTH_USERINFO_URL)
throw new Error(
'POSTIZ_OAUTH_USERINFO_URL environment variable is not set'
);
if (!POSTIZ_OAUTH_URL)
throw new Error('POSTIZ_OAUTH_URL environment variable is not set');
if (!POSTIZ_OAUTH_TOKEN_URL)
throw new Error('POSTIZ_OAUTH_TOKEN_URL environment variable is not set');
if (!POSTIZ_OAUTH_CLIENT_ID)
throw new Error('POSTIZ_OAUTH_CLIENT_ID environment variable is not set');
if (!POSTIZ_OAUTH_CLIENT_SECRET)
throw new Error(
'POSTIZ_OAUTH_CLIENT_SECRET environment variable is not set'
);
if (!POSTIZ_OAUTH_AUTH_URL)
throw new Error('POSTIZ_OAUTH_AUTH_URL environment variable is not set');
if (!FRONTEND_URL)
throw new Error('FRONTEND_URL environment variable is not set');

this.authUrl = POSTIZ_OAUTH_AUTH_URL;
this.baseUrl = POSTIZ_OAUTH_URL;
this.clientId = POSTIZ_OAUTH_CLIENT_ID;
this.clientSecret = POSTIZ_OAUTH_CLIENT_SECRET;
this.frontendUrl = FRONTEND_URL;
this.tokenUrl = POSTIZ_OAUTH_TOKEN_URL;
this.userInfoUrl = POSTIZ_OAUTH_USERINFO_URL;
}

generateLink(): string {
const params = new URLSearchParams({
client_id: this.clientId,
scope: 'openid profile email',
response_type: 'code',
redirect_uri: `${this.frontendUrl}/settings`,
});

return `${this.authUrl}/?${params.toString()}`;
}

async getToken(code: string): Promise<string> {
const response = await fetch(`${this.tokenUrl}/`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.clientId,
client_secret: this.clientSecret,
code,
redirect_uri: `${this.frontendUrl}/settings`,
}),
});

if (!response.ok) {
const error = await response.text();
throw new Error(`Token request failed: ${error}`);
}

const { access_token } = await response.json();
return access_token;
}

async getUser(access_token: string): Promise<{ email: string; id: string }> {
const response = await fetch(`${this.userInfoUrl}/`, {
headers: {
Authorization: `Bearer ${access_token}`,
Accept: 'application/json',
},
});

if (!response.ok) {
const error = await response.text();
throw new Error(`User info request failed: ${error}`);
}

const { email, sub: id } = await response.json();
return { email, id };
}
}
3 changes: 3 additions & 0 deletions apps/backend/src/services/auth/providers/providers.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.int
import { GoogleProvider } from '@gitroom/backend/services/auth/providers/google.provider';
import { FarcasterProvider } from '@gitroom/backend/services/auth/providers/farcaster.provider';
import { WalletProvider } from '@gitroom/backend/services/auth/providers/wallet.provider';
import { OauthProvider } from '@gitroom/backend/services/auth/providers/oauth.provider';

export class ProvidersFactory {
static loadProvider(provider: Provider): ProvidersInterface {
Expand All @@ -16,6 +17,8 @@ export class ProvidersFactory {
return new FarcasterProvider();
case Provider.WALLET:
return new WalletProvider();
case Provider.GENERIC:
return new OauthProvider();
Comment on lines +20 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Added support for the GENERIC provider type.

The case for Provider.GENERIC correctly instantiates the OauthProvider class when requested, following the factory pattern consistently with other providers. Based on the relevant code snippet, make sure all required environment variables (POSTIZ_OAUTH_*) are documented in the deployment guide.


🏁 Script executed:

#!/bin/bash
# Verify if the environment variables are documented in the example file
echo "Checking for OAuth environment variables in .env.example"
fd -t f ".env.example" | xargs cat | grep -E "POSTIZ_OAUTH_"

Length of output: 181


🏁 Script executed:

#!/bin/bash
# Search for POSTIZ_OAUTH_ references across the entire repository
echo "🔍 Searching for POSTIZ_OAUTH_ across all files"
rg -n "POSTIZ_OAUTH_" .

# List all .env* files to ensure we catch variants like .env.sample, .env.development, etc.
echo "📂 Listing all .env* files"
fd -t f "^\.env" || echo "No .env* files found"

# Check each .env* file for POSTIZ_OAUTH_ entries
echo "🔎 Inspecting .env* files for POSTIZ_OAUTH_"
fd -t f "^\.env" | while read -r file; do
  echo "➡️  File: $file"
  grep -H "POSTIZ_OAUTH_" "$file" || echo "   └─ No matches in $file"
done

# Search documentation directories (e.g., docs/, .github/) for environment variable mentions
echo "📖 Searching in docs/ and .github/"
rg -n "POSTIZ_OAUTH_" docs .github || echo "No documentation entries found for POSTIZ_OAUTH_"

Length of output: 3939


Please document the new POSTIZ_OAUTH_ variables in your environment examples and deployment guide*

The OauthProvider now relies on six POSTIZ_OAUTH_* variables, but I couldn’t find any .env-style example or docs entries listing them. Before merging, please:

• Add the following keys to your .env.example (or equivalent) with placeholder values:

  • POSTIZ_OAUTH_AUTH_URL
  • POSTIZ_OAUTH_URL
  • POSTIZ_OAUTH_TOKEN_URL
  • POSTIZ_OAUTH_CLIENT_ID
  • POSTIZ_OAUTH_CLIENT_SECRET
  • POSTIZ_OAUTH_USERINFO_URL

• Update your deployment or onboarding documentation (e.g., in your docs/ directory or README) to explain each variable’s purpose and expected format.

That will ensure users know how to configure the new GENERIC provider correctly.

}
}
}
9 changes: 9 additions & 0 deletions apps/frontend/public/icons/generic-oauth.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
discordUrl={process.env.NEXT_PUBLIC_DISCORD_SUPPORT!}
frontEndUrl={process.env.FRONTEND_URL!}
isGeneral={!!process.env.IS_GENERAL}
genericOauth={!!process.env.POSTIZ_GENERIC_OAUTH}
oauthLogoUrl={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL!}
oauthDisplayName={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME!}
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
tolt={process.env.NEXT_PUBLIC_TOLT!}
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}
Expand Down
9 changes: 6 additions & 3 deletions apps/frontend/src/components/auth/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useMemo, useState } from 'react';
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto';
import { GithubProvider } from '@gitroom/frontend/components/auth/providers/github.provider';
import { OauthProvider } from '@gitroom/frontend/components/auth/providers/oauth.provider';
import interClass from '@gitroom/react/helpers/inter.font';
import { GoogleProvider } from '@gitroom/frontend/components/auth/providers/google.provider';
import { useVariables } from '@gitroom/react/helpers/variable.context';
Expand All @@ -24,7 +25,8 @@ type Inputs = {

export function Login() {
const [loading, setLoading] = useState(false);
const { isGeneral, neynarClientId, billingEnabled } = useVariables();
const { isGeneral, neynarClientId, billingEnabled, genericOauth } =
useVariables();
const resolver = useMemo(() => {
return classValidatorResolver(LoginUserDto);
}, []);
Expand Down Expand Up @@ -63,8 +65,9 @@ export function Login() {
Sign In
</h1>
</div>

{!isGeneral ? (
{isGeneral && genericOauth ? (
<OauthProvider />
) : !isGeneral ? (
<GithubProvider />
) : (
<div className="gap-[5px] flex flex-col">
Expand Down
42 changes: 42 additions & 0 deletions apps/frontend/src/components/auth/providers/oauth.provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useCallback } from 'react';
import Image from 'next/image';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import interClass from '@gitroom/react/helpers/inter.font';
import { useVariables } from '@gitroom/react/helpers/variable.context';

export const OauthProvider = () => {
const fetch = useFetch();
const { oauthLogoUrl, oauthDisplayName } = useVariables();

const gotoLogin = useCallback(async () => {
try {
const response = await fetch('/auth/oauth/GENERIC');
if (!response.ok) {
throw new Error(
`Login link request failed with status ${response.status}`
);
}
const link = await response.text();
window.location.href = link;
} catch (error) {
console.error('Failed to get generic oauth login link:', error);
}
}, []);
Comment on lines +11 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add user-facing error handling

The error handling currently only logs to the console, but there's no feedback mechanism for the user if the OAuth login link request fails.

Consider implementing a state variable to track errors and display a user-friendly message:

import { useCallback } from 'react';
+import { useState } from 'react';
import Image from 'next/image';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import interClass from '@gitroom/react/helpers/inter.font';
import { useVariables } from '@gitroom/react/helpers/variable.context';

export const OauthProvider = () => {
  const fetch = useFetch();
  const { oauthLogoUrl, oauthDisplayName } = useVariables();
+  const [error, setError] = useState<string | null>(null);

  const gotoLogin = useCallback(async () => {
    try {
+      setError(null);
      const response = await fetch('/auth/oauth/GENERIC');
      if (!response.ok) {
        throw new Error(
          `Login link request failed with status ${response.status}`
        );
      }
      const link = await response.text();
      window.location.href = link;
    } catch (error) {
      console.error('Failed to get generic oauth login link:', error);
+      setError('Failed to initiate login. Please try again later.');
    }
  }, []);

Then display the error message in the UI:

  return (
    <div
      onClick={gotoLogin}
      className={`cursor-pointer bg-white h-[44px] rounded-[4px] flex justify-center items-center text-customColor16 ${interClass} gap-[4px]`}
    >
      <div>
        <Image
          src={oauthLogoUrl || '/icons/generic-oauth.svg'}
          alt="genericOauth"
          width={40}
          height={40}
        />
      </div>
      <div>Sign in with {oauthDisplayName || 'OAuth'}</div>
+      {error && <div className="text-red-500 text-sm mt-2">{error}</div>}
    </div>
  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const gotoLogin = useCallback(async () => {
try {
const response = await fetch('/auth/oauth/GENERIC');
if (!response.ok) {
throw new Error(
`Login link request failed with status ${response.status}`
);
}
const link = await response.text();
window.location.href = link;
} catch (error) {
console.error('Failed to get generic oauth login link:', error);
}
}, []);
import { useCallback } from 'react';
import { useState } from 'react';
import Image from 'next/image';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import interClass from '@gitroom/react/helpers/inter.font';
import { useVariables } from '@gitroom/react/helpers/variable.context';
export const OauthProvider = () => {
const fetch = useFetch();
const { oauthLogoUrl, oauthDisplayName } = useVariables();
const [error, setError] = useState<string | null>(null);
const gotoLogin = useCallback(async () => {
try {
setError(null);
const response = await fetch('/auth/oauth/GENERIC');
if (!response.ok) {
throw new Error(
`Login link request failed with status ${response.status}`
);
}
const link = await response.text();
window.location.href = link;
} catch (error) {
console.error('Failed to get generic oauth login link:', error);
setError('Failed to initiate login. Please try again later.');
}
}, []);
return (
<div
onClick={gotoLogin}
className={`cursor-pointer bg-white h-[44px] rounded-[4px] flex justify-center items-center text-customColor16 ${interClass} gap-[4px]`}
>
<div>
<Image
src={oauthLogoUrl || '/icons/generic-oauth.svg'}
alt="genericOauth"
width={40}
height={40}
/>
</div>
<div>Sign in with {oauthDisplayName || 'OAuth'}</div>
{error && <div className="text-red-500 text-sm mt-2">{error}</div>}
</div>
);
};


return (
<div
onClick={gotoLogin}
className={`cursor-pointer bg-white h-[44px] rounded-[4px] flex justify-center items-center text-customColor16 ${interClass} gap-[4px]`}
>
<div>
<Image
src={oauthLogoUrl || '/icons/generic-oauth.svg'}
alt="genericOauth"
width={40}
height={40}
/>
</div>
<div>Sign in with {oauthDisplayName || 'OAuth'}</div>
</div>
);
};
4 changes: 3 additions & 1 deletion apps/frontend/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export async function middleware(request: NextRequest) {
? ''
: (url.indexOf('?') > -1 ? '&' : '?') +
`provider=${(findIndex === 'settings'
? 'github'
? process.env.POSTIZ_GENERIC_OAUTH
? 'generic'
: 'github'
Comment on lines +47 to +49
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance type safety for environment variable check

The condition checks process.env.POSTIZ_GENERIC_OAUTH directly without type conversion, which may lead to unexpected behavior since environment variables are strings. A more robust approach would explicitly convert to boolean.

-          ? process.env.POSTIZ_GENERIC_OAUTH
+          ? process.env.POSTIZ_GENERIC_OAUTH === 'true'
            ? 'generic'
            : 'github'

Additionally, consider renaming 'generic' to 'oauth' or 'authentik' to better reflect the PR's purpose of adding Authentik SSO support.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
? process.env.POSTIZ_GENERIC_OAUTH
? 'generic'
: 'github'
? process.env.POSTIZ_GENERIC_OAUTH === 'true'
? 'generic'
: 'github'

: findIndex
).toUpperCase()}`;
return NextResponse.redirect(
Expand Down
3 changes: 2 additions & 1 deletion libraries/nestjs-libraries/src/database/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@ enum Provider {
GOOGLE
FARCASTER
WALLET
GENERIC
}

enum Role {
Expand All @@ -648,4 +649,4 @@ enum APPROVED_SUBMIT_FOR_ORDER {
NO
WAITING_CONFIRMATION
YES
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { createContext, FC, ReactNode, useContext, useEffect } from 'react';
interface VariableContextInterface {
billingEnabled: boolean;
isGeneral: boolean;
genericOauth: boolean;
oauthLogoUrl: string;
oauthDisplayName: string;
frontEndUrl: string;
plontoKey: string;
storageProvider: 'local' | 'cloudflare',
storageProvider: 'local' | 'cloudflare';
backendUrl: string;
discordUrl: string;
uploadDirectory: string;
Expand All @@ -20,6 +23,9 @@ interface VariableContextInterface {
const VariableContext = createContext({
billingEnabled: false,
isGeneral: true,
genericOauth: false,
oauthLogoUrl: '',
oauthDisplayName: '',
frontEndUrl: '',
storageProvider: 'local',
plontoKey: '',
Expand Down Expand Up @@ -52,9 +58,9 @@ export const VariableContextComponent: FC<

export const useVariables = () => {
return useContext(VariableContext);
}
};

export const loadVars = () => {
// @ts-ignore
return window.vars as VariableContextInterface;
}
};