Merge branch 'master' into blog/bring-the-fun-hackathon-winners
No files matched your search
@@ -0,0 +1,91 @@
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
name: Tests
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the main branch
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
autotests:
|
||||
name: Run tests and generate Allure Report
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['14']
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./tests
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run infrastructure
|
||||
run: |
|
||||
cp ../docker/.env.example ../docker/.env
|
||||
npm run docker:up
|
||||
|
||||
- name: Run Test
|
||||
run: npm run test
|
||||
continue-on-error: true
|
||||
|
||||
- name: Stop infrastructure
|
||||
run: npm run docker:down
|
||||
|
||||
- name: Get Allure history
|
||||
uses: actions/checkout@v2
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: supabase/test-reports
|
||||
ref: gh-pages
|
||||
path: gh-pages
|
||||
|
||||
- name: Allure Report action
|
||||
uses: simple-elf/allure-report-action@master
|
||||
if: always()
|
||||
with:
|
||||
github_repo: test-reports
|
||||
allure_results: allure-results
|
||||
allure_history: allure-history
|
||||
subfolder: supabase-integration
|
||||
keep_reports: 50
|
||||
|
||||
- name: Deploy report to Github Pages
|
||||
if: always()
|
||||
uses: peaceiris/actions-gh-pages@v2
|
||||
env:
|
||||
EXTERNAL_REPOSITORY: supabase/test-reports
|
||||
ACTIONS_DEPLOY_KEY: ${{ secrets.DEPLOY_TO_TEST_REPORTS_KEY }}
|
||||
PUBLISH_BRANCH: gh-pages
|
||||
PUBLISH_DIR: allure-history
|
||||
|
||||
- name: Post the link to the report
|
||||
uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
context: 'Test report'
|
||||
state: 'success'
|
||||
sha: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
target_url: https://supabase.github.io/test-reports/supabase-integration/${{ github.run_number }}
|
||||
@@ -6,6 +6,8 @@ out
|
||||
tmp
|
||||
|
||||
coverage
|
||||
allure-results
|
||||
allure-report
|
||||
.nyc_output
|
||||
*.log
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ DISABLE_SIGNUP=false
|
||||
|
||||
## Email auth
|
||||
ENABLE_EMAIL_SIGNUP=true
|
||||
ENABLE_EMAIL_AUTOCONFIRM=true
|
||||
ENABLE_EMAIL_AUTOCONFIRM=false
|
||||
SMTP_ADMIN_EMAIL=admin@example.com
|
||||
SMTP_HOST=mail
|
||||
SMTP_PORT=2500
|
||||
@@ -25,8 +25,8 @@ SMTP_PASS=fake_mail_password
|
||||
SMTP_SENDER_NAME=fake_sender
|
||||
|
||||
## Phone auth
|
||||
ENABLE_PHONE_SIGNUP=false
|
||||
ENABLE_PHONE_AUTOCONFIRM=false
|
||||
ENABLE_PHONE_SIGNUP=true
|
||||
ENABLE_PHONE_AUTOCONFIRM=true
|
||||
|
||||
# Ports
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^1.1.2",
|
||||
"@supabase/supabase-js": "^1.34.1",
|
||||
"next": "latest",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"sass": "^1.26.2",
|
||||
"tailwindcss": "^1.1.4"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Supabase
|
||||
|
||||
[Supabase](https://supabase.com) – это альтернатива Firebase с открытым исходным кодом. Мы создаем функционал Firebase, используя инструменты Enterprise уровня с открытым исходным кодом.
|
||||
[Supabase](https://supabase.com) – это альтернатива Firebase с открытым исходным кодом. Мы создаем функциональность Firebase, используя инструменты Enterprise уровня с открытым исходным кодом.
|
||||
|
||||
- [x] Облачная база данных Postgres
|
||||
- [x] Подписки в режиме реального времени
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
## Как это работает
|
||||
|
||||
Supabase - это сочетание инструментов с открытым исходным кодом. Мы создаем функционал Firebase, используя инструменты Enterprise уровня с открытым исходным кодом. Если инструмент с открытой лицензией MIT, Apache 2 или аналогичной существует, мы будем использовать и поддерживать его. Если такого инструмента не существует, мы создаем и открываем его в публичный доступ сами. Supabase не является точной копией Firebase. Наша цель - предоставить разработчикам опыт работы с Firebase, используя инструменты с открытым исходным кодом.
|
||||
Supabase - это сочетание инструментов с открытым исходным кодом. Мы создаем функциональность Firebase, используя инструменты Enterprise уровня с открытым исходным кодом. Если инструмент с открытой лицензией MIT, Apache 2 или аналогичной существует, мы будем использовать и поддерживать его. Если такого инструмента не существует, мы создаем и открываем его в публичный доступ сами. Supabase не является точной копией Firebase. Наша цель - предоставить разработчикам опыт работы с Firebase, используя инструменты с открытым исходным кодом.
|
||||
|
||||
**Текущая архитектура**
|
||||
|
||||
@@ -49,8 +49,8 @@ Supabase – это [облачная платформа](https://app.supabase.i
|
||||
|
||||

|
||||
|
||||
- [PostgreSQL](https://www.postgresql.org/) – это объектно-реляционная система баз данных с более чем 30 годами активной разработки, которая завоевала прочную репутацию благодаря надежности, устойчивости функционала и производительности.
|
||||
- [Realtime](https://github.com/supabase/realtime) – это сервер, написанный на Elixir, который позволяет cледить за вставками, обновлениями и удалениями в базе PostgreSQL с помощью веб-сокета. Supabase использует встроенный функционал репликации Postgres, преобразует поток байтов репликации в JSON, а затем передает JSON через веб-сокеты.
|
||||
- [PostgreSQL](https://www.postgresql.org/) – это объектно-реляционная система баз данных с более чем 30 годами активной разработки, которая завоевала прочную репутацию благодаря надежности, устойчивости функциональности и производительности.
|
||||
- [Realtime](https://github.com/supabase/realtime) – это сервер, написанный на Elixir, который позволяет cледить за вставками, обновлениями и удалениями в базе PostgreSQL с помощью веб-сокета. Supabase использует встроенную функциональность репликации Postgres, преобразует поток байтов репликации в JSON, а затем передает JSON через веб-сокеты.
|
||||
- [PostgREST](http://postgrest.org/) – это веб-сервер, который превращает вашу базу данных PostgreSQL в RESTful API.
|
||||
- [Storage](https://github.com/supabase/storage-api) предоставляет RESTful интерфейс для управления файлами, хранящимися в S3, используя Postgres для управления разрешениями.
|
||||
- [postgres-meta](https://github.com/supabase/postgres-meta) – это RESTful API для управления Postgres, позволяющий получать таблицы, добавлять роли, выполнять запросы и т.д.
|
||||
|
||||
@@ -7,6 +7,7 @@ import { post } from 'lib/common/fetch'
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { UpdateSuccess } from '.'
|
||||
import BackButton from 'components/ui/BackButton'
|
||||
|
||||
interface Props {
|
||||
onSelectBack: () => void
|
||||
@@ -21,7 +22,9 @@ const EnterpriseRequest: FC<Props> = ({ onSelectBack }) => {
|
||||
const [isSuccessful, setIsSuccessful] = useState(false)
|
||||
|
||||
const initialValues = {
|
||||
name: `${profile?.first_name} ${profile?.last_name}`,
|
||||
name: `${profile?.first_name ? profile?.first_name : ''} ${
|
||||
profile?.last_name ? profile.last_name : ''
|
||||
}`,
|
||||
email: profile?.primary_email,
|
||||
company: '',
|
||||
message: '',
|
||||
@@ -81,43 +84,37 @@ const EnterpriseRequest: FC<Props> = ({ onSelectBack }) => {
|
||||
enterTo="transform opacity-100 translate-x-0"
|
||||
>
|
||||
<div className="space-y-8 w-4/5">
|
||||
<div className="relative">
|
||||
<div className="absolute top-[2px] -left-24">
|
||||
<Button type="text" icon={<IconArrowLeft />} onClick={onSelectBack}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-lg">Customised plans tailored for your business needs</h4>
|
||||
<p className="text-scale-1100">
|
||||
Let us know a few details and we’ll be in contact with you!
|
||||
</p>
|
||||
</div>
|
||||
<Form
|
||||
validateOnBlur
|
||||
initialValues={initialValues}
|
||||
validate={onValidate}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting }: { isSubmitting: boolean }) => (
|
||||
<div className="space-y-12 py-16">
|
||||
<div className="space-y-4 w-3/5">
|
||||
<Input layout="horizontal" label="Name" id="name" name="name" />
|
||||
<Input layout="horizontal" label="Email" id="email" name="email" />
|
||||
<Input layout="horizontal" label="Company" id="company" name="company" />
|
||||
</div>
|
||||
<Input.TextArea
|
||||
id="message"
|
||||
name="message"
|
||||
label="Let us know what you intend to use Supabase for"
|
||||
/>
|
||||
<Button htmlType="submit" loading={isSubmitting}>
|
||||
Submit request
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
<BackButton onClick={() => onSelectBack()} />
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-lg">Customised plans tailored for your business needs</h4>
|
||||
<p className="text-scale-1100">
|
||||
Let us know a few details and our team will get back to you as soon as possible.
|
||||
</p>
|
||||
</div>
|
||||
<Form
|
||||
validateOnBlur
|
||||
initialValues={initialValues}
|
||||
validate={onValidate}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting }: { isSubmitting: boolean }) => (
|
||||
<div className="space-y-12">
|
||||
<div className="space-y-4 w-3/5">
|
||||
<Input layout="horizontal" label="Name" id="name" name="name" />
|
||||
<Input layout="horizontal" label="Email" id="email" name="email" />
|
||||
<Input layout="horizontal" label="Company" id="company" name="company" />
|
||||
</div>
|
||||
<Input.TextArea
|
||||
id="message"
|
||||
name="message"
|
||||
label="Let us know what you intend to use Supabase for"
|
||||
/>
|
||||
<Button htmlType="submit" loading={isSubmitting}>
|
||||
Submit request
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</Transition>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { FC } from 'react'
|
||||
import { Listbox, IconLoader, Button, IconPlus, IconAlertCircle } from '@supabase/ui'
|
||||
import {
|
||||
Listbox,
|
||||
IconLoader,
|
||||
Button,
|
||||
IconPlus,
|
||||
IconAlertCircle,
|
||||
IconCreditCard,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { SubscriptionPreview } from '../Billing.types'
|
||||
import { getProductPrice } from '../Billing.utils'
|
||||
@@ -48,6 +55,11 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
onConfirmPayment,
|
||||
isSubmitting,
|
||||
}) => {
|
||||
/**
|
||||
* Feature flags
|
||||
*/
|
||||
const nativeBilling = useFlag('nativeBilling')
|
||||
|
||||
const { ui } = useStore()
|
||||
const projectRegion = ui.selectedProject?.region
|
||||
|
||||
@@ -66,14 +78,14 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-scale-300 w-full px-20 py-10 space-y-8"
|
||||
className="bg-panel-body-light dark:bg-panel-body-dark w-full px-6 lg:px-12 py-10 space-y-8 border-l"
|
||||
style={{ height: 'calc(100vh - 49.5px)' }}
|
||||
>
|
||||
<p>Payment Summary</p>
|
||||
|
||||
{/* Subscription details */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">Selected subscription</p>
|
||||
<p className="text-sm text-scale-1100">Selected subscription</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={`${isChangingPlan ? 'text-scale-1100 line-through' : ''} text-sm`}>
|
||||
{getPlanName(currentPlan)}
|
||||
@@ -93,46 +105,54 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
{/* Add on details */}
|
||||
{projectRegion !== 'af-south-1' && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">Selected add-ons</p>
|
||||
{currentComputeSize === undefined && selectedComputeSize === undefined && (
|
||||
<p className="text-scale-1100 text-sm">No add-ons selected</p>
|
||||
)}
|
||||
{currentComputeSize !== undefined && (
|
||||
<div className="flex items-center justify-between">
|
||||
<p
|
||||
className={`${isChangingComputeSize ? 'text-scale-1100 line-through' : ''} text-sm`}
|
||||
>
|
||||
{currentComputeSize.name}
|
||||
</p>
|
||||
<p
|
||||
className={`${isChangingComputeSize ? 'text-scale-1100 line-through' : ''} text-sm`}
|
||||
>
|
||||
${(getProductPrice(currentComputeSize).unit_amount / 100).toFixed(2)}
|
||||
</p>
|
||||
{nativeBilling && (
|
||||
<>
|
||||
{projectRegion !== 'af-south-1' && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">Selected add-ons</p>
|
||||
{currentComputeSize === undefined && selectedComputeSize === undefined && (
|
||||
<p className="text-scale-1100 text-sm">No add-ons selected</p>
|
||||
)}
|
||||
{currentComputeSize !== undefined && (
|
||||
<div className="flex items-center justify-between">
|
||||
<p
|
||||
className={`${
|
||||
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
|
||||
} text-sm`}
|
||||
>
|
||||
{currentComputeSize.name}
|
||||
</p>
|
||||
<p
|
||||
className={`${
|
||||
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
|
||||
} text-sm`}
|
||||
>
|
||||
${(getProductPrice(currentComputeSize).unit_amount / 100).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isChangingComputeSize && (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm">{selectedComputeSize.name}</p>
|
||||
<p className="text-sm">
|
||||
${(getProductPrice(selectedComputeSize).unit_amount / 100).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isChangingComputeSize && (
|
||||
<div className="!mt-4">
|
||||
<InformationBox
|
||||
hideCollapse
|
||||
defaultVisibility
|
||||
icon={<IconAlertCircle strokeWidth={2} />}
|
||||
title="Changing your compute size"
|
||||
description="It will take up to 2 minutes for changes to take place, and your project will be unavailable during that time"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isChangingComputeSize && (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm">{selectedComputeSize.name}</p>
|
||||
<p className="text-sm">
|
||||
${(getProductPrice(selectedComputeSize).unit_amount / 100).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isChangingComputeSize && (
|
||||
<div className="!mt-4">
|
||||
<InformationBox
|
||||
hideCollapse
|
||||
defaultVisibility
|
||||
icon={<IconAlertCircle strokeWidth={2} />}
|
||||
title="Changing your compute size"
|
||||
description="It will take up to 2 minutes for changes to take place, and your project will be unavailable during that time"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="h-px w-full bg-scale-600" />
|
||||
@@ -152,12 +172,12 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
<p className="text-sm text-scale-1100">Retrieving payment methods</p>
|
||||
</div>
|
||||
) : paymentMethods.length === 0 ? (
|
||||
<div className="flex items-center justify-between rounded-md px-4 py-2 bg-scale-400 border border-scale-700">
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconAlertCircle size={16} />
|
||||
<p className="text-scale-1100 text-sm">No saved payment methods</p>
|
||||
<div className="flex items-center justify-between rounded-md px-4 py-2 bg-scale-100 border border-dashed">
|
||||
<div className="flex items-center space-x-4 text-scale-1100">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm">No saved payment methods</p>
|
||||
</div>
|
||||
<Button type="default" icon={<IconPlus />} onClick={onSelectAddNewPaymentMethod}>
|
||||
<Button type="default" icon={<IconCreditCard />} onClick={onSelectAddNewPaymentMethod}>
|
||||
Add new
|
||||
</Button>
|
||||
</div>
|
||||
@@ -204,6 +224,8 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || isLoadingPaymentMethods || !hasChangesToPlan}
|
||||
onClick={() => onConfirmPayment()}
|
||||
block
|
||||
size="medium"
|
||||
>
|
||||
Confirm payment
|
||||
</Button>
|
||||
|
||||
@@ -71,7 +71,7 @@ const PaymentTotal: FC<Props> = ({
|
||||
{amountDueImmediately < 0 ? (
|
||||
<p className="text-sm text-scale-1100">
|
||||
A total of ${Math.abs(amountDueImmediately).toFixed(2)} will be returned on{' '}
|
||||
<span className="font-bold text-green-1100">
|
||||
<span className="text-brand-900">
|
||||
{billingDate.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
@@ -84,7 +84,7 @@ const PaymentTotal: FC<Props> = ({
|
||||
<p className="text-sm text-scale-1100">
|
||||
This amount {!isSpendCapEnabled && !isBillingToday && '+ usage fees '}will be
|
||||
charged on{' '}
|
||||
<span className="font-bold text-green-1100">
|
||||
<span className="text-brand-900">
|
||||
{billingDate.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
|
||||
@@ -25,10 +25,15 @@ const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan })
|
||||
<div className="space-y-8">
|
||||
<h4 className="text-lg">Change your project's subscription</h4>
|
||||
{/* FE will make a call to fetch all plans first at the page level */}
|
||||
<Plans plans={formattedTiers} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
|
||||
<div className="grid lg:grid-cols-3 gap-8 py-8">
|
||||
<Plans plans={formattedTiers} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<Link href="https://supabase.com/pricing">
|
||||
<a target="_blank" className="text-sm text-scale-1100 hover:text-scale-1200 transition">
|
||||
<a
|
||||
target="_blank"
|
||||
className="text-sm underline text-scale-1100 hover:text-scale-1200 transition"
|
||||
>
|
||||
See detailed comparisons across plans
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -59,7 +59,13 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Button disabled={disabled} type={type} onClick={() => onSelectPlan(plan)}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
onClick={() => onSelectPlan(plan)}
|
||||
block
|
||||
size="medium"
|
||||
>
|
||||
{ctaText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
|
||||
/**
|
||||
* @mildtomato same plan metadata is also in /www website
|
||||
* would be good if this constant was shared across apps
|
||||
*
|
||||
* https://github.com/supabase/supabase/blob/master/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts
|
||||
*/
|
||||
export const PRICING_META = {
|
||||
[STRIPE_PRODUCT_IDS.FREE]: {
|
||||
name: 'Free',
|
||||
href: '#',
|
||||
priceMonthly: 0,
|
||||
warning: 'Limit of 2 free projects',
|
||||
description: 'Perfect for passion projects & simple websites.',
|
||||
features: [
|
||||
'Up to 500MB database & 1GB file storage',
|
||||
'Up to 2GB bandwidth',
|
||||
'Up to 50MB file uploads',
|
||||
'Social OAuth providers',
|
||||
'Up to 500K Edge Function invocations',
|
||||
'1-day log retention',
|
||||
'Community support',
|
||||
],
|
||||
additional: 'Free projects are paused after 1 week of inactivity.',
|
||||
|
||||
cta: 'Get Started',
|
||||
},
|
||||
[STRIPE_PRODUCT_IDS.PRO]: {
|
||||
name: 'Pro',
|
||||
href: '#',
|
||||
from: true,
|
||||
priceMonthly: 25,
|
||||
warning: '+ additional use',
|
||||
description: 'For production applications with the option to scale.',
|
||||
features: [
|
||||
'8GB database & 100GB file storage',
|
||||
'50GB bandwith',
|
||||
'3GB file uploads',
|
||||
'Social OAuth providers',
|
||||
'2M Edge Function invocations',
|
||||
'Daily backups',
|
||||
'7-day log retention',
|
||||
'No project pausing',
|
||||
'Email support',
|
||||
],
|
||||
scale: 'Additional fees apply for usage and storage beyond the limits above.',
|
||||
shutdown: '',
|
||||
preface: 'Everything below included in the base plan',
|
||||
additional: 'Need more? Turn off your spend cap to Pay As You Grow ',
|
||||
cta: 'Get Started',
|
||||
},
|
||||
Enterprise: {
|
||||
name: 'Enterprise',
|
||||
href: '/contact/enterprise',
|
||||
description: 'For large-scale applications managing serious workloads.',
|
||||
features: [
|
||||
`Point in time recovery`,
|
||||
`Designated Support manager & SLAs`,
|
||||
`Enterprise OAuth providers`,
|
||||
`SSO/ SAML`,
|
||||
`SOC2`,
|
||||
`Custom contracts & invoicing`,
|
||||
`On-premise support`,
|
||||
`24×7×365 premium enterprise support`,
|
||||
],
|
||||
scale: '',
|
||||
shutdown: '',
|
||||
cta: 'Contact Us',
|
||||
},
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FC } from 'react'
|
||||
import { Badge } from '@supabase/ui'
|
||||
|
||||
import { IconCheck } from '@supabase/ui'
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { FC } from 'react'
|
||||
import { StripeProduct } from '../..'
|
||||
import PlanCTAButton from './PlanCTAButton'
|
||||
import { PRICING_META } from './Plans.Constants'
|
||||
|
||||
interface Props {
|
||||
plans: any[]
|
||||
@@ -23,84 +23,127 @@ const AnimatedGradientBackground = () => (
|
||||
const CurrentSubscriptionBanner = () => (
|
||||
<div
|
||||
className={[
|
||||
'absolute top-0 right-0 flex items-center justify-center',
|
||||
'bg-green-900 px-4 py-1 rounded-bl-md',
|
||||
'absolute h-8 -top-7 left-0 w-full',
|
||||
'flex items-center justify-center',
|
||||
'bg-brand-400 text-brand-900',
|
||||
'rounded-t-md border-t border-l border-r border border-b-0',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-xs">Current plan</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
/**
|
||||
* JSX below for the mapped plans is also very similar to www/pages/pricing
|
||||
* TO DO: move to use shared components for these
|
||||
*
|
||||
* https://github.com/supabase/supabase/blob/master/www/pages/pricing/index.tsx
|
||||
*/
|
||||
|
||||
const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
|
||||
return (
|
||||
<div className="flex justify-between space-x-4">
|
||||
<>
|
||||
{plans.map((plan) => {
|
||||
const isCurrentPlan =
|
||||
plan.id === currentPlan?.prod_id ||
|
||||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO) ||
|
||||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG)
|
||||
|
||||
const pointers = plan.metadata.features.split('\\n').map((x: string) => x.trim())
|
||||
return (
|
||||
<div
|
||||
key={plan.name}
|
||||
className={[
|
||||
'flex flex-col justify-between w-1/3 px-6 py-8',
|
||||
'bg-gray-300 border border-gray-500 rounded-md relative',
|
||||
'overflow-hidden',
|
||||
].join(' ')}
|
||||
>
|
||||
{/* [Joshen] Trying some animations here to make Pro pop, on the fence tbh */}
|
||||
{plan.id === STRIPE_PRODUCT_IDS.PRO && <AnimatedGradientBackground />}
|
||||
|
||||
{/* Label to show current subscription (Pro and PAYG are treated as the same plan on the UI) */}
|
||||
{isCurrentPlan && <CurrentSubscriptionBanner />}
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h3 className="text-xl">{plan.name}</h3>
|
||||
{plan.isPopular && <Badge>Popular</Badge>}
|
||||
<div key={PRICING_META[plan.id].name} className="relative h-full">
|
||||
{plan.id === STRIPE_PRODUCT_IDS.PRO && (
|
||||
<div className="absolute overflow-hidden w-full h-full left-0 bottom-0 opacity-50">
|
||||
<AnimatedGradientBackground />
|
||||
</div>
|
||||
<p className="text-scale-1100 text-sm mt-2">{plan.description}</p>
|
||||
<div className="py-8">
|
||||
{plan.prices.length === 0 ? (
|
||||
<p className="text-2xl text-center">Contact us</p>
|
||||
) : (
|
||||
<div className="flex items-end justify-center">
|
||||
<p className="text-3xl">${plan.prices[0].unit_amount / 100}</p>
|
||||
<p className="text-sm text-scale-1100 relative -top-[2px]"> /month</p>
|
||||
)}
|
||||
<div className="flex flex-col rounded border overflow-hidden h-full">
|
||||
{isCurrentPlan && <CurrentSubscriptionBanner />}
|
||||
<div className="px-8 py-6 bg-white dark:bg-scale-300">
|
||||
<span
|
||||
className="inline-flex text-cd font-normal tracking-wide rounded-full text-base text-scale-1200"
|
||||
id="tier-standard"
|
||||
>
|
||||
{PRICING_META[plan.id].name}
|
||||
</span>
|
||||
<div className="flex items-baseline mt-2">
|
||||
<div className="flex space-x-2">
|
||||
{PRICING_META[plan.id].priceMonthly !== undefined ? (
|
||||
<>
|
||||
<div className="flex items-end gap-1">
|
||||
{PRICING_META[plan.id].from && (
|
||||
<span className="text-base font-medium text-scale-1200">From</span>
|
||||
)}
|
||||
<div>
|
||||
<span className="text-2xl">${PRICING_META[plan.id].priceMonthly}</span>
|
||||
<span className="ml-1 text-xl font-medium text-scale-900">/mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-2xl">Contact Us</span>
|
||||
)}
|
||||
<div className="flex h-8">
|
||||
{PRICING_META[plan.id].warning && (
|
||||
<div className="px-2 py-1 mt-2 text-xs rounded-md bg-brand-300 bg-opacity-30 text-brand-1000">
|
||||
{PRICING_META[plan.id].warning}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<PlanCTAButton
|
||||
plan={plan}
|
||||
currentPlan={currentPlan}
|
||||
onSelectPlan={onSelectPlan}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-scale-1100">{PRICING_META[plan.id].description}</p>
|
||||
</div>
|
||||
<div
|
||||
className={[
|
||||
'flex-col justify-between flex-1',
|
||||
'space-y-6 border-t dark:border-scale-400',
|
||||
'bg-scale-100 dark:bg-scale-300',
|
||||
'px-8 py-6 h-full hidden lg:flex',
|
||||
].join(' ')}
|
||||
>
|
||||
{PRICING_META[plan.id].preface && (
|
||||
<p className="text-sm text-scale-1200">{PRICING_META[plan.id].preface}</p>
|
||||
)}
|
||||
<ul role="list" className="divide-y dark:divide-scale-400">
|
||||
{PRICING_META[plan.id].features.map((feature) => (
|
||||
<li key={feature} className="flex items-center py-2">
|
||||
<IconCheck
|
||||
className="w-3 h-3 text-brand-900 "
|
||||
aria-hidden="true"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<p className="mb-0 ml-3 text-xs text-scale-1100 dark:text-scale-1200">
|
||||
{feature}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="space-y-2">
|
||||
{PRICING_META[plan.id].additional && (
|
||||
<p className="text-sm text-scale-1200">{PRICING_META[plan.id].additional}</p>
|
||||
)}
|
||||
{PRICING_META[plan.id].scale && (
|
||||
<p className="text-xs text-scale-900">{PRICING_META[plan.id].scale}</p>
|
||||
)}
|
||||
{PRICING_META[plan.id].shutdown && (
|
||||
<p className="text-xs text-scale-900">{PRICING_META[plan.id].shutdown}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-4">
|
||||
{pointers.map((pointer: string, idx: number) => (
|
||||
<li key={`pointer-${idx}`} className="text-sm flex">
|
||||
<div className="w-[15%]">
|
||||
<svg
|
||||
className={`h-5 w-5 text-green-900`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="w-[85%]">
|
||||
<span>{pointer}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<PlanCTAButton plan={plan} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import { Badge, Button, IconArrowLeft, IconHelpCircle, Toggle, Modal } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { post, patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { getURL } from 'lib/helpers'
|
||||
@@ -18,6 +18,7 @@ import { SubscriptionPreview } from '../Billing.types'
|
||||
import UpdateSuccess from '../UpdateSuccess'
|
||||
import { formatComputeSizes } from '../AddOns/AddOns.utils'
|
||||
import { formSubscriptionUpdatePayload } from './ProUpgrade.utils'
|
||||
import BackButton from 'components/ui/BackButton'
|
||||
|
||||
// Do not allow compute size changes for af-south-1
|
||||
|
||||
@@ -36,6 +37,11 @@ const ProUpgrade: FC<Props> = ({
|
||||
isLoadingPaymentMethods,
|
||||
onSelectBack,
|
||||
}) => {
|
||||
/**
|
||||
* Feature flags
|
||||
*/
|
||||
const nativeBilling = useFlag('nativeBilling')
|
||||
|
||||
const { ui } = useStore()
|
||||
|
||||
const { addons } = products
|
||||
@@ -151,39 +157,37 @@ const ProUpgrade: FC<Props> = ({
|
||||
className="w-full flex items-start justify-between"
|
||||
>
|
||||
<>
|
||||
<div className="w-3/5 mt-10">
|
||||
<div className="relative ml-64">
|
||||
<div className="absolute top-[2px] -left-24">
|
||||
<Button type="text" icon={<IconArrowLeft />} onClick={onSelectBack}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
<div className="2xl:min-w-5xl mx-auto mt-10">
|
||||
<div className="relative px-5 space-y-4">
|
||||
<BackButton onClick={() => onSelectBack()} />
|
||||
<div className="space-y-8">
|
||||
<h4 className="text-lg">Change your project's subscription</h4>
|
||||
<h4 className="text-lg text-scale-900">Change your project's subscription</h4>
|
||||
<div
|
||||
className="space-y-8 overflow-scroll pb-8 pr-20"
|
||||
className="space-y-8 overflow-scroll"
|
||||
style={{ height: 'calc(100vh - 6.3rem - 49.5px)' }}
|
||||
>
|
||||
{!isManagingProSubscription ? (
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xl">
|
||||
Welcome to <span className="text-green-1100">Pro</span>
|
||||
<p className="text-sm text-scale-1100">
|
||||
<div className="space-y-2">
|
||||
{!isManagingProSubscription ? (
|
||||
<>
|
||||
<h3 className="text-xl">
|
||||
Welcome to <span className="text-brand-900">Pro</span>
|
||||
</h3>
|
||||
<p className="text-base text-scale-1100">
|
||||
Your new subscription will begin immediately after payment
|
||||
</p>
|
||||
</h3>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xl">
|
||||
Managing your <span className="text-green-1100">Pro</span> plan
|
||||
<p className="text-sm text-scale-1100">
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="text-3xl">
|
||||
Managing your <span className="text-brand-900">Pro</span> plan
|
||||
</h3>
|
||||
<p className="text-base text-scale-1100">
|
||||
Your billing cycle will reset after payment
|
||||
</p>
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-start justify-between">
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-16 border rounded px-6 py-4 bg-panel-body-light dark:bg-panel-body-dark drop-shadow-sm border-panel-border-light border-panel-border-dark">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>Enable spend cap</p>
|
||||
@@ -195,7 +199,7 @@ const ProUpgrade: FC<Props> = ({
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-scale-1100">
|
||||
If disabled, additional resources will be charged on a per-usage basis
|
||||
If enabled, additional resources will not be charged on a per-usage basis
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
@@ -203,7 +207,7 @@ const ProUpgrade: FC<Props> = ({
|
||||
onChange={() => setIsSpendCapEnabled(!isSpendCapEnabled)}
|
||||
/>
|
||||
</div>
|
||||
{projectRegion !== 'af-south-1' && (
|
||||
{nativeBilling && projectRegion !== 'af-south-1' && (
|
||||
<>
|
||||
<Divider light />
|
||||
<ComputeSizeSelection
|
||||
@@ -218,7 +222,7 @@ const ProUpgrade: FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-2/5">
|
||||
<div className="w-[32rem]">
|
||||
<PaymentSummaryPanel
|
||||
isRefreshingPreview={isRefreshingPreview}
|
||||
subscriptionPreview={subscriptionPreview}
|
||||
|
||||
@@ -7,12 +7,11 @@ import { Dictionary } from '@supabase/grid'
|
||||
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { useStore, useFlag, useSubscriptionStats } from 'hooks'
|
||||
import { useStore, useSubscriptionStats } from 'hooks'
|
||||
import CostBreakdownRow from './CostBreakdownRow'
|
||||
import { StripeSubscription } from './Subscription.types'
|
||||
import { deriveFeatureCost, deriveProductCost } from '../PAYGUsage/PAYGUsage.utils'
|
||||
import { chargeableProducts } from '../PAYGUsage/PAYGUsage.constants'
|
||||
import UpgradeButton from './UpgradeButton'
|
||||
|
||||
interface Props {
|
||||
project: any
|
||||
@@ -37,7 +36,7 @@ const Subscription: FC<Props> = ({
|
||||
const { ui } = useStore()
|
||||
const isOrgOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const nativeBilling = useFlag('nativeBilling')
|
||||
// const nativeBilling = useFlag('nativeBilling')
|
||||
const subscriptionStats = useSubscriptionStats()
|
||||
|
||||
const isPayg = subscription?.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG
|
||||
@@ -65,28 +64,20 @@ const Subscription: FC<Props> = ({
|
||||
</p>
|
||||
<h3 className="text-xl mb-0">{subscription?.tier.name ?? '-'}</h3>
|
||||
</div>
|
||||
{nativeBilling ? (
|
||||
<div className="flex flex-col items-end space-y-2">
|
||||
<Button
|
||||
disabled={!isOrgOwner}
|
||||
onClick={() => router.push(`/project/${project.ref}/settings/billing/update`)}
|
||||
type="primary"
|
||||
>
|
||||
Change subscription
|
||||
</Button>
|
||||
{!isOrgOwner && (
|
||||
<p className="text-sm text-scale-1100">
|
||||
Only the organization owner can amend subscriptions
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<UpgradeButton
|
||||
paid={paid}
|
||||
projectRef={project.ref}
|
||||
subscriptionStats={subscriptionStats}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col items-end space-y-2">
|
||||
<Button
|
||||
disabled={!isOrgOwner}
|
||||
onClick={() => router.push(`/project/${project.ref}/settings/billing/update`)}
|
||||
type="primary"
|
||||
>
|
||||
Change subscription
|
||||
</Button>
|
||||
{!isOrgOwner && (
|
||||
<p className="text-sm text-scale-1100">
|
||||
Only the organization owner can amend subscriptions
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{paid && (
|
||||
<div className="px-6 pt-4">
|
||||
|
||||
@@ -11,7 +11,8 @@ import { CancellationReasons } from './SubcriptionCancellation.constants'
|
||||
|
||||
/**
|
||||
* Generates a Stripe Billing Portal link for a project.
|
||||
* To be deprecate after native billing is completely rolled out
|
||||
* This is now deprecated since 180422 since we have native billing
|
||||
* Feel free to delete this file if we'd reckon we no longer need this fallback
|
||||
*
|
||||
* @param {Object} props.projectRef Project Ref
|
||||
* @param {Boolean} props.paid Tier name
|
||||
|
||||
@@ -245,9 +245,7 @@ const LogTable = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={'flex flex-row h-full ' + (!queryType ? 'border-l border-r' : '')}
|
||||
>
|
||||
<div className={`flex flex-row h-full ${!queryType ? 'border-l border-r' : ''}`}>
|
||||
<DataGrid
|
||||
style={{ height: '100%' }}
|
||||
className={`
|
||||
@@ -265,22 +263,11 @@ const LogTable = ({
|
||||
<>
|
||||
<div className="py-4 w-full h-full flex-col space-y-12">
|
||||
<div
|
||||
className={`transition-all
|
||||
duration-500
|
||||
delay-200
|
||||
|
||||
flex
|
||||
flex-col
|
||||
items-center
|
||||
|
||||
gap-6
|
||||
text-center
|
||||
mt-16
|
||||
opacity-100
|
||||
scale-100
|
||||
|
||||
justify-center
|
||||
`}
|
||||
className={[
|
||||
'transition-all duration-500 delay-200',
|
||||
'flex flex-col items-center gap-6 text-center',
|
||||
'mt-16 opacity-100 scale-100 justify-center',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="relative border border-scale-600 border-dashed dark:border-scale-400 w-32 h-4 rounded px-2 flex items-center"></div>
|
||||
|
||||
@@ -15,32 +15,34 @@ const BillingLayout: FC<Props> = ({ children }) => {
|
||||
<BaseLayout hideHeader hideIconBar>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2 border-b dark:border-dark flex items-center space-x-3">
|
||||
<Link href={`/project/${ui.selectedProject?.ref}/settings/billing`}>
|
||||
<a>
|
||||
<Button type="text" size="tiny">
|
||||
<IconX size={22} strokeWidth={1.5} />
|
||||
</Button>
|
||||
<div className="py-4 border-b dark:border-dark flex items-center space-x-3 px-5">
|
||||
<Link href={`/project/${ui.selectedProject?.ref}/settings/billing`} passHref>
|
||||
<a className="transition-colors text-scale-900 hover:text-scale-1200">
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm text-scale-1100">{ui.selectedOrganization?.name}</p>
|
||||
<span className="text-scale-800">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
shapeRendering="geometricPrecision"
|
||||
>
|
||||
<path d="M16 3.549L7.12 20.600"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<p className="text-sm text-scale-1100">{ui.selectedProject?.name}</p>
|
||||
<div className="flex items-center space-x-6">
|
||||
<h1 className="text-base text-scale-1200">Customize your plan</h1>
|
||||
<div className="h-6 w-px bg-scale-600"></div>
|
||||
<div className="flex space-x-3 items-center">
|
||||
<p className="text-sm text-scale-1100">{ui.selectedOrganization?.name}</p>
|
||||
<span className="text-scale-800">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
shapeRendering="geometricPrecision"
|
||||
>
|
||||
<path d="M16 3.549L7.12 20.600"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<p className="text-sm text-scale-1100">{ui.selectedProject?.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Button, IconArrowLeft } from '@supabase/ui'
|
||||
|
||||
export function BackButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<div className="2xl:absolute 2xl:top-[2px] -ml-2 2xl:-left-24">
|
||||
<Button type="text" icon={<IconArrowLeft />} onClick={onClick}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BackButton
|
||||
@@ -105,10 +105,10 @@ export const PROJECT_STATUS = {
|
||||
RESTORING: 'RESTORING',
|
||||
}
|
||||
|
||||
export const STRIPE_PRODUCT_IDS = {
|
||||
FREE: process?.env?.NEXT_PUBLIC_STRIPE_FREE_TIER_ID,
|
||||
PRO: process?.env?.NEXT_PUBLIC_STRIPE_PRO_TIER_ID,
|
||||
PAYG: process?.env?.NEXT_PUBLIC_STRIPE_PAYG_TIER_ID,
|
||||
export const STRIPE_PRODUCT_IDS: { [x: string]: string } = {
|
||||
FREE: process?.env?.NEXT_PUBLIC_STRIPE_FREE_TIER_ID || 'prod_Ip4vqwv3EJ7Mi0',
|
||||
PRO: process?.env?.NEXT_PUBLIC_STRIPE_PRO_TIER_ID || 'prod_IsRLOp58Z7V4XN',
|
||||
PAYG: process?.env?.NEXT_PUBLIC_STRIPE_PAYG_TIER_ID || 'prod_JlTbw91xcM6NY4',
|
||||
}
|
||||
|
||||
export const DEFAULT_MINIMUM_PASSWORD_STRENGTH = 4
|
||||
|
||||
@@ -122,14 +122,12 @@ const EmptyFunctions = () => {
|
||||
<div className="grid py-12 lg:grid-cols-12 gap-y-12 lg:gap-x-16 max-w-7xl">
|
||||
<div className="col-span-5 space-y-4">
|
||||
<p className="max-w-lg text-base text-scale-1200">
|
||||
Scalable pay-as-you-go functions as a service (FaaS) to run your code with zero server
|
||||
management.
|
||||
Scalable functions to run your code with no server management.
|
||||
</p>
|
||||
<p className="max-w-lg text-sm text-scale-1100">
|
||||
No servers to provision, manage, or upgrade Automatically scale based on the load
|
||||
Integrated monitoring, logging, and debugging capability Built-in security at role and
|
||||
per function level based on the principle of least privilege Key networking capabilities
|
||||
for hybrid and multi-cloud scenarios
|
||||
Edge Functions are server-side Typescript functions, distributed globally at the edge -
|
||||
close to your users. They can be used for listening to webhooks or integrating your
|
||||
Supabase project with third-parties.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Link passHref href="https://supabase.com/docs/guides/functions">
|
||||
|
||||
@@ -106,7 +106,7 @@ const BillingUpdate: NextPage = () => {
|
||||
|
||||
return (
|
||||
<BillingLayout>
|
||||
<div className="mx-auto max-w-5xl my-10">
|
||||
<div className="mx-auto max-w-6xl my-10 px-6">
|
||||
<PlanSelection
|
||||
visible={!selectedPlan || (selectedPlan && showConfirmDowngrade)}
|
||||
tiers={products?.tiers ?? []}
|
||||
|
||||
@@ -465,7 +465,7 @@ const GeneralSettings: FC<any> = ({ projectRef }) => {
|
||||
disabled
|
||||
value={
|
||||
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD]` +
|
||||
`host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
|
||||
` host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
|
||||
` database=${connectionInfo.db_name}`
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import { AllureReporterApi, jasmine_, registerAllureReporter } from 'jest-allure2-adapter'
|
||||
import { ContentType, Severity } from 'allure-js-commons'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type TestDecorator = (
|
||||
target: object,
|
||||
property: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) => PropertyDescriptor
|
||||
export class JasmineAllureReporter implements jasmine_.CustomReporter {
|
||||
allure: AllureReporterApi
|
||||
|
||||
constructor(allure: AllureReporterApi) {
|
||||
this.allure = allure
|
||||
}
|
||||
|
||||
suiteStarted(suite?: jasmine_.CustomReporterResult): void {
|
||||
this.allure.startGroup(suite.description)
|
||||
// some actions here on suite started
|
||||
}
|
||||
|
||||
suiteDone(): void {
|
||||
// some actions here on suite end
|
||||
this.allure.endGroup()
|
||||
}
|
||||
|
||||
specStarted(spec: jasmine_.CustomReporterResult): void {
|
||||
this.allure.startTest(spec)
|
||||
// some actions here on test started
|
||||
}
|
||||
|
||||
specDone(spec: jasmine_.CustomReporterResult): void {
|
||||
// some actions here on spec end
|
||||
this.allure.endTest(spec)
|
||||
}
|
||||
}
|
||||
|
||||
registerAllureReporter(undefined, (allure) => new JasmineAllureReporter(allure))
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
reporter: AllureReporterApi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAllure(): AllureReporterApi {
|
||||
// @ts-ignore - we are checking if reporter is defined
|
||||
if (!global.reporter) {
|
||||
throw new Error('Unable to find Allure implementation')
|
||||
}
|
||||
// @ts-ignore - we know that reporter is an AllureReporterApi
|
||||
return global.reporter
|
||||
}
|
||||
|
||||
export function step<T>(nameFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return (target: object, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
const original: object = descriptor.value
|
||||
let callable: (args: T) => void = () => {
|
||||
/* */
|
||||
}
|
||||
|
||||
if (typeof original === 'function') {
|
||||
descriptor.value = function (...args: [T]) {
|
||||
try {
|
||||
const value: string = typeof nameFn === 'function' ? nameFn.apply(this, args) : nameFn
|
||||
callable = () => getAllure().step(value, () => original.apply(this, args))
|
||||
// tslint:disable-next-line:no-console
|
||||
console.info(`Step: ${value || nameFn}`)
|
||||
} catch (e) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(`[ERROR] Failed to apply decorator: ${e}`)
|
||||
}
|
||||
return callable.apply(this, args)
|
||||
}
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
export function attachment<T>(name: string, type: ContentType) {
|
||||
return (
|
||||
_target: object,
|
||||
_propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
): PropertyDescriptor => {
|
||||
const original: object = descriptor.value
|
||||
let callable: (args: T) => void = () => {
|
||||
/* */
|
||||
}
|
||||
|
||||
if (typeof original === 'function') {
|
||||
descriptor.value = async function (...args: [T]) {
|
||||
try {
|
||||
const content: Buffer | string = await original.apply(this, args)
|
||||
callable = () =>
|
||||
getAllure().step(name, () => {
|
||||
getAllure().attachment(type.toString(), content, type)
|
||||
})
|
||||
} catch (e) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(`[ERROR] Failed to apply decorator: ${e}`)
|
||||
}
|
||||
return callable.apply(this, args)
|
||||
}
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
export function attach(name: string, content: string | Buffer, type: ContentType): void {
|
||||
getAllure().step(name, () => {
|
||||
getAllure().attachment(type.toString(), content, type)
|
||||
})
|
||||
}
|
||||
|
||||
export function log(name: string, description?: string): void {
|
||||
console.info(description ? `${name}: ${description}` : name)
|
||||
getAllure().step(name, () => {
|
||||
if (description) {
|
||||
getAllure().step(description, () => {
|
||||
/* */
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function feature<T>(featureFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return processDecorator(featureFn, (name) => getAllure().feature(name))
|
||||
}
|
||||
|
||||
export function story<T>(storyFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return processDecorator(storyFn, (name) => getAllure().story(name))
|
||||
}
|
||||
|
||||
export function severity<T>(
|
||||
severityFn: Severity | string | ((arg: T) => string | Severity)
|
||||
): TestDecorator {
|
||||
return processDecorator(severityFn, (name: Severity) => getAllure().severity(name))
|
||||
}
|
||||
|
||||
export function tag<T>(tagFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return processDecorator(tagFn, (name) => getAllure().tag(name))
|
||||
}
|
||||
|
||||
export function owner<T>(ownerFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return processDecorator(ownerFn, (name) => getAllure().owner(name))
|
||||
}
|
||||
|
||||
export function description<T>(descriptionFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return processDecorator(descriptionFn, (text) => getAllure().description(text))
|
||||
}
|
||||
|
||||
function processDecorator<T>(
|
||||
parameterFn: string | ((arg: T) => string),
|
||||
reporterFn: (arg: string) => void
|
||||
): TestDecorator {
|
||||
return (target: object, property: string, descriptor: PropertyDescriptor) => {
|
||||
return processDescriptor(parameterFn, reporterFn, descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
function processDescriptor<T>(
|
||||
parameterFn: string | ((arg: T) => string),
|
||||
reporterFn: (arg: string) => void,
|
||||
descriptor: PropertyDescriptor
|
||||
): PropertyDescriptor {
|
||||
const original: object = descriptor.value
|
||||
if (typeof original === 'function') {
|
||||
descriptor.value = function (...args: [T]) {
|
||||
try {
|
||||
const value: string =
|
||||
typeof parameterFn === 'function' ? parameterFn.apply(this, args) : parameterFn
|
||||
reporterFn(value)
|
||||
} catch (e) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(`[ERROR] Failed to apply decorator: ${e}`)
|
||||
}
|
||||
return original.apply(this, args)
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop of Object.keys(original)) {
|
||||
if (original.hasOwnProperty(prop) && prop.startsWith('__testdeck_')) {
|
||||
// @ts-ignore - we know that prop exists
|
||||
descriptor.value[prop] = original[prop]
|
||||
}
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
process.env.SUPABASE_DB_HOST = 'localhost'
|
||||
process.env.SUPABASE_DB_PORT = '5432'
|
||||
process.env.SUPABASE_DB_PASS = 'your-super-secret-and-long-postgres-password'
|
||||
process.env.SUPABASE_GOTRUE = 'http://localhost:8000/auth/v1'
|
||||
process.env.SUPABASE_URL = 'http://localhost:8000'
|
||||
process.env.SUPABASE_KEY_ANON =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'
|
||||
process.env.SUPABASE_KEY_ADMIN =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q'
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q'
|
||||
@@ -0,0 +1,4 @@
|
||||
api
|
||||
node_modules
|
||||
package-lock.json
|
||||
docker*
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
These tests can be run with Docker.
|
||||
|
||||
We can use either [allure-commandline](https://github.com/allure-framework/allure2) as it is open source and <https://github.com/marketplace/actions/allure-report-with-history> as an github action to host reports with history on GitHub Pages.
|
||||
|
||||
Or use their paid version if we want a bit more groove and possibility to manage test cases not only as code but also manually: <https://qameta.io>. It can be self-hosted or cloud. Not sure that we need it now, but we can always migrate seamlessly to it if we would need it.
|
||||
|
||||
### Steps
|
||||
|
||||
In the parent folder:
|
||||
@@ -11,4 +15,4 @@ In the parent folder:
|
||||
|
||||
### Clean up
|
||||
|
||||
- `npm run docker:remove`
|
||||
- `npm run docker:remove`
|
||||
@@ -0,0 +1,331 @@
|
||||
import { suite, test } from '@testdeck/jest'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
import { ApiError, Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js'
|
||||
|
||||
import { FEATURE } from '../templates/enums'
|
||||
import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter'
|
||||
import { Hooks } from './hooks'
|
||||
|
||||
@suite('authentication')
|
||||
class Authentication extends Hooks {
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When user sign up then corresponding user in auth schema should be created')
|
||||
@test
|
||||
async 'signup should create user'() {
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
|
||||
const fakeUser = {
|
||||
email: faker.internet.exampleEmail(),
|
||||
password: faker.internet.password(),
|
||||
username: faker.internet.userName(),
|
||||
}
|
||||
const { user, session, error: signUpError } = await this.signUp(supabase, fakeUser)
|
||||
|
||||
expect(signUpError).toBeNull()
|
||||
expect(user).toBeDefined()
|
||||
expect(user.email).toEqual(fakeUser.email.toLowerCase())
|
||||
expect(session).toBeDefined()
|
||||
|
||||
const [createdUser] = await this.selectUser(user)
|
||||
expect(createdUser.email).toEqual(fakeUser.email.toLowerCase())
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When user sign up then he should not be logged in until he confirms his email')
|
||||
@test
|
||||
async 'new users'() {
|
||||
// sign up user
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
|
||||
const fakeUser = {
|
||||
email: faker.internet.exampleEmail(),
|
||||
password: faker.internet.password(),
|
||||
username: faker.internet.userName(),
|
||||
}
|
||||
const {
|
||||
user,
|
||||
session: emptySession,
|
||||
error: signUpError,
|
||||
} = await this.signUp(supabase, fakeUser)
|
||||
|
||||
expect(signUpError).toBeNull()
|
||||
expect(user).toBeDefined()
|
||||
expect(user.email).toEqual(fakeUser.email.toLowerCase())
|
||||
expect(emptySession).toBeNull()
|
||||
|
||||
// check if user is not signed in cause he has not confirmed his email
|
||||
const { error: errorInsertFailed } = await this.insertProfile(supabase, user, fakeUser)
|
||||
log('check if insert failed caused user has not confirmed email')
|
||||
expect(errorInsertFailed).not.toBeNull()
|
||||
|
||||
// confirm user email and sign in
|
||||
const [{ confirmation_token: token }] = await this.getConfirmationToken(user)
|
||||
const verifyResponse = await this.verify(token)
|
||||
expect(verifyResponse.ok).toBeTruthy()
|
||||
|
||||
const { session, error: signInError } = await supabase.auth.signIn({
|
||||
email: fakeUser.email,
|
||||
password: fakeUser.password,
|
||||
})
|
||||
expect(signInError).toBeNull()
|
||||
expect(session).toBeDefined()
|
||||
|
||||
// check if user is signed in
|
||||
const { data: profile, error: errorInsert } = await this.insertProfile(supabase, user, fakeUser)
|
||||
expect(errorInsert).toBeNull()
|
||||
expect(profile.username).toMatch(fakeUser.username)
|
||||
|
||||
const { data: profileGot } = await this.getUserProfile(supabase)
|
||||
expect(profileGot.username).toMatch(profile.username)
|
||||
|
||||
// check if user is able to sign out
|
||||
const { error } = await this.signOut(supabase)
|
||||
expect(error).toBeNull()
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When user sign up with phone then he should be logged in')
|
||||
@test
|
||||
async 'new users by phone'() {
|
||||
// sign up user
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
|
||||
const fakeUser = {
|
||||
password: faker.internet.password(),
|
||||
username: faker.internet.userName(),
|
||||
phone: faker.phone.phoneNumber('!#!##!######'),
|
||||
}
|
||||
const { user, session, error: signUpError } = await this.signUpByPhone(supabase, fakeUser)
|
||||
|
||||
expect(signUpError).toBeNull()
|
||||
expect(user).toBeDefined()
|
||||
expect(user.phone).toEqual(fakeUser.phone)
|
||||
expect(session).toBeDefined()
|
||||
|
||||
// check if user is signed in
|
||||
const { data: profile, error: errorInsert } = await this.insertProfile(supabase, user, fakeUser)
|
||||
expect(errorInsert).toBeNull()
|
||||
expect(profile.username).toMatch(fakeUser.username)
|
||||
|
||||
const { data: profileGot } = await this.getUserProfile(supabase)
|
||||
expect(profileGot.username).toMatch(profile.username)
|
||||
|
||||
// check if user is able to sign out
|
||||
const { error } = await this.signOut(supabase)
|
||||
expect(error).toBeNull()
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When user is already signed up then he should be able to log in')
|
||||
@test
|
||||
async 'existing users'() {
|
||||
// create user
|
||||
const fakeUser = await this.createUser()
|
||||
|
||||
// sign in as user
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
const {
|
||||
session,
|
||||
user,
|
||||
error: signInError,
|
||||
} = await supabase.auth.signIn({
|
||||
email: fakeUser.email,
|
||||
password: fakeUser.password,
|
||||
})
|
||||
|
||||
expect(signInError).toBeNull()
|
||||
expect(session).toBeDefined()
|
||||
expect(user).toBeDefined()
|
||||
expect(user.email).toEqual(fakeUser.email.toLowerCase())
|
||||
|
||||
// check if user is signed in correctly and rls is working
|
||||
const { data: profileInserted, error: errorInsert } = await this.insertProfile(
|
||||
supabase,
|
||||
user,
|
||||
fakeUser
|
||||
)
|
||||
expect(errorInsert).toBeNull()
|
||||
expect(profileInserted.username).toMatch(fakeUser.username)
|
||||
|
||||
const { data: profileGot } = await this.getUserProfile(supabase)
|
||||
expect(profileGot.username).toMatch(profileInserted.username)
|
||||
|
||||
// check if user is able to sign out
|
||||
const { error } = await this.signOut(supabase)
|
||||
expect(error).toBeNull()
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When user is signed in then he should be able to get his info and metadata')
|
||||
@test
|
||||
async 'get user'() {
|
||||
// create user
|
||||
const username = faker.internet.userName()
|
||||
const date = faker.date.recent().toUTCString()
|
||||
const fakeUser = await this.createUser({
|
||||
username: username,
|
||||
date: date,
|
||||
})
|
||||
|
||||
// sign in as user
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
await supabase.auth.signIn({
|
||||
email: fakeUser.email,
|
||||
password: fakeUser.password,
|
||||
})
|
||||
|
||||
// get signed in user data
|
||||
const user = this.getUser(supabase)
|
||||
|
||||
log('Check if user is signed in correctly and can get his data')
|
||||
expect(user).toBeDefined()
|
||||
expect(user.email).toEqual(fakeUser.email.toLowerCase())
|
||||
expect(user.role).toEqual('authenticated')
|
||||
expect(user.aud).toEqual('authenticated')
|
||||
// verify if metadata is correctly set after sing up
|
||||
expect(user.user_metadata).toEqual({
|
||||
username: username,
|
||||
date: date,
|
||||
})
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When user is signed in then he should be able update himself in auth schema')
|
||||
@test.skip
|
||||
async 'update user'() {
|
||||
// create user
|
||||
const fakeUser = await this.createUser()
|
||||
|
||||
// sign in as user
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
await supabase.auth.signIn({
|
||||
email: fakeUser.email,
|
||||
password: fakeUser.password,
|
||||
})
|
||||
|
||||
// get signed in user data
|
||||
const user = this.getUser(supabase)
|
||||
|
||||
// update user
|
||||
// todo update params
|
||||
const {
|
||||
user: updUser,
|
||||
data: updUserData,
|
||||
error: updUserError,
|
||||
} = await this.updateUser(supabase, {})
|
||||
|
||||
// todo check if returned user is updated
|
||||
|
||||
const updatedUser = this.getUser(supabase)
|
||||
|
||||
// todo check if user is updated
|
||||
|
||||
const userFromApi = this.getUserFromApi(supabase)
|
||||
|
||||
// todo check if user is updated on backend
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When user changes session then he still should be correctly logined')
|
||||
@test.skip
|
||||
async 'set session'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When user changes auth then all new requests should have new JWT')
|
||||
@test.skip
|
||||
async 'set auth'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When user refreshes session then user and session have to be updated')
|
||||
@test.skip
|
||||
async 'refresh session'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When user subscribes on auth changes then user has to receive auth updates')
|
||||
@test.skip
|
||||
async 'on auth state changed'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
// steps
|
||||
|
||||
@step('I sign up with a valid email and password')
|
||||
private async signUpByPhone(
|
||||
supabase: SupabaseClient,
|
||||
{
|
||||
phone = faker.phone.phoneNumber(),
|
||||
password = faker.internet.password(),
|
||||
}: {
|
||||
phone?: string
|
||||
password?: string
|
||||
} = {},
|
||||
options: {
|
||||
redirectTo?: string
|
||||
data?: object
|
||||
} = {}
|
||||
): Promise<{
|
||||
user: User
|
||||
session: Session
|
||||
error: ApiError
|
||||
}> {
|
||||
return supabase.auth.signUp(
|
||||
{
|
||||
phone: phone,
|
||||
password: password,
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
@step('Check if I am logged in by checking if I can insert my profile')
|
||||
private async insertProfile(
|
||||
supabase: SupabaseClient,
|
||||
user: User,
|
||||
fakeUser: {
|
||||
username: string
|
||||
}
|
||||
): Promise<{ data: any; error: any }> {
|
||||
return supabase
|
||||
.from('profiles')
|
||||
.insert({
|
||||
id: user.id,
|
||||
username: fakeUser.username,
|
||||
})
|
||||
.single()
|
||||
}
|
||||
|
||||
@step('Check if I am logged in by checking if I can get my profile')
|
||||
private async getUserProfile(supabase: SupabaseClient): Promise<{ data: any }> {
|
||||
return supabase.from('profiles').select().maybeSingle()
|
||||
}
|
||||
|
||||
@step('Update user info')
|
||||
private async updateUser(
|
||||
supabase: SupabaseClient,
|
||||
attr: UserAttributes
|
||||
): Promise<{
|
||||
data: User
|
||||
user: User
|
||||
error: ApiError
|
||||
}> {
|
||||
return supabase.auth.update(attr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { params, suite, test } from '@testdeck/jest'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
import { ApiError, Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js'
|
||||
|
||||
import { FEATURE } from '../templates/enums'
|
||||
import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter'
|
||||
import { Hooks } from './hooks'
|
||||
|
||||
@suite('authentication API')
|
||||
class AuthenticationAPI extends Hooks {
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you create user then it has to be in auth db schema')
|
||||
@test.skip
|
||||
async 'create user'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you create user then he can sign in')
|
||||
@test.skip
|
||||
async 'create user can login'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description('When you try to create user with anon key then you should get error')
|
||||
@test.skip
|
||||
async 'create user with anon key'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you list users then you should get all users')
|
||||
@test.skip
|
||||
async 'list users'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description('When you try to list user with anon key then you should get error')
|
||||
@test.skip
|
||||
async 'list users with anon key'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you sign up user with email then he should be able to login')
|
||||
@test.skip
|
||||
async 'sign up with email'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you sign in user with email then he should be able to update his profile')
|
||||
@test.skip
|
||||
async 'sign in with email'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you sign up user with phone then he should be able to login')
|
||||
@test.skip
|
||||
async 'sign up with phone'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you sign in user with phone then he should be able to update his profile')
|
||||
@test.skip
|
||||
async 'sign in with phone'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you send magic link then email should be sent to user')
|
||||
@test.skip
|
||||
async 'send magic link'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you send invite then corresponding email should be sent to user')
|
||||
@test.skip
|
||||
async 'send invite link'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you send reset password then corresponding email should be sent to user')
|
||||
@test.skip
|
||||
async 'send reset password'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you sign out user then current session has to be removed')
|
||||
@test.skip
|
||||
async 'sign out'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you get provider url then corresponding auth provider url should be returned')
|
||||
@params.skip({ provider: 'google', options: {}, expectedURL: 'todo' })
|
||||
@params.skip({
|
||||
provider: 'google',
|
||||
options: { redirectTo: 'todo', scopes: 'todo' },
|
||||
expectedURL: 'todo',
|
||||
})
|
||||
@params.skip({ provider: 'twitter', options: {}, expectedURL: 'todo' })
|
||||
// ...
|
||||
async 'get url for provider'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you get user then currently logined user date has to be returned')
|
||||
@test.skip
|
||||
async 'get user'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you update user then currently logined user date has to be updated')
|
||||
@test.skip
|
||||
async 'update user'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you delete user then this user has to be deleted and unable to login')
|
||||
@test.skip
|
||||
async 'delete user'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description('When you delete user with anon key you have to receive an error')
|
||||
@test.skip
|
||||
async 'delete user with anon key'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you change auth cookie then all new requests should have new JWT')
|
||||
@test.skip
|
||||
async 'set auth cookie'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you refreshes session then user and session have to be updated')
|
||||
@test.skip
|
||||
async 'refresh session'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you get user by cookie then the corresponding user data has to be returned')
|
||||
@test.skip
|
||||
async 'get user by cookie'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you generate link then the right link has to be returned')
|
||||
@params.skip({ type: 'signup', options: {}, email: 'todo' })
|
||||
@params.skip({
|
||||
type: 'signup',
|
||||
options: {
|
||||
redirectTo: 'todo',
|
||||
password: 'todo',
|
||||
data: {
|
||||
/* todo */
|
||||
},
|
||||
},
|
||||
email: 'todo',
|
||||
})
|
||||
@params.skip({ type: 'magiclink', options: {}, email: 'todo' })
|
||||
// ...
|
||||
async 'generate link'() {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { params, suite, test } from '@testdeck/jest'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
import { ApiError, Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js'
|
||||
|
||||
import { FEATURE } from '../templates/enums'
|
||||
import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter'
|
||||
import { Hooks } from './hooks'
|
||||
|
||||
@suite('functions')
|
||||
class Functions extends Hooks {
|
||||
@feature(FEATURE.STORAGE)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you get functions client then you are able to set auth')
|
||||
@test.skip
|
||||
async 'set auth'() {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import postgres from 'postgres'
|
||||
import crossFetch from 'cross-fetch'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import {
|
||||
ApiError,
|
||||
createClient,
|
||||
Session,
|
||||
SupabaseClient,
|
||||
SupabaseClientOptions,
|
||||
User,
|
||||
} from '@supabase/supabase-js'
|
||||
|
||||
import { JasmineAllureReporter, step } from '../../.jest/jest-custom-reporter'
|
||||
|
||||
export abstract class Hooks {
|
||||
static sql = postgres({
|
||||
host: process.env.SUPABASE_DB_HOST,
|
||||
port: parseInt(process.env.SUPABASE_DB_PORT),
|
||||
database: 'postgres',
|
||||
username: 'postgres',
|
||||
password: process.env.SUPABASE_DB_PASS,
|
||||
})
|
||||
|
||||
@step('terminate sql connection')
|
||||
static async after(): Promise<any> {
|
||||
try {
|
||||
Hooks.sql.end({ timeout: 100 })
|
||||
return Promise.resolve(null)
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
@step('Create Supabase client')
|
||||
createSupaClient(url: string, key: string, options: SupabaseClientOptions = {}): SupabaseClient {
|
||||
return createClient(url, key, options)
|
||||
}
|
||||
|
||||
@step('Create a valid user')
|
||||
async createUser(data: object = {}): Promise<{
|
||||
email: string
|
||||
password: string
|
||||
username: string
|
||||
}> {
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
|
||||
const fakeUser = {
|
||||
email: faker.internet.exampleEmail(),
|
||||
password: faker.internet.password(),
|
||||
username: faker.internet.userName(),
|
||||
}
|
||||
const { user, error: signUpError } = await this.signUp(supabase, fakeUser, {
|
||||
data: data,
|
||||
})
|
||||
expect(signUpError).toBeNull()
|
||||
|
||||
const [{ confirmation_token: token }] = await this.getConfirmationToken(user)
|
||||
|
||||
await this.verify(token)
|
||||
|
||||
return fakeUser
|
||||
}
|
||||
|
||||
@step((token: string) => `verify with token ${token}`)
|
||||
async verify(token: string): Promise<Response> {
|
||||
return crossFetch(`${process.env.SUPABASE_GOTRUE}/verify`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
type: 'signup',
|
||||
token: token,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@step((user: User) => `get confirmation token for user ${user.id}`)
|
||||
async getConfirmationToken(user: User): Promise<[{ confirmation_token: any }]> {
|
||||
return Hooks.sql`
|
||||
select confirmation_token
|
||||
from auth.users
|
||||
where id = ${user.id}
|
||||
`
|
||||
}
|
||||
|
||||
@step('I sign up with a valid email and password')
|
||||
async signUp(
|
||||
supabase: SupabaseClient,
|
||||
{
|
||||
email = faker.internet.exampleEmail(),
|
||||
password = faker.internet.password(),
|
||||
}: {
|
||||
email?: string
|
||||
password?: string
|
||||
} = {},
|
||||
options: {
|
||||
redirectTo?: string
|
||||
data?: object
|
||||
} = {}
|
||||
): Promise<{
|
||||
user: User
|
||||
session: Session
|
||||
error: ApiError
|
||||
}> {
|
||||
return supabase.auth.signUp(
|
||||
{
|
||||
email: email,
|
||||
password: password,
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
@step('Check if I am being able to log out')
|
||||
async signOut(supabase: SupabaseClient): Promise<{ error: any }> {
|
||||
return supabase.auth.signOut()
|
||||
}
|
||||
|
||||
@step('Get user data, if there is a logged in user')
|
||||
getUser(supabase: SupabaseClient) {
|
||||
return supabase.auth.user()
|
||||
}
|
||||
|
||||
@step('Get user data, if there is a logged in user')
|
||||
getUserFromApi(supabase: SupabaseClient) {
|
||||
return supabase.auth.api.getUser(supabase.auth.session().access_token)
|
||||
}
|
||||
|
||||
@step((user: User) => `Get user by ID (${user.id}) from Supabase auth schema`)
|
||||
async selectUser(user: User): Promise<[{ email: string }]> {
|
||||
return Hooks.sql`
|
||||
select
|
||||
email
|
||||
from auth.users
|
||||
where
|
||||
id = ${user.id}
|
||||
`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { params, suite, test } from '@testdeck/jest'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
import { ApiError, Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js'
|
||||
|
||||
import { FEATURE } from '../templates/enums'
|
||||
import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter'
|
||||
import { Hooks } from './hooks'
|
||||
|
||||
@suite('realtime')
|
||||
class Realtime extends Hooks {
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When you call "on" table then connected realtime client should be returned')
|
||||
@test.skip
|
||||
async 'realtime connect'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you call "on" table but not subscribe then no events have to be returned')
|
||||
@test.skip
|
||||
async 'no event updates until subscribe'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description(
|
||||
'When you create 2 subs (1 subscribed and 1 not yet) then both should be returned on get subs'
|
||||
)
|
||||
@test.skip
|
||||
async 'get subscriptions'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you subscribe on table then corresponding events have to be returned')
|
||||
@params.skip({ event: 'INSERT', returnedTypes: ['insert'] })
|
||||
@params.skip({ event: 'UPDATE', returnedTypes: ['update'] })
|
||||
@params.skip({ event: 'DELETE', returnedTypes: ['delete'] })
|
||||
@params.skip({ event: '*', returnedTypes: ['insert', 'update', 'delete'] })
|
||||
async 'subscribe on table'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.NORMAL)
|
||||
@description(
|
||||
'When you create multiple subscriptions then corresponding events have to be returned'
|
||||
)
|
||||
@params.skip({
|
||||
events: [
|
||||
{ event: 'INSERT', table: 'rt_table1' },
|
||||
{ event: '*', table: 'rt_table1' },
|
||||
],
|
||||
returnedIds: [[], []],
|
||||
})
|
||||
@params.skip({
|
||||
events: [
|
||||
{ event: 'UPDATE', table: 'rt_table1' },
|
||||
{ event: 'DELETE', table: 'rt_table1' },
|
||||
],
|
||||
returnedIds: [[], []],
|
||||
})
|
||||
@params.skip({
|
||||
events: [
|
||||
{ event: 'INSERT', table: 'rt_table1' },
|
||||
{ event: 'INSERT', table: 'rt_table2' },
|
||||
],
|
||||
returnedIds: [[], []],
|
||||
})
|
||||
async 'multiple subscriptions'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you subscribe on table with RLS then corresponding events have to be returned')
|
||||
@params.skip({ event: 'INSERT', returnedTypes: ['insert'] })
|
||||
@params.skip({ event: 'UPDATE', returnedTypes: ['update'] })
|
||||
@params.skip({ event: 'DELETE', returnedTypes: ['delete'] })
|
||||
@params.skip({ event: '*', returnedTypes: ['insert', 'update', 'delete'] })
|
||||
async 'subscribe on table with RLS'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description('When you subscribe on table with RLS then only allowed events have to be returned')
|
||||
@params.skip({ event: 'INSERT', returnedIds: [] })
|
||||
@params.skip({ event: 'UPDATE', returnedIds: [] })
|
||||
@params.skip({ event: 'DELETE', returnedIds: [] })
|
||||
@params.skip({ event: '*', returnedIds: [] })
|
||||
async 'subscribe on table with RLS policies'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description(
|
||||
'When you subscribe on table with 2 clients then they have to receive different events'
|
||||
)
|
||||
@test.skip
|
||||
async 'subscribe from 2 clients'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.NORMAL)
|
||||
@description(
|
||||
'When you subscribe on table with RLS policies then service key subscription has to bypass RLS'
|
||||
)
|
||||
@params.skip({ event: 'INSERT', returnedTypes: ['insert'] })
|
||||
@params.skip({ event: 'UPDATE', returnedTypes: ['update'] })
|
||||
@params.skip({ event: 'DELETE', returnedTypes: ['delete'] })
|
||||
@params.skip({ event: '*', returnedTypes: ['insert', 'update', 'delete'] })
|
||||
async 'subscribe on RLS table with service key'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description('When you unsubscribe from table then no events have to be returned')
|
||||
@test.skip
|
||||
async 'unsubscribe from table'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description('When you remove one subscription then only events from another have to be returned')
|
||||
@test.skip
|
||||
async 'remove one subscription'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description('When you remove all subscription then no events have to be returned')
|
||||
@test.skip
|
||||
async 'remove all subscriptions'() {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { params, suite, test } from '@testdeck/jest'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
import { ApiError, Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js'
|
||||
|
||||
import { FEATURE } from '../templates/enums'
|
||||
import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter'
|
||||
import { Hooks } from './hooks'
|
||||
|
||||
@suite('rpc')
|
||||
class Procedures extends Hooks {
|
||||
@feature(FEATURE.STORAGE)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When you call rpc then you are able to receive its result')
|
||||
@test.skip
|
||||
async 'call rpc'() {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { params, suite, test } from '@testdeck/jest'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
import { ApiError, Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js'
|
||||
|
||||
import { FEATURE } from '../templates/enums'
|
||||
import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter'
|
||||
import { Hooks } from './hooks'
|
||||
|
||||
@suite('storage')
|
||||
class Storage extends Hooks {
|
||||
@feature(FEATURE.STORAGE)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When you create public bucket then it has to be available')
|
||||
@test.skip
|
||||
async 'create public bucket'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.STORAGE)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When you create private bucket then it has to be available')
|
||||
@test.skip
|
||||
async 'create private bucket'() {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import allure
|
||||
|
||||
@allure.severity(allure.severity_level.BLOCKER)
|
||||
@allure.suite("authentication")
|
||||
@allure.feature("authentication")
|
||||
class TestAuthentication(object):
|
||||
@allure.description("""When user sign up then he should be logged in""")
|
||||
def test_new_users(self):
|
||||
self.create_supabase_anonymous_client()
|
||||
self.sign_up_valid("email", "password")
|
||||
self.check_logged_in()
|
||||
self.check_log_out()
|
||||
|
||||
def test_existing_users(self):
|
||||
""" When user is already signed up then he should be able to logged in """
|
||||
self.create_valid_user()
|
||||
self.sign_in_valid("email", "password")
|
||||
self.check_logged_in()
|
||||
self.check_log_out()
|
||||
|
||||
@allure.step("Create Supabase anonymous client")
|
||||
def create_supabase_anonymous_client(self):
|
||||
pass
|
||||
|
||||
@allure.step("I sign up with a valid {email} and {password}")
|
||||
def sign_up_valid(self, email, password="password"):
|
||||
pass
|
||||
|
||||
@allure.step("Create a valid user")
|
||||
def create_valid_user(self):
|
||||
pass
|
||||
|
||||
@allure.step("I sign in with a valid {email} and {password}")
|
||||
def sign_in_valid(self, email, password="password"):
|
||||
pass
|
||||
|
||||
@allure.step("Check if I am logged in")
|
||||
def check_logged_in(self):
|
||||
pass
|
||||
|
||||
@allure.step("Check if I am being able to log out")
|
||||
def check_log_out(self):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
import { FEATURE } from './enums'
|
||||
|
||||
describe('authentication', () => {
|
||||
beforeEach(() => {
|
||||
reporter.feature(FEATURE.AUTHENTICATION)
|
||||
reporter.severity(Severity.BLOCKER)
|
||||
})
|
||||
|
||||
test('New users', () => {
|
||||
reporter.description('When user sign up then he should be logged in')
|
||||
|
||||
reporter.step('Create Supabase anonymous client', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('I sign up with a valid email and password', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('Check if I am logged in', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('Check if I am being able to log out', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
})
|
||||
|
||||
test('Existing users', () => {
|
||||
reporter.description('When user is already signed up then he should be able to logged in')
|
||||
|
||||
reporter.step('Create a valid user', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('I sign in with a valid email and password', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('Check if I am logged in', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('Check if I am being able to log out', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
export enum FEATURE {
|
||||
AUTHENTICATION = 'authentication',
|
||||
REALTIME = 'realtime',
|
||||
STORAGE = 'storage',
|
||||
FUNCTIONS = 'functions',
|
||||
RPC = 'rpc',
|
||||
DB = 'database',
|
||||
Filters = 'database.filters',
|
||||
Modifiers = 'database.modifiers',
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import * as faker from 'faker'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const unauthorized = createClient(process.env.SUPABASE_URL, 'FAKE_KEY')
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
const admin = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN)
|
||||
|
||||
test('Unauthorized', async () => {
|
||||
const { error } = await unauthorized.from('profiles').select()
|
||||
expect(error.message).toBe('Invalid authentication credentials')
|
||||
})
|
||||
|
||||
test('Simple test', async () => {
|
||||
const fakeOne = {
|
||||
email: faker.internet.email().toLowerCase(),
|
||||
password: faker.internet.password(),
|
||||
username: faker.internet.userName(),
|
||||
}
|
||||
const fakeTwo = {
|
||||
email: faker.internet.email().toLowerCase(),
|
||||
password: faker.internet.password(),
|
||||
username: faker.internet.userName(),
|
||||
}
|
||||
const first = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
const second = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
|
||||
const { user: firstUser } = await first.auth.signUp({
|
||||
email: fakeOne.email,
|
||||
password: fakeOne.password,
|
||||
})
|
||||
const { user: secondUser } = await second.auth.signUp({
|
||||
email: fakeTwo.email,
|
||||
password: fakeTwo.password,
|
||||
})
|
||||
expect(firstUser.email).toEqual(fakeOne.email)
|
||||
expect(secondUser.email).toEqual(fakeTwo.email)
|
||||
|
||||
const { data: firstProfile } = await first
|
||||
.from('profiles')
|
||||
.insert({
|
||||
id: firstUser.id,
|
||||
username: fakeOne.username,
|
||||
})
|
||||
.single()
|
||||
expect(firstProfile.username).toMatch(fakeOne.username)
|
||||
|
||||
// Cannot insert the second user on the first client
|
||||
const { error: secondProfile } = await first
|
||||
.from('profiles')
|
||||
.insert({
|
||||
id: secondUser.id,
|
||||
username: fakeTwo.username,
|
||||
})
|
||||
.single()
|
||||
expect(secondProfile.message).toMatch(/new row violates row-level security policy for table/)
|
||||
|
||||
const { data: firstProfileList } = await first.from('profiles').select()
|
||||
const { data: secondProfileList } = await second.from('profiles').select()
|
||||
const { data: adminProfileList } = await admin.from('profiles').select()
|
||||
|
||||
expect(firstProfileList.length).toBe(1)
|
||||
expect(secondProfileList.length).toBe(0)
|
||||
expect(adminProfileList.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Config } from '@jest/types'
|
||||
|
||||
const config: Config.InitialOptions = {
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
'^.+\\.ts?$': 'ts-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'cjs', 'json', 'node'],
|
||||
// globalSetup: './test/hooks.ts',
|
||||
// globalTeardown: './teardown.js',
|
||||
setupFiles: ['<rootDir>/.jest/jest-env.js'],
|
||||
setupFilesAfterEnv: ['<rootDir>/.jest/jest-custom-reporter.ts'],
|
||||
testRunner: 'jest-jasmine2',
|
||||
testTimeout: 5000,
|
||||
// testEnvironmentOptions: {
|
||||
// jiraUrl: ""
|
||||
// },
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 0,
|
||||
functions: 0,
|
||||
lines: 0,
|
||||
statements: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
export default config
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "supabase-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "These tests can be run with Docker.",
|
||||
"scripts": {
|
||||
"docker:up": "cd ../docker && docker-compose --env-file ./.env -f docker-compose.yml -f dev/docker-compose.dev.yml up -d",
|
||||
"docker:down": "cd ../docker && docker-compose --env-file ./.env -f docker-compose.yml -f dev/docker-compose.dev.yml down",
|
||||
"test": "jest",
|
||||
"allure:generate": "rm -rf allure-report && node_modules/allure-commandline/bin/allure generate",
|
||||
"allure:serve": "node_modules/allure-commandline/bin/allure serve",
|
||||
"test:report": "npm run allure:generate && npm run allure:serve"
|
||||
},
|
||||
"author": "Supabase, Inc.",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^6.1.2",
|
||||
"@supabase/supabase-js": "^1.34.1",
|
||||
"@testdeck/jest": "^0.2.0",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/node": "^17.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||
"@typescript-eslint/parser": "^5.10.0",
|
||||
"allure-commandline": "^2.17.2",
|
||||
"allure-js-commons": "^2.0.0-beta.14",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"eslint": "^8.7.0",
|
||||
"jest": "^27.4.7",
|
||||
"jest-allure2-adapter": "^0.3.11",
|
||||
"postgres": "^3.0.5",
|
||||
"ts-jest": "^27.1.3",
|
||||
"ts-node": "^10.7.0",
|
||||
"typescript": "^4.6.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"inlineSources": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["es6", "DOM"],
|
||||
"removeComments": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["node_modules/*", "./types/*"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"./node_modules/@types/",
|
||||
"./node_modules/allure2-js-commons/dist/declarations/**/"
|
||||
]
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"compileOnSave": false
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Supabase Beta March 2022
|
||||
description: Functions, GraphQL, and much more.
|
||||
author: ant_wilson
|
||||
image: 2022-march/thumb.jpg
|
||||
thumb: 2022-march/thumb.jpg
|
||||
tags:
|
||||
- launch-week
|
||||
date: '2022-04-15T11:00:00'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
Definitely our most feature-packed month so far. Here's a ton of stuff we shipped throughout March...
|
||||
|
||||
## Functions
|
||||
|
||||
The one we've all been waiting for. You can now [deploy Javascript & Typescript functions](/blog/2022/03/31/supabase-edge-functions) to Supabase. You can develop and test locally, link a project, and deploy globally to the edge in a matter of seconds!
|
||||
|
||||
[](/blog/2022/03/31/supabase-edge-functions)
|
||||
|
||||
## GraphQL
|
||||
|
||||
You can now [query your Supabase database with GraphQL](/blog/2022/03/29/graphql-now-available). We built this as a postgres extension, so it's portable, open source, and solves some of the problems that exist in other GraphQL implementations.
|
||||
|
||||
[](/blog/2022/03/29/graphql-now-available)
|
||||
|
||||
## Realtime
|
||||
|
||||
Supabase Realtime [now supports multiplayer and presence](/blog/2022/04/01/supabase-realtime-with-multiplayer-features), so you can build collaborative applications like Figma and Google Docs with Supabase. Come and have some fun on our demo application.
|
||||
|
||||
[](/blog/2022/04/01/supabase-realtime-with-multiplayer-features)
|
||||
|
||||
## Enterprise
|
||||
|
||||
[Logging and Observability dashboard](/blog/2022/03/30/supabase-enterprise), Prometheus endpoint, SLAs, premium support, pricing updates, and Point-In-Time-Recovery. Everything you need to use Supabase at scale.
|
||||
|
||||
[](/blog/2022/03/30/supabase-enterprise)
|
||||
|
||||
## GitHub Secret Scanning
|
||||
|
||||
We're now an official [GitHub secret scanning partner](/blog/2022/03/28/community-day#supabase-is-now-a-github-secret-scanning-partner), meaning that if you accidentally push your service_role key to GitHub, you'll get a notification email, and your keys will automatically be rolled on Supabase. For private repos you can disable this.
|
||||
|
||||
[](/blog/2022/03/28/community-day#supabase-is-now-a-github-secret-scanning-partner)
|
||||
|
||||
## Community Day
|
||||
|
||||
[PostgREST 10 pre-release](/blog/2022/03/28/community-day#postgrest-10-pre-release), new OAuth providers, Next-JS auth helpers, new courses, and an open source Partner Gallery.
|
||||
|
||||
[](/blog/2022/03/28/community-day)
|
||||
|
||||
## Should I open source my company?
|
||||
|
||||
[We wrote an article](/blog/2022/03/25/should-i-open-source-my-company) for anyone considoring making their codebase open source. We explore the advantages and tackle some common concerns.
|
||||
|
||||
[](/blog/2022/03/25/should-i-open-source-my-company)
|
||||
|
||||
## Supabrew - Never Code Thirsty
|
||||
|
||||
For April Fools - [we announced a non-alcoholic](/blog/2022/04/01/supabrew), light, and refreshing brew. Perfect for coding late into the night. We're pushing the boundaries of Developer Experience.
|
||||
|
||||
[](/blog/2022/04/01/supabrew)
|
||||
|
||||
## Video Highlights
|
||||
|
||||
Relive all the action from Launch Week 4:
|
||||
- [Community Day Live Stream](https://www.youtube.com/watch?v=D0gIgjozOzc) on YouTube
|
||||
- [GraphQL Announcement Live Stream](https://www.youtube.com/watch?v=fApxu_U_hdw) on YouTube
|
||||
- [Enterprise Announcements Live Stream](https://www.youtube.com/watch?v=UZrl1cwzTDg) on YouTube
|
||||
- [Functions Announcement with Deno Team](https://www.youtube.com/watch?v=Xci5SOv76k0) on YouTube
|
||||
- [Realtime Demo App](https://www.youtube.com/watch?v=BelYEMJ2N00) on YouTube
|
||||
- [Realtime Remix w/Supabase Launch Party](https://www.youtube.com/watch?v=rntEMgaenHs) on YouTube
|
||||
- [Building an Image Gallery with Vercel](https://www.youtube.com/watch?v=BSoRXk1FIw8) by Lee Robinson
|
||||
|
||||
## Hiring
|
||||
|
||||
We're Hiring - for a ton of new roles across engineering, marketing, and HR. We hire fully remotely, and you'll get to work alongside some incredible people, on one of the fastest growing open source companies. [See which roles we're hiring for](https://about.supabase.com/careers).
|
||||
|
||||
[](https://about.supabase.com/careers)
|
||||
|
||||
## Twitter
|
||||
|
||||
We're having a 24/7*365 meme-fest on [Twitter](https://twitter.com/supabase). Come join us.
|
||||
|
||||
[](https://twitter.com/supabase)
|
||||
|
||||
## TikTok
|
||||
|
||||
Check us out on [TikTok](https://www.tiktok.com/@supabase.com?fromUrl=%2Fsupabase.com&lang=en)
|
||||
|
||||
[](https://www.tiktok.com/@supabase.com?fromUrl=%2Fsupabase.com&lang=en)
|
||||
|
||||
## Swag Store
|
||||
|
||||
A reminder that we have a [Supabase Swag Store](https://store.supabase.com)
|
||||
|
||||
[](https://store.supabase.com)
|
||||
|
||||
## GitHub
|
||||
|
||||
We hit 30K stars!!: [github.com/supabase/supabase](http://github.com/supabase/supabase)
|
||||
|
||||
## Get started
|
||||
|
||||
- Start using Supabase today: **[app.supabase.io](https://app.supabase.io/)**
|
||||
- Make sure to **[star us on GitHub](https://github.com/supabase/supabase)**
|
||||
- Follow us **[on Twitter](https://twitter.com/supabase)**
|
||||
- Subscribe to our **[YouTube channel](https://www.youtube.com/c/supabase)**
|
||||
- Become a **[sponsor](https://github.com/sponsors/supabase)**
|
||||
@@ -56,7 +56,7 @@ function ExampleCard(props: any) {
|
||||
<div className="flex gap-2 items-center mt-3">
|
||||
{props.vercel_deploy_url && (
|
||||
<a target="_blank" href={props.vercel_deploy_url}>
|
||||
<img src="https://vercel.com/button" />
|
||||
<img className="h-6" src="https://vercel.com/button" />
|
||||
</a>
|
||||
)}
|
||||
{props.demo_url && (
|
||||
|
||||
@@ -21,6 +21,12 @@ export default function IndexPage() {
|
||||
const meta_description =
|
||||
'Explore Supabase fees and pricing information. Find our competitive pricing tiers, with no hidden pricing. We have generous free tiers for those getting started, and Pay As You Go for those scaling up.'
|
||||
|
||||
/**
|
||||
* @mildtomato same plan metadata is also in /studio dashboard
|
||||
* would be good if this constant was shared across apps
|
||||
*
|
||||
* https://github.com/supabase/supabase/blob/master/www/pages/pricing/index.tsx
|
||||
*/
|
||||
const tiers = [
|
||||
{
|
||||
name: 'Free',
|
||||
|
||||
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 656 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -15,6 +15,14 @@
|
||||
<description>Celebrating many amazing OSS Hackathon projects using GraphQL and Edge Functions.</description>
|
||||
<pubDate>Sun, 17 Apr 2022 16:00:00 GMT</pubDate>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/2022/04/15/beta-update-march-2022</guid>
|
||||
<title>Supabase Beta March 2022</title>
|
||||
<link>https://supabase.com/blog/2022/04/15/beta-update-march-2022</link>
|
||||
<description>Functions, GraphQL, and much more.</description>
|
||||
<pubDate>Thu, 14 Apr 2022 16:00:00 GMT</pubDate>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/2022/04/01/supabrew</guid>
|
||||
|
||||