mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-09-04 12:56:14 +08:00
commit
38218c5f70
28 changed files with 24825 additions and 66286 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -2,6 +2,8 @@ name: Create & Publish Docker Image
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
|
|
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
@ -28,7 +28,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
node-version: [18.x]
|
||||
|
||||
services:
|
||||
mysql:
|
||||
|
@ -48,10 +48,10 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Node 16.x
|
||||
- name: Node 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: Cache NPM
|
||||
uses: actions/cache@v2
|
||||
|
|
4
apps/platform/.gitignore
vendored
4
apps/platform/.gitignore
vendored
|
@ -107,3 +107,7 @@ build
|
|||
|
||||
# VS Code
|
||||
.DS_Store
|
||||
|
||||
# Uploads
|
||||
/public/uploads/*
|
||||
!/public/uploads/.gitkeep
|
||||
|
|
|
@ -13,6 +13,7 @@ COPY --from=compile /usr/src/app/apps/platform/package*.json ./
|
|||
COPY --from=compile /usr/src/app/apps/platform/build ./
|
||||
COPY --from=compile /usr/src/app/apps/platform/db ./db
|
||||
COPY --from=compile /usr/src/app/apps/platform/scripts ./scripts
|
||||
COPY --from=compile /usr/src/app/apps/platform/public ./public
|
||||
RUN npm ci --only=production
|
||||
|
||||
# --------------> The production image
|
||||
|
|
12198
apps/platform/package-lock.json
generated
12198
apps/platform/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -30,6 +30,7 @@
|
|||
"koa": "^2.13.4",
|
||||
"koa-body": "5.0.0",
|
||||
"koa-jwt": "^4.0.3",
|
||||
"koa-static": "^5.0.0",
|
||||
"mysql2": "^2.3.3",
|
||||
"node-pushnotifications": "^2.0.3",
|
||||
"node-schedule": "^2.1.0",
|
||||
|
@ -42,7 +43,7 @@
|
|||
"start": "nodemon",
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint --ext .ts --max-warnings 0 src/",
|
||||
"test": "jest --forceExit --testTimeout 10000",
|
||||
"test": "jest --forceExit --runInBand --testTimeout 10000",
|
||||
"docker:build": "docker buildx build -f ./Dockerfile -t ghcr.io/parcelvoy/api:latest ../../",
|
||||
"docker:build:push": "npm run docker:build -- --push",
|
||||
"migration:create": "node ./scripts/create-migration.mjs"
|
||||
|
@ -54,6 +55,7 @@
|
|||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/koa__cors": "^3.3.0",
|
||||
"@types/koa__router": "^8.0.11",
|
||||
"@types/koa-static": "^4.0.2",
|
||||
"@types/node": "^18.7.18",
|
||||
"@types/node-pushnotifications": "^1.0.4",
|
||||
"@types/node-schedule": "^2.1.0",
|
||||
|
|
0
apps/platform/public/uploads/.gitkeep
Normal file
0
apps/platform/public/uploads/.gitkeep
Normal file
|
@ -1,6 +1,7 @@
|
|||
import Koa from 'koa'
|
||||
import koaBody from 'koa-body'
|
||||
import cors from '@koa/cors'
|
||||
import serve from 'koa-static'
|
||||
import controllers from './config/controllers'
|
||||
import { RequestError } from './core/errors'
|
||||
|
||||
|
@ -18,6 +19,10 @@ export default class Api extends Koa {
|
|||
ctx.state.app = this.app
|
||||
return next()
|
||||
})
|
||||
.use(serve('./public', {
|
||||
hidden: true,
|
||||
defer: true,
|
||||
}))
|
||||
|
||||
this.use(async (ctx, next) => {
|
||||
try {
|
||||
|
|
|
@ -205,7 +205,7 @@ describe('CampaignService', () => {
|
|||
send_at: new Date(),
|
||||
}) as SentCampaign
|
||||
|
||||
const inclusiveIds = []
|
||||
const inclusiveIds: number[] = []
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const user = await createUser(params.project_id)
|
||||
await addUserToList(user, list)
|
||||
|
|
|
@ -35,7 +35,8 @@ const connect = (config: DatabaseConfig, withDB = true) => {
|
|||
})
|
||||
}
|
||||
|
||||
const migrate = async (db: Database) => {
|
||||
const migrate = async (db: Database, fresh = false) => {
|
||||
if (fresh) await db.raw('CREATE DATABASE parcelvoy')
|
||||
return db.migrate.latest({
|
||||
directory: './db/migrations',
|
||||
tableName: 'migrations',
|
||||
|
@ -53,7 +54,7 @@ export default async (config: DatabaseConfig) => {
|
|||
|
||||
// On error, try to create the database and try again
|
||||
const db = connect(config, false)
|
||||
db.raw('CREATE DATABASE parcelvoy')
|
||||
await migrate(db, true)
|
||||
return connect(config)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,19 +53,20 @@ export default (type?: EnvType): Env => {
|
|||
},
|
||||
}),
|
||||
}),
|
||||
storage: {
|
||||
baseUrl: process.env.STORAGE_BASE_URL!,
|
||||
...driver<Omit<StorageConfig, 'baseUrl'>>(process.env.STORAGE_DRIVER, {
|
||||
s3: () => ({
|
||||
bucket: process.env.AWS_S3_BUCKET!,
|
||||
region: process.env.AWS_REGION!,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
||||
},
|
||||
}),
|
||||
storage: driver<StorageConfig>(process.env.STORAGE_DRIVER ?? 'local', {
|
||||
s3: () => ({
|
||||
baseUrl: process.env.STORAGE_BASE_URL,
|
||||
bucket: process.env.AWS_S3_BUCKET!,
|
||||
region: process.env.AWS_REGION!,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
||||
},
|
||||
}),
|
||||
},
|
||||
local: () => ({
|
||||
baseUrl: process.env.STORAGE_BASE_URL,
|
||||
}),
|
||||
}),
|
||||
baseUrl: process.env.BASE_URL!,
|
||||
port: parseInt(process.env.PORT!),
|
||||
secret: process.env.APP_SECRET!,
|
||||
|
|
|
@ -218,13 +218,17 @@ export default class Model {
|
|||
}
|
||||
}
|
||||
|
||||
static async insert<T extends typeof Model>(this: T, data: Partial<InstanceType<T>>, db?: Database): Promise<number>
|
||||
static async insert<T extends typeof Model>(this: T, data: Partial<InstanceType<T>>[], db?: Database): Promise<number[]>
|
||||
static async insert<T extends typeof Model>(
|
||||
this: T,
|
||||
data: Partial<InstanceType<T>> | Partial<InstanceType<T>>[] = {},
|
||||
db: Database = App.main.db,
|
||||
): Promise<number> {
|
||||
): Promise<number | number[]> {
|
||||
const formattedData = this.formatJson(data)
|
||||
return await this.table(db).insert(formattedData)
|
||||
const value = await this.table(db).insert(formattedData)
|
||||
if (Array.isArray(data)) return value
|
||||
return value[0]
|
||||
}
|
||||
|
||||
static async insertAndFetch<T extends typeof Model>(
|
||||
|
@ -232,8 +236,7 @@ export default class Model {
|
|||
data: Partial<InstanceType<T>> = {},
|
||||
db: Database = App.main.db,
|
||||
): Promise<InstanceType<T>> {
|
||||
const formattedData = this.formatJson(data)
|
||||
const id: number = await this.table(db).insert(formattedData)
|
||||
const id = await this.insert(data, db)
|
||||
return await this.find(id, b => b, db) as InstanceType<T>
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ export const openWrapHtml = (html: string, params: TrackedLinkParams) => {
|
|||
const imageHtml = `<img border="0" width="1" height="1" alt="" src="${link}" />`
|
||||
const hasBody = html.includes(bodyTag)
|
||||
if (hasBody) {
|
||||
html.replace(bodyTag, imageHtml + bodyTag)
|
||||
html = html.replace(bodyTag, (imageHtml + bodyTag))
|
||||
} else {
|
||||
html += imageHtml
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Model from '../core/Model'
|
||||
import { combineURLs } from '../utilities'
|
||||
import Storage from './Storage'
|
||||
|
||||
export default class Image extends Model {
|
||||
project_id!: number
|
||||
|
@ -15,7 +15,7 @@ export default class Image extends Model {
|
|||
}
|
||||
|
||||
get url(): string {
|
||||
return combineURLs([process.env.STORAGE_BASE_URL!, this.filename])
|
||||
return Storage.url(this.filename)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
|
26
apps/platform/src/storage/LocalStorageProvider.ts
Normal file
26
apps/platform/src/storage/LocalStorageProvider.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { StorageTypeConfig } from './Storage'
|
||||
import { ImageUploadTask, StorageProvider } from './StorageProvider'
|
||||
|
||||
export interface LocalConfig extends StorageTypeConfig {
|
||||
driver: 'local'
|
||||
}
|
||||
|
||||
export class LocalStorageProvider implements StorageProvider {
|
||||
|
||||
path(filename: string) {
|
||||
return path.join(process.cwd(), 'public', 'uploads', filename)
|
||||
}
|
||||
|
||||
async upload(task: ImageUploadTask) {
|
||||
await fs.writeFile(
|
||||
task.url,
|
||||
task.stream,
|
||||
)
|
||||
}
|
||||
|
||||
async delete(filename: string): Promise<void> {
|
||||
await fs.unlink(filename)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import { StorageTypeConfig } from './Storage'
|
|||
import { ImageUploadTask, StorageProvider } from './StorageProvider'
|
||||
|
||||
export interface S3Config extends StorageTypeConfig, AWSConfig {
|
||||
driver: 's3'
|
||||
bucket: string
|
||||
}
|
||||
|
||||
|
@ -17,6 +18,10 @@ export class S3StorageProvider implements StorageProvider {
|
|||
this.config = config
|
||||
}
|
||||
|
||||
path(filename: string) {
|
||||
return filename
|
||||
}
|
||||
|
||||
async upload(task: ImageUploadTask) {
|
||||
const pass = new PassThrough()
|
||||
const s3 = new S3(this.config)
|
||||
|
|
|
@ -8,10 +8,12 @@ import { combineURLs, uuid } from '../utilities'
|
|||
import { InternalError } from '../core/errors'
|
||||
import StorageError from './StorageError'
|
||||
import App from '../app'
|
||||
import { LocalConfig, LocalStorageProvider } from './LocalStorageProvider'
|
||||
|
||||
export type StorageConfig = S3Config & { baseUrl: string }
|
||||
export type StorageConfig = S3Config | LocalConfig
|
||||
export interface StorageTypeConfig extends DriverConfig {
|
||||
driver: StorageProviderName
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
export interface ImageUpload {
|
||||
|
@ -24,6 +26,8 @@ export default class Storage {
|
|||
constructor(config?: StorageConfig) {
|
||||
if (config?.driver === 's3') {
|
||||
this.provider = new S3StorageProvider(config)
|
||||
} else if (config?.driver === 'local') {
|
||||
this.provider = new LocalStorageProvider()
|
||||
} else {
|
||||
throw new InternalError(StorageError.UndefinedStorageMethod)
|
||||
}
|
||||
|
@ -34,7 +38,7 @@ export default class Storage {
|
|||
const originalPath = path.parse(image.metadata.fileName)
|
||||
const extension = originalPath.ext
|
||||
const fileName = originalPath.name
|
||||
const url = `${key}${extension}`
|
||||
const url = this.provider.path(`${key}${extension}`)
|
||||
|
||||
await this.upload({
|
||||
stream: image.file,
|
||||
|
@ -53,7 +57,14 @@ export default class Storage {
|
|||
await this.provider.upload(task)
|
||||
}
|
||||
|
||||
url(path: string): string {
|
||||
return combineURLs([App.main.env.storage.baseUrl, path])
|
||||
static url(path: string): string {
|
||||
|
||||
// If an override is provide, utilize that
|
||||
if (App.main.env.storage.baseUrl) {
|
||||
return combineURLs([App.main.env.storage.baseUrl, path])
|
||||
}
|
||||
|
||||
// Otherwise default back to local path
|
||||
return `/uploads/${path}`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Stream } from 'stream'
|
||||
|
||||
export type StorageProviderName = 's3'
|
||||
export type StorageProviderName = 's3' | 'local'
|
||||
|
||||
export interface ImageUploadTask {
|
||||
stream: Stream
|
||||
|
@ -8,6 +8,7 @@ export interface ImageUploadTask {
|
|||
}
|
||||
|
||||
export interface StorageProvider {
|
||||
path(filename: string): string
|
||||
upload(task: ImageUploadTask): Promise<void>
|
||||
delete(filename: string): Promise<void>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# --------------> The compiler image
|
||||
FROM node:16 AS compile
|
||||
FROM node:18 AS compile
|
||||
WORKDIR /usr/src/app/apps/ui
|
||||
COPY ./tsconfig.base.json /usr/src/app
|
||||
COPY ./apps/ui ./
|
||||
|
|
|
@ -10,4 +10,10 @@ server {
|
|||
location /api {
|
||||
proxy_pass http://api:3001;
|
||||
}
|
||||
|
||||
location /uploads {
|
||||
proxy_pass http://api:3001;
|
||||
}
|
||||
|
||||
client_max_body_size 64M;
|
||||
}
|
13541
apps/ui/package-lock.json
generated
13541
apps/ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^4.5.14",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@headlessui/react": "1.7.3",
|
||||
"@heroicons/react": "^2.0.11",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
|
@ -26,7 +26,7 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.33.0",
|
||||
"react-hot-toast": "2.4.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-router-dom": "^6.4.2",
|
||||
"react-scripts": "5.0.1",
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
|
||||
.modal.fullscreen .modal-content {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-inner .modal-header h3 {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
.email-editor {
|
||||
height: 100vh;
|
||||
background: var(--color-background);
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-template-rows: 60px auto auto;
|
||||
|
|
|
@ -168,20 +168,22 @@ export default function EmailEditor() {
|
|||
open
|
||||
onClose={() => navigate(`../campaigns/${campaign.id}/design`)}
|
||||
>
|
||||
<Tabs
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={setSelectedIndex}
|
||||
tabs={tabs}
|
||||
append={
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => setIsAddLocaleOpen(true)}
|
||||
>
|
||||
{'Add Locale'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<section className="email-editor">
|
||||
<Tabs
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={setSelectedIndex}
|
||||
tabs={tabs}
|
||||
append={
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => setIsAddLocaleOpen(true)}
|
||||
>
|
||||
{'Add Locale'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
</Modal>
|
||||
<CreateLocaleModal
|
||||
open={isAddLocaleOpen}
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
}
|
||||
|
||||
.image-gallery .image img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
|
@ -28,7 +28,7 @@ services:
|
|||
NODE_ENV: ${NODE_ENV}
|
||||
BASE_URL: ${BASE_URL}
|
||||
APP_SECRET: ${APP_SECRET}
|
||||
PORT: 3000
|
||||
PORT: 3001
|
||||
DB_CLIENT: ${DB_CLIENT}
|
||||
DB_HOST: mysql
|
||||
DB_USERNAME: ${DB_USERNAME}
|
||||
|
@ -41,7 +41,7 @@ services:
|
|||
QUEUE_DRIVER: ${QUEUE_DRIVER}
|
||||
AWS_SQS_QUEUE_URL: ${AWS_SQS_QUEUE_URL}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
||||
AWS_SECRET_ACCESS_KEY: ${S3_LOCAWS_SECRET_ACCESS_KEYAL_STACK}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
||||
AWS_REGION: ${AWS_REGION}
|
||||
AUTH_DRIVER: ${AUTH_DRIVER}
|
||||
AUTH_SAML_CALLBACK_URL: ${AUTH_SAML_CALLBACK_URL}
|
||||
|
@ -54,6 +54,8 @@ services:
|
|||
AUTH_OPENID_CLIENT_SECRET: ${AUTH_OPENID_CLIENT_SECRET}
|
||||
AUTH_OPENID_REDIRECT_URI: ${AUTH_OPENID_REDIRECT_URI}
|
||||
AUTH_OPENID_DOMAIN_WHITELIST: ${AUTH_OPENID_DOMAIN_WHITELIST}
|
||||
volumes:
|
||||
- uploads:/usr/src/app/public/uploads
|
||||
ui:
|
||||
image: 'ghcr.io/parcelvoy/ui:latest'
|
||||
depends_on:
|
||||
|
@ -64,4 +66,6 @@ services:
|
|||
- 80:3000
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
uploads:
|
||||
driver: local
|
65188
package-lock.json
generated
65188
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue