mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-09-01 12:26:08 +08:00
Merge pull request #94 from parcelvoy/feat/basic-auth-option
Adds basic auth option
This commit is contained in:
commit
b47960ae76
18 changed files with 242 additions and 15 deletions
21
.env.example
Normal file
21
.env.example
Normal file
|
@ -0,0 +1,21 @@
|
|||
APP_SECRET=Ck7Rt1uiy5WGbrTFj1HBjymbBoA6zqih
|
||||
BASE_URL=http://localhost
|
||||
UI_PORT=3000
|
||||
|
||||
DB_CLIENT=mysql2
|
||||
DB_HOST=127.0.0.1
|
||||
DB_USERNAME=parcelvoy
|
||||
DB_PASSWORD=parcelvoypassword
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=parcelvoy
|
||||
|
||||
QUEUE_DRIVER=redis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
|
||||
STORAGE_DRIVER=local
|
||||
STORAGE_BASE_URL=http://localhost/uploads
|
||||
|
||||
AUTH_DRIVER=basic
|
||||
AUTH_BASIC_USERNAME=parcelvoy
|
||||
AUTH_BASIC_PASSWORD=password
|
BIN
.github/assets/logo-dark.png
vendored
Normal file
BIN
.github/assets/logo-dark.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
BIN
.github/assets/logo-light.png
vendored
Normal file
BIN
.github/assets/logo-light.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -72,6 +72,7 @@ typings/
|
|||
# dotenv environment variables file
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
|
40
README.md
40
README.md
|
@ -1,5 +1,18 @@
|
|||
# Parcelvoy
|
||||
Engage your customers through effortless communication.
|
||||
<br />
|
||||
<div align="center">
|
||||
<a href="https://parcelvoy.com" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/logo-dark.png#gh-dark-mode-only">
|
||||
<img src=".github/assets/logo-light.png#gh-light-mode-only" width="360" alt="Logo"/>
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h1 align="center">Open Source Multi-Channel Marketing</h1>
|
||||
|
||||
<p align="center">Engage your customers through effortless communication.</p>
|
||||
|
||||
<br />
|
||||
|
||||
## Features
|
||||
- 💬 **Cross Channel Messaging** Send data-driven emails, push notifications and text messages.
|
||||
|
@ -12,4 +25,25 @@ Engage your customers through effortless communication.
|
|||
|
||||
## 🚀 Getting Started
|
||||
|
||||
Documentation coming soon!
|
||||
You can run Parcelvoy locally or in the cloud easily using Docker.
|
||||
|
||||
To get up and running quickly to try things out, copy our latest `docker-compose.yml` and `.env` file onto your machine and go!
|
||||
```
|
||||
mkdir parcelvoy && cd parcelvoy
|
||||
wget https://raw.githubusercontent.com/parcelvoy/parcelvoy/master/{.env.example,docker-compose.yml}
|
||||
mv .env.example .env
|
||||
docker compose up -d # run the Docker container
|
||||
```
|
||||
|
||||
Login to the web app at http://localhost:3000 by entering the default credentials found in the copied `.env` file.
|
||||
|
||||
```
|
||||
AUTH_BASIC_USERNAME=parcelvoy
|
||||
AUTH_BASIC_PASSWORD=password
|
||||
```
|
||||
|
||||
**Note:** We would recommend changing these default credentials as well as your `APP_SECRET` before ever using Parcelvoy in production.
|
||||
|
||||
For full documentation on the platform and more information on deployment, check out our docs.
|
||||
|
||||
**[Explore the Docs »](https://docs.parcelvoy.com)**
|
||||
|
|
|
@ -5,10 +5,11 @@ import SAMLProvider, { SAMLConfig } from './SAMLAuthProvider'
|
|||
import { DriverConfig } from '../config/env'
|
||||
import LoggerAuthProvider from './LoggerAuthProvider'
|
||||
import { logger } from '../config/logger'
|
||||
import BasicAuthProvider, { BasicAuthConfig } from './BasicAuthProvider'
|
||||
|
||||
export type AuthProviderName = 'saml' | 'openid' | 'logger'
|
||||
export type AuthProviderName = 'basic' | 'saml' | 'openid' | 'logger'
|
||||
|
||||
export type AuthConfig = SAMLConfig | OpenIDConfig
|
||||
export type AuthConfig = BasicAuthConfig | SAMLConfig | OpenIDConfig
|
||||
|
||||
export interface AuthTypeConfig extends DriverConfig {
|
||||
tokenLife: number
|
||||
|
@ -19,7 +20,9 @@ export default class Auth {
|
|||
provider: AuthProvider
|
||||
|
||||
constructor(config?: AuthConfig) {
|
||||
if (config?.driver === 'saml') {
|
||||
if (config?.driver === 'basic') {
|
||||
this.provider = new BasicAuthProvider(config)
|
||||
} else if (config?.driver === 'saml') {
|
||||
this.provider = new SAMLProvider(config)
|
||||
} else if (config?.driver === 'openid') {
|
||||
this.provider = new OpenIDProvider(config)
|
||||
|
|
|
@ -29,7 +29,7 @@ router.post('/logout', async ctx => {
|
|||
if (oauth) {
|
||||
await revokeAccessToken(oauth.access_token, ctx)
|
||||
}
|
||||
ctx.body = {} // logout redirect env property?
|
||||
ctx.redirect('/')
|
||||
})
|
||||
|
||||
router.get('/logout', async ctx => {
|
||||
|
@ -37,7 +37,7 @@ router.get('/logout', async ctx => {
|
|||
if (oauth) {
|
||||
await revokeAccessToken(oauth.access_token, ctx)
|
||||
}
|
||||
ctx.body = '<html><body>bye!</body></html>' // TODO redirect?
|
||||
ctx.redirect('/')
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
|
@ -35,4 +35,9 @@ export default {
|
|||
code: 1007,
|
||||
statusCode: 403,
|
||||
},
|
||||
InvalidCredentials: {
|
||||
message: 'The email and password combination provided is invalid.',
|
||||
code: 1008,
|
||||
statusCode: 400,
|
||||
},
|
||||
} as Record<string, ErrorType>
|
||||
|
|
53
apps/platform/src/auth/BasicAuthProvider.ts
Normal file
53
apps/platform/src/auth/BasicAuthProvider.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { Context } from 'koa'
|
||||
import { AuthTypeConfig } from './Auth'
|
||||
import { getAdminByEmail } from './AdminRepository'
|
||||
import AuthProvider from './AuthProvider'
|
||||
import App from '../app'
|
||||
import { combineURLs, firstQueryParam } from '../utilities'
|
||||
import { Admin } from './Admin'
|
||||
import { RequestError } from '../core/errors'
|
||||
import AuthError from './AuthError'
|
||||
|
||||
export interface BasicAuthConfig extends AuthTypeConfig {
|
||||
driver: 'basic'
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export default class BasicAuthProvider extends AuthProvider {
|
||||
|
||||
private config: BasicAuthConfig
|
||||
constructor(config: BasicAuthConfig) {
|
||||
super()
|
||||
this.config = config
|
||||
}
|
||||
|
||||
async start(ctx: Context) {
|
||||
|
||||
const redirect = firstQueryParam(ctx.request.query.r)
|
||||
|
||||
// Redirect to the login form
|
||||
ctx.redirect(combineURLs([App.main.env.baseUrl, '/login/basic']) + '?r=' + redirect)
|
||||
}
|
||||
|
||||
async validate(ctx: Context) {
|
||||
|
||||
console.log('basic auth!', ctx.request.body)
|
||||
const { email, password } = ctx.request.body
|
||||
if (!email || !password) throw new RequestError(AuthError.InvalidCredentials)
|
||||
|
||||
// Check email and password match
|
||||
if (email !== this.config.email || password !== this.config.password) {
|
||||
throw new RequestError(AuthError.InvalidCredentials)
|
||||
}
|
||||
|
||||
// Find admin, otherwise first time, create
|
||||
let admin = await getAdminByEmail(email)
|
||||
if (!admin) {
|
||||
admin = await Admin.insertAndFetch({ email, first_name: 'Admin' })
|
||||
}
|
||||
|
||||
// Process the login
|
||||
await this.login(admin.id, ctx)
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@ export default class LoggerAuthProvider extends AuthProvider {
|
|||
}
|
||||
|
||||
callbackUrl(token: string): string {
|
||||
const baseUrl = combineURLs([App.main.env.baseUrl, 'auth/login'])
|
||||
const baseUrl = combineURLs([App.main.env.baseUrl, 'api/auth/login'])
|
||||
const url = new URL(baseUrl)
|
||||
url.searchParams.set('token', token)
|
||||
return url.href
|
||||
|
|
|
@ -79,6 +79,11 @@ export default (type?: EnvType): Env => {
|
|||
port: parseInt(process.env.PORT!),
|
||||
secret: process.env.APP_SECRET!,
|
||||
auth: driver<AuthConfig>(process.env.AUTH_DRIVER, {
|
||||
basic: () => ({
|
||||
tokenLife: defaultTokenLife,
|
||||
email: process.env.AUTH_BASIC_EMAIL!,
|
||||
password: process.env.AUTH_BASIC_PASSWORD!,
|
||||
}),
|
||||
saml: () => ({
|
||||
tokenLife: defaultTokenLife,
|
||||
callbackUrl: process.env.AUTH_SAML_CALLBACK_URL,
|
||||
|
|
|
@ -122,6 +122,11 @@ const api = {
|
|||
window.location.href = env.api.baseURL + '/auth/logout'
|
||||
},
|
||||
|
||||
async basicAuth(email: string, password: string, redirect: string = '/') {
|
||||
await client.post('/auth/login/callback', { email, password })
|
||||
window.location.href = redirect
|
||||
},
|
||||
|
||||
profile: {
|
||||
get: async () => {
|
||||
if (!cache.profile) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { FieldProps } from './Field'
|
|||
import './TextField.css'
|
||||
|
||||
export interface TextFieldProps<X extends FieldValues, P extends FieldPath<X>> extends FieldProps<X, P> {
|
||||
type?: 'text' | 'time' | 'date' | 'datetime-local' | 'number'
|
||||
type?: 'text' | 'time' | 'date' | 'datetime-local' | 'number' | 'password'
|
||||
textarea?: boolean
|
||||
size?: 'small' | 'regular'
|
||||
value?: string | number | readonly string[] | undefined
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
}
|
||||
|
||||
.auth-step {
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-grey-soft);
|
||||
|
@ -24,3 +25,7 @@
|
|||
.auth-step h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-step .form {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
|
49
apps/ui/src/views/auth/LoginBasic.tsx
Normal file
49
apps/ui/src/views/auth/LoginBasic.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import api from '../../api'
|
||||
import { ReactComponent as Logo } from '../../assets/logo.svg'
|
||||
import Alert from '../../ui/Alert'
|
||||
import FormWrapper from '../../ui/form/FormWrapper'
|
||||
import TextField from '../../ui/form/TextField'
|
||||
import './Auth.css'
|
||||
|
||||
interface LoginBasicParams {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
|
||||
const handleLogin = async ({ email, password }: LoginBasicParams) => {
|
||||
try {
|
||||
await api.basicAuth(email, password, searchParams.get('r') ?? '/')
|
||||
} catch (error: any) {
|
||||
if (error?.response?.data) {
|
||||
setError(error.response.data.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="auth login">
|
||||
<div className="logo">
|
||||
<Logo />
|
||||
</div>
|
||||
<div className="auth-step">
|
||||
<h1>Login</h1>
|
||||
<FormWrapper<LoginBasicParams>
|
||||
onSubmit={handleLogin}>
|
||||
{form => <>
|
||||
{error && (
|
||||
<Alert variant="error" title="Error">{error}</Alert>
|
||||
)}
|
||||
<TextField form={form} name="email" />
|
||||
<TextField form={form} name="password" type="password" />
|
||||
</>}
|
||||
</FormWrapper>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -39,6 +39,7 @@ import { CampaignsIcon, JourneysIcon, ListsIcon, SettingsIcon, UsersIcon } from
|
|||
import { Projects } from './project/Projects'
|
||||
import { pushRecentProject } from '../utils'
|
||||
import { ProjectRoleRequired } from './project/ProjectRoleRequired'
|
||||
import LoginBasic from './auth/LoginBasic'
|
||||
|
||||
export const useRoute = (includeProject = true) => {
|
||||
const { projectId = '' } = useParams()
|
||||
|
@ -58,6 +59,10 @@ export const router = createBrowserRouter([
|
|||
path: '/login',
|
||||
element: <Login />,
|
||||
},
|
||||
{
|
||||
path: '/login/basic',
|
||||
element: <LoginBasic />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
errorElement: <ErrorPage />,
|
||||
|
|
|
@ -42,7 +42,7 @@ services:
|
|||
DB_HOST: mysql
|
||||
DB_USERNAME: ${DB_USERNAME}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_PORT: 3306
|
||||
DB_PORT: ${DB_PORT}
|
||||
DB_DATABASE: ${DB_DATABASE}
|
||||
STORAGE_DRIVER: ${STORAGE_DRIVER}
|
||||
STORAGE_BASE_URL: ${STORAGE_BASE_URL}
|
||||
|
@ -55,6 +55,8 @@ services:
|
|||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
||||
AWS_REGION: ${AWS_REGION}
|
||||
AUTH_DRIVER: ${AUTH_DRIVER}
|
||||
AUTH_BASIC_EMAIL: ${AUTH_BASIC_EMAIL}
|
||||
AUTH_BASIC_PASSWORD: ${AUTH_BASIC_PASSWORD}
|
||||
AUTH_SAML_CALLBACK_URL: ${AUTH_SAML_CALLBACK_URL}
|
||||
AUTH_SAML_ENTRY_POINT_URL: ${AUTH_SAML_ENTRY_POINT_URL}
|
||||
AUTH_SAML_ISSUER: ${AUTH_SAML_ISSUER}
|
||||
|
@ -75,7 +77,7 @@ services:
|
|||
environment:
|
||||
API_BASE_URL: ${API_BASE_URL}
|
||||
ports:
|
||||
- 80:3000
|
||||
- ${UI_PORT}:3000
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
|
|
|
@ -1,9 +1,48 @@
|
|||
# Authentication
|
||||
Parcelvoy utilizes SSO for authentication supporting both SAML and OpenID. There is no local authentication method available at this time.
|
||||
Parcelvoy comes with a few different types of authentication out of the box:
|
||||
- Basic
|
||||
- SAML
|
||||
- OpenID
|
||||
|
||||
## Setting up SAML
|
||||
Whereas a lot of platforms will gate SSO as a luxury feature and charge extra for it (this is known as the ***SSO Tax***) we opted to go the opposite direction and lean in completely to SSO to make sure you understand that Parcelvoy takes your security seriously. SSO is not something that only Enterprise companies should have, but should be available at every level.
|
||||
|
||||
## Setting up OpenID
|
||||
## Basic
|
||||
Right out of the gate Parcelvoy is setup to use a simple ***Basic*** auth that allows for a single user that can be set inside of the environment variables. This is a limited form of auth as it does not allow for multiple users and is largely meant for evaluation purposes.
|
||||
|
||||
To change the credentials for basic auth, modify the following environment variables and restart your Parcelvoy instance.
|
||||
```
|
||||
AUTH_BASIC_USERNAME=parcelvoy
|
||||
AUTH_BASIC_PASSWORD=password
|
||||
```
|
||||
|
||||
|
||||
## SAML
|
||||
***Instructions coming soon.***
|
||||
|
||||
### Config
|
||||
| key | type | required |
|
||||
|--|--|--|
|
||||
| AUTH_DRIVER | 'saml' | true |
|
||||
| AUTH_SAML_CALLBACK_URL | string | true |
|
||||
| AUTH_SAML_ENTRY_POINT_URL | string | true |
|
||||
| AUTH_SAML_ISSUER | string | true |
|
||||
| AUTH_SAML_CERT | string | true |
|
||||
| AUTH_SAML_IS_AUTHN_SIGNED | boolean | false |
|
||||
|
||||
## OpenID
|
||||
***Instructions coming soon.***
|
||||
|
||||
### Config
|
||||
| key | type | required |
|
||||
|--|--|--|
|
||||
| AUTH_DRIVER | 'openid' | true |
|
||||
| AUTH_OPENID_ISSUER_URL | string | true |
|
||||
| AUTH_OPENID_CLIENT_ID | string | true |
|
||||
| AUTH_OPENID_CLIENT_SECRET | string | true |
|
||||
| AUTH_OPENID_REDIRECT_URI | string | true |
|
||||
| AUTH_OPENID_DOMAIN_WHITELIST | string | true |
|
||||
|
||||
### Google Workspace
|
||||
You can utilize either SAML or OpenID to connect to your Google Account. We'll be highlighting how to setup SAML as it is slightly easier than OpenID to configure.
|
||||
|
||||
***Instructions coming soon.***
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue