mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-08-28 11:46:02 +08:00
Adds basic auth option
This commit is contained in:
parent
0701d6e374
commit
034abfd15c
15 changed files with 183 additions and 11 deletions
21
.env
Normal file
21
.env
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
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -70,7 +70,6 @@ typings/
|
|||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
|
22
README.md
22
README.md
|
@ -12,4 +12,24 @@ 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.yaml` and `.env` file onto your machine and go!
|
||||
```
|
||||
mkdir parcelvoy && cd parcelvoy
|
||||
wget https://raw.githubusercontent.com/parcelvoy/parcelvoy/master/{.env,docker-compose.yaml}
|
||||
docker compose up -d # run the Docker container
|
||||
```
|
||||
|
||||
Login to the web app at http://localhost:3000 by entering the default credentials found in your .env file.
|
||||
|
||||
```
|
||||
AUTH_BASIC_USERNAME=parcelvoy
|
||||
AUTH_BASIC_PASSWORD=password
|
||||
```
|
||||
|
||||
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
|
||||
- ${PORT}:3000
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue