mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' of github.com:appwrite/console into redesign-plan-summary-card
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
name: E2E
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ['**']
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
|
||||
env:
|
||||
VITE_STRIPE_PUBLIC_KEY: ${{ vars.VITE_STRIPE_PUBLIC_KEY }}
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: E2E Tests
|
||||
run: npm run e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 7
|
||||
@@ -28,6 +28,6 @@ jobs:
|
||||
- name: Linter
|
||||
run: npm run lint
|
||||
- name: Unit Tests
|
||||
run: npm test
|
||||
run: npm run test
|
||||
- name: Build Console
|
||||
run: npm run build
|
||||
|
||||
+3
-1
@@ -8,6 +8,8 @@ node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
.DS_STORE
|
||||
.cache
|
||||
@@ -145,4 +147,4 @@ dist
|
||||
.stylelintcache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
.svelte-kit
|
||||
|
||||
Generated
+22
-88
@@ -6,7 +6,7 @@
|
||||
"": {
|
||||
"name": "@appwrite/console",
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.1",
|
||||
"@appwrite.io/console": "^0.6.2",
|
||||
"@appwrite.io/pink": "0.20.0",
|
||||
"@appwrite.io/pink-icons": "0.20.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
@@ -29,7 +29,7 @@
|
||||
"devDependencies": {
|
||||
"@melt-ui/pp": "^0.1.4",
|
||||
"@melt-ui/svelte": "^0.61.2",
|
||||
"@playwright/test": "^1.37.1",
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.3.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||
@@ -157,13 +157,9 @@
|
||||
"integrity": "sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg=="
|
||||
},
|
||||
"node_modules/@appwrite.io/console": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.6.1.tgz",
|
||||
"integrity": "sha512-XlyitzDGmuBzJLBtFuZzL9vS4ZgAWzxuvWrILc7LwdNh2oklzhbJ8BTO8/CAxjxOwR6UOLKjXdoQnqOMqR66Yw==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "3.1.5",
|
||||
"isomorphic-form-data": "2.0.0"
|
||||
}
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.6.2.tgz",
|
||||
"integrity": "sha512-3qYknuFwhvTN2GnPB8G1w5DgxfVorGtfj4uuEaXbTmMUmjA8fHaW5VkJriuUxCi6/TpxDxqV/hhEkdoXL+5H4w=="
|
||||
},
|
||||
"node_modules/@appwrite.io/pink": {
|
||||
"version": "0.20.0",
|
||||
@@ -1895,12 +1891,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.43.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
|
||||
"integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz",
|
||||
"integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.43.1"
|
||||
"playwright": "1.44.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -3452,7 +3448,8 @@
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
@@ -3945,6 +3942,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -4002,14 +4000,6 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
|
||||
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
|
||||
"dependencies": {
|
||||
"node-fetch": "2.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@@ -4223,6 +4213,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -4954,19 +4945,6 @@
|
||||
"is-callable": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
|
||||
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -5782,14 +5760,6 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/isomorphic-form-data": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz",
|
||||
"integrity": "sha512-TYgVnXWeESVmQSg4GLVbalmQ+B4NPi/H4eWxqALKj63KsUrcu301YDjBqaOw3h+cbak7Na4Xyps3BiptHtxTfg==",
|
||||
"dependencies": {
|
||||
"form-data": "^2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
@@ -7107,6 +7077,7 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -7115,6 +7086,7 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
@@ -7236,44 +7208,6 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@@ -7678,12 +7612,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.43.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
|
||||
"integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz",
|
||||
"integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.43.1"
|
||||
"playwright-core": "1.44.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -7696,9 +7630,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.43.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
|
||||
"integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz",
|
||||
"integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
|
||||
+4
-3
@@ -15,10 +15,11 @@
|
||||
"test": "TZ=EST vitest run",
|
||||
"test:ui": "TZ=EST vitest --ui",
|
||||
"test:watch": "TZ=EST vitest watch",
|
||||
"e2e": "playwright test tests/e2e"
|
||||
"e2e": "playwright test tests/e2e",
|
||||
"e2e:ui": "playwright test tests/e2e --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.1",
|
||||
"@appwrite.io/console": "^0.6.2",
|
||||
"@appwrite.io/pink": "0.20.0",
|
||||
"@appwrite.io/pink-icons": "0.20.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
@@ -41,7 +42,7 @@
|
||||
"devDependencies": {
|
||||
"@melt-ui/pp": "^0.1.4",
|
||||
"@melt-ui/svelte": "^0.61.2",
|
||||
"@playwright/test": "^1.37.1",
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.3.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { type PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
timeout: 120000,
|
||||
reportSlowTests: null,
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
webServer: {
|
||||
timeout: 120000,
|
||||
env: {
|
||||
VITE_APPWRITE_ENDPOINT: 'http://console-tests.appwrite.org/v1',
|
||||
VITE_CONSOLE_MODE: 'cloud'
|
||||
},
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ export enum Submit {
|
||||
AccountAuthenticatorDelete = 'submit_account_authenticator_delete',
|
||||
AccountRecoveryCodesCreate = 'submit_account_recovery_codes_create',
|
||||
AccountRecoveryCodesUpdate = 'submit_account_recovery_codes_update',
|
||||
AccountDeleteIdentity = 'submit_account_delete_identity',
|
||||
UserCreate = 'submit_user_create',
|
||||
UserDelete = 'submit_user_delete',
|
||||
UserUpdateEmail = 'submit_user_update_email',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { Card } from '..';
|
||||
|
||||
export let service: string;
|
||||
export let eventSource: string;
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<slot>
|
||||
<div class="u-flex u-flex-vertical u-main-center u-cross-center u-gap-8">
|
||||
<h6 class="body-text-1 u-bold u-trim-1">Upgrade to add {service}</h6>
|
||||
<p class="text u-text-center">
|
||||
Upgrade to a {tierToPlan(BillingPlan.PRO).name} plan to add {service} to your organization
|
||||
</p>
|
||||
|
||||
<Button
|
||||
class="u-margin-block-start-16"
|
||||
secondary
|
||||
fullWidthMobile
|
||||
on:click={() => {
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: eventSource
|
||||
});
|
||||
wizard.start(ChangeOrganizationTierCloud);
|
||||
}}>
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
</slot>
|
||||
</Card>
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as PaymentBoxes } from './paymentBoxes.svelte';
|
||||
export { default as CouponInput } from './couponInput.svelte';
|
||||
export { default as EmptyCardCloud } from './emptyCardCloud.svelte';
|
||||
|
||||
@@ -73,3 +73,4 @@ export { default as RadioBoxes } from './radioBoxes.svelte';
|
||||
export { default as ModalWrapper } from './modalWrapper.svelte';
|
||||
export { default as ModalSideCol } from './modalSideCol.svelte';
|
||||
export { default as ImagePreview } from './imagePreview.svelte';
|
||||
export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte';
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<script context="module" lang="ts">
|
||||
export async function verify(challenge: Models.MfaChallenge, code: string) {
|
||||
try {
|
||||
if (challenge == null) {
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(
|
||||
AuthenticationFactor.Totp
|
||||
);
|
||||
}
|
||||
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountCreate);
|
||||
} catch (error) {
|
||||
trackError(error, Submit.AccountCreate);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Button, FormItem, FormList, InputDigits, InputText } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
|
||||
|
||||
export let factors: Models.MfaFactors & { recoveryCode: boolean };
|
||||
/** If true, the form will be submitted automatically when the code is entered. */
|
||||
export let autoSubmit: boolean = true;
|
||||
export let showVerifyButton: boolean = true;
|
||||
export let disabled: boolean = false;
|
||||
export let challenge: Models.MfaChallenge;
|
||||
export let code: string;
|
||||
|
||||
let challengeType: AuthenticationFactor;
|
||||
|
||||
const enabledFactors = Object.entries(factors).filter(([_, enabled]) => enabled);
|
||||
|
||||
async function createChallenge(factor: AuthenticationFactor) {
|
||||
disabled = true;
|
||||
challengeType = factor;
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
disabled = false;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const enabledNonRecoveryFactors = enabledFactors.filter(
|
||||
([factor, _]) => factor != 'recoveryCode'
|
||||
);
|
||||
if (enabledNonRecoveryFactors.length == 1) {
|
||||
if (factors.phone) {
|
||||
createChallenge(AuthenticationFactor.Phone);
|
||||
} else if (factors.email) {
|
||||
createChallenge(AuthenticationFactor.Email);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<FormList>
|
||||
{#if challengeType == AuthenticationFactor.Recoverycode}
|
||||
<p>
|
||||
Enter below one of the recovery codes you received when enabling MFA for this account.
|
||||
</p>
|
||||
<InputText id="recovery-code" bind:value={code} required autofocus />
|
||||
{:else}
|
||||
{#if factors.totp && (challengeType == AuthenticationFactor.Totp || challengeType == null)}
|
||||
<p>Enter below a 6-digit one-time code generated by your authentication app.</p>
|
||||
{:else if challengeType == AuthenticationFactor.Email}
|
||||
<p>A 6-digit verification code was sent to your email, enter it below.</p>
|
||||
{:else if challengeType == AuthenticationFactor.Phone}
|
||||
<p>A 6-digit verification code was sent to your phone, enter it below.</p>
|
||||
{/if}
|
||||
<InputDigits bind:value={code} required autofocus {autoSubmit} />
|
||||
{/if}
|
||||
{#if showVerifyButton}
|
||||
<FormItem>
|
||||
<Button fullWidth submit {disabled}>Verify</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if enabledFactors.length > 1}
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
{#if factors.totp && challengeType != null && challengeType != AuthenticationFactor.Totp}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Totp)}>
|
||||
<span class="icon-device-mobile u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Authenticator app</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.email && challengeType != AuthenticationFactor.Email}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Email)}>
|
||||
<span class="icon-mail u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Email verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.phone && challengeType != AuthenticationFactor.Phone}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Phone)}>
|
||||
<span class="icon-chat-alt u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Phone verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.recoveryCode && challengeType != AuthenticationFactor.Recoverycode}
|
||||
<FormItem>
|
||||
<Button
|
||||
text
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Recoverycode)}>
|
||||
<span class="text">Use recovery code</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
export let items = [];
|
||||
export let limit = 5;
|
||||
export let hideFooter = false;
|
||||
|
||||
let offset = 0;
|
||||
|
||||
@@ -10,9 +11,11 @@
|
||||
$: paginatedItems = items.slice(offset, offset + limit);
|
||||
</script>
|
||||
|
||||
<slot {paginatedItems} />
|
||||
<slot {paginatedItems} {limit} />
|
||||
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {total}</p>
|
||||
<PaginationInline {limit} bind:offset sum={total} hidePages />
|
||||
</div>
|
||||
{#if !hideFooter}
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {total}</p>
|
||||
<PaginationInline {limit} bind:offset sum={total} hidePages />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -118,8 +118,8 @@
|
||||
<!-- svelte-ignore css-unused-selector -->
|
||||
<style lang="scss" global>
|
||||
.tippy-user .tippy-box {
|
||||
--p-drop-bg-color: var(--color-neutral-500);
|
||||
--p-drop-border-color: var(--color-neutral-200);
|
||||
--p-drop-bg-color: var(--color-neutral-105);
|
||||
--p-drop-border-color: var(--color-neutral-85);
|
||||
|
||||
body.theme-light & {
|
||||
--p-drop-bg-color: var(--color-neutral-0);
|
||||
|
||||
@@ -1,46 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import SupportOfflineLight from '$lib/images/support/support-offline-light.png';
|
||||
import SupportOfflineDark from '$lib/images/support/support-offline-dark.png';
|
||||
import SupportOnlineLight from '$lib/images/support/support-online-light.png';
|
||||
import SupportOnlineDark from '$lib/images/support/support-online-dark.png';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import SupportWizard from '../../routes/console/supportWizard.svelte';
|
||||
import { isSupportOnline, showSupportModal } from '../../routes/console/wizard/support/store';
|
||||
import { showSupportModal } from '../../routes/console/wizard/support/store';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { localeTimezoneName, utcHourToLocaleHour } from '$lib/helpers/date';
|
||||
|
||||
export let show = false;
|
||||
|
||||
$: isPaid =
|
||||
$organization?.billingPlan === BillingPlan.PRO ||
|
||||
$organization?.billingPlan === BillingPlan.SCALE;
|
||||
|
||||
$: supportTimings = `${utcHourToLocaleHour('09:00')} - ${utcHourToLocaleHour('17:00')} ${localeTimezoneName()}`;
|
||||
</script>
|
||||
|
||||
{#if isCloud}
|
||||
<section class="drop-section u-grid u-gap-24 u-padding-24">
|
||||
<div class="u-flex u-main-center">
|
||||
{#if isSupportOnline()}
|
||||
{#if $app.themeInUse === 'light'}
|
||||
<img src={SupportOnlineLight} alt="Support" height="87" width="230" />
|
||||
{:else}
|
||||
<img src={SupportOnlineDark} alt="Support" height="87" width="230" />
|
||||
{/if}
|
||||
{:else if $app.themeInUse === 'light'}
|
||||
<img src={SupportOfflineLight} alt="Support" height="87" width="230" />
|
||||
{:else}
|
||||
<img src={SupportOfflineDark} alt="Support" height="87" width="230" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="eyebrow-heading-3">Premium support</h4>
|
||||
{#if isPaid}
|
||||
<p class="u-line-height-1-5 u-margin-block-start-8">
|
||||
Get personalized support from the Appwrite team from <b>09:00 - 17:00 UTC</b>
|
||||
Get personalized support from the Appwrite team from <b>{supportTimings}</b>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ export const INTERVAL = 5 * 60000; // default interval to check for feedback
|
||||
|
||||
export enum Dependencies {
|
||||
FACTORS = 'dependency:factors',
|
||||
IDENTITIES = 'dependency:identities',
|
||||
CREDIT = 'dependency:credit',
|
||||
INVOICES = 'dependency:invoices',
|
||||
ADDRESS = 'dependency:address',
|
||||
|
||||
@@ -24,7 +24,11 @@
|
||||
onValueChange: ({ next }) => {
|
||||
value = next.join('');
|
||||
|
||||
if (autoSubmit && value.length === length && !autoSubmitted) {
|
||||
if (value.length < length) {
|
||||
autoSubmitted = false;
|
||||
}
|
||||
|
||||
if (element && autoSubmit && value.length === length && !autoSubmitted) {
|
||||
autoSubmitted = true;
|
||||
const firstInputElement = element.querySelector('input');
|
||||
firstInputElement?.form.requestSubmit();
|
||||
|
||||
@@ -2,6 +2,15 @@ import { browser } from '$app/environment';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
export type WeekDay =
|
||||
| 'Sunday'
|
||||
| 'Monday'
|
||||
| 'Tuesday'
|
||||
| 'Wednesday'
|
||||
| 'Thursday'
|
||||
| 'Friday'
|
||||
| 'Saturday';
|
||||
|
||||
if (browser) {
|
||||
dayjs.extend(relativeTime);
|
||||
}
|
||||
@@ -73,6 +82,52 @@ export const toLocaleTimeISO = (datetime: string | number) => {
|
||||
return date.toLocaleTimeString('sv');
|
||||
};
|
||||
|
||||
export const utcHourToLocaleHour = (utcTimeString: string) => {
|
||||
const now = new Date();
|
||||
const [hours, minutes] = utcTimeString.split(':').map(Number);
|
||||
const customTime = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes);
|
||||
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hourCycle: 'h23'
|
||||
};
|
||||
|
||||
return new Intl.DateTimeFormat('en', options).format(new Date(customTime));
|
||||
};
|
||||
|
||||
export const utcWeekDayToLocaleWeekDay = (utcDay: WeekDay, utcTimeString: string) => {
|
||||
const now = new Date();
|
||||
|
||||
const daysOfTheWeek = {
|
||||
Sunday: 0,
|
||||
Monday: 1,
|
||||
Tuesday: 2,
|
||||
Wednesday: 3,
|
||||
Thursday: 4,
|
||||
Friday: 5,
|
||||
Saturday: 6
|
||||
};
|
||||
|
||||
const utcDayNumber = daysOfTheWeek[utcDay];
|
||||
const [hours, minutes] = utcTimeString.split(':').map(Number);
|
||||
|
||||
const daysUntilNextUtcDay = (7 + utcDayNumber - now.getUTCDay()) % 7;
|
||||
now.setUTCDate(now.getUTCDate() + daysUntilNextUtcDay);
|
||||
now.setUTCHours(hours, minutes, 0, 0);
|
||||
|
||||
return new Intl.DateTimeFormat('en', { weekday: 'short' }).format(now);
|
||||
};
|
||||
|
||||
export const localeTimezoneName = () => {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
timeZoneName: 'longGeneric'
|
||||
};
|
||||
|
||||
const dateWithTimezone = new Intl.DateTimeFormat('en', options).format(new Date());
|
||||
return dateWithTimezone.split(', ')[1];
|
||||
};
|
||||
|
||||
export const isSameDay = (date1: Date, date2: Date) => {
|
||||
return (
|
||||
date1.getFullYear() === date2.getFullYear() &&
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { AvatarInitials, DropList, DropListItem, DropListLink, Support } from '$lib/components';
|
||||
import { app } from '$lib/stores/app';
|
||||
@@ -24,7 +24,7 @@
|
||||
import CreateOrganizationCloud from '$routes/console/createOrganizationCloud.svelte';
|
||||
import { Feedback } from '$lib/components/feedback';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
|
||||
let showDropdown = false;
|
||||
let showSupport = false;
|
||||
@@ -40,6 +40,7 @@
|
||||
|
||||
async function logout() {
|
||||
await sdk.forConsole.account.deleteSession('current');
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountLogout);
|
||||
await goto(`${base}/login`);
|
||||
}
|
||||
|
||||
@@ -1762,6 +1762,46 @@ export const marketplace: MarketplaceTemplate[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: 'icon-chip',
|
||||
id: 'chat-with-anyscale',
|
||||
name: 'Chat with AnyScale',
|
||||
tagline: 'Create a chatbot using the AnyScale API.',
|
||||
permissions: ['any'],
|
||||
cron: '',
|
||||
events: [],
|
||||
timeout: 15,
|
||||
usecases: ['AI'],
|
||||
runtimes: [
|
||||
...getRuntimes(
|
||||
TemplateRuntimes.NODE,
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/chat-with-anyscale'
|
||||
)
|
||||
],
|
||||
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/chat-with-anyscale">file</a>.`,
|
||||
vcsProvider: 'github',
|
||||
providerRepositoryId: 'templates',
|
||||
providerOwner: 'appwrite',
|
||||
providerBranch: 'main',
|
||||
variables: [
|
||||
{
|
||||
name: 'ANYSCALE_API_KEY',
|
||||
description: `A unique key used to authenticate with the AnyScale API. <a class="u-bold" target="_blank" href="https://app.endpoints.anyscale.com/credentials">Learn more</a>.`,
|
||||
placeholder: 'd03xxxxxxxx26',
|
||||
required: true,
|
||||
type: 'password'
|
||||
},
|
||||
{
|
||||
name: 'ANYSCALE_MAX_TOKENS',
|
||||
description: `The maximum number of tokens that Anyscale responses should contain. <a class="u-bold" target="_blank" href="https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them">Learn more</a>.`,
|
||||
placeholder: '',
|
||||
required: false,
|
||||
type: 'number'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: 'icon-music-note',
|
||||
id: 'music-generation-with-huggingface',
|
||||
|
||||
+232
-170
@@ -1,6 +1,4 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Apple from '../../routes/console/project-[project]/auth/(providers)/appleOAuth.svelte';
|
||||
import Auth0 from '../../routes/console/project-[project]/auth/(providers)/auth0OAuth.svelte';
|
||||
import Authentik from '../../routes/console/project-[project]/auth/(providers)/authentikOAuth.svelte';
|
||||
@@ -11,176 +9,240 @@ import Microsoft from '../../routes/console/project-[project]/auth/(providers)/m
|
||||
import Oidc from '../../routes/console/project-[project]/auth/(providers)/oidcOAuth.svelte';
|
||||
import Okta from '../../routes/console/project-[project]/auth/(providers)/oktaOAuth.svelte';
|
||||
|
||||
export type Provider = Models.AuthProvider & {
|
||||
export type Provider = {
|
||||
name: string;
|
||||
icon: string;
|
||||
docs?: string;
|
||||
component?: typeof SvelteComponent<unknown>;
|
||||
component: typeof SvelteComponent<unknown>;
|
||||
};
|
||||
|
||||
export type Providers = {
|
||||
providers: Provider[];
|
||||
export const oAuthProviders: Record<string, Provider> = {
|
||||
amazon: {
|
||||
name: 'Amazon',
|
||||
icon: 'amazon',
|
||||
docs: 'https://developer.amazon.com/apps-and-games/services-and-apis',
|
||||
component: Main
|
||||
},
|
||||
apple: {
|
||||
name: 'Apple',
|
||||
icon: 'apple',
|
||||
docs: 'https://developer.apple.com/',
|
||||
component: Apple
|
||||
},
|
||||
auth0: {
|
||||
name: 'Auth0',
|
||||
icon: 'auth0',
|
||||
docs: 'https://auth0.com/developers',
|
||||
component: Auth0
|
||||
},
|
||||
authentik: {
|
||||
name: 'Authentik',
|
||||
icon: 'authentik',
|
||||
docs: 'https://goauthentik.io/integrations/sources/oauth/',
|
||||
component: Authentik
|
||||
},
|
||||
autodesk: {
|
||||
name: 'Autodesk',
|
||||
icon: 'autodesk',
|
||||
docs: 'https://forge.autodesk.com/en/docs/oauth/v2/developers_guide/overview/',
|
||||
component: Main
|
||||
},
|
||||
bitbucket: {
|
||||
name: 'Bitbucket',
|
||||
icon: 'bitbucket',
|
||||
docs: 'https://developer.atlassian.com/bitbucket',
|
||||
component: Main
|
||||
},
|
||||
bitly: {
|
||||
name: 'Bitly',
|
||||
icon: 'bitly',
|
||||
docs: 'https://dev.bitly.com/v4_documentation.html',
|
||||
component: Main
|
||||
},
|
||||
box: {
|
||||
name: 'Box',
|
||||
icon: 'box',
|
||||
docs: 'https://developer.box.com/reference/',
|
||||
component: Main
|
||||
},
|
||||
dailymotion: {
|
||||
name: 'Dailymotion',
|
||||
icon: 'dailymotion',
|
||||
docs: 'https://developers.dailymotion.com/api/',
|
||||
component: Main
|
||||
},
|
||||
discord: {
|
||||
name: 'Discord',
|
||||
icon: 'discord',
|
||||
docs: 'https://discordapp.com/developers/docs/topics/oauth2',
|
||||
component: Main
|
||||
},
|
||||
disqus: {
|
||||
name: 'Disqus',
|
||||
icon: 'disqus',
|
||||
docs: 'https://disqus.com/api/docs/auth/',
|
||||
component: Main
|
||||
},
|
||||
dropbox: {
|
||||
name: 'Dropbox',
|
||||
icon: 'dropbox',
|
||||
docs: 'https://www.dropbox.com/developers/documentation',
|
||||
component: Main
|
||||
},
|
||||
etsy: {
|
||||
name: 'Etsy',
|
||||
icon: 'etsy',
|
||||
docs: 'https://developers.etsy.com/',
|
||||
component: Main
|
||||
},
|
||||
facebook: {
|
||||
name: 'Facebook',
|
||||
icon: 'facebook',
|
||||
docs: 'https://developers.facebook.com/',
|
||||
component: Main
|
||||
},
|
||||
github: {
|
||||
name: 'GitHub',
|
||||
icon: 'github',
|
||||
docs: 'https://developer.github.com',
|
||||
component: Main
|
||||
},
|
||||
gitlab: {
|
||||
name: 'GitLab',
|
||||
icon: 'gitlab',
|
||||
docs: 'https://docs.gitlab.com/ee/api/',
|
||||
component: GitLab
|
||||
},
|
||||
google: {
|
||||
name: 'Google',
|
||||
icon: 'google',
|
||||
docs: 'https://support.google.com/googleapi/answer/6158849',
|
||||
component: Google
|
||||
},
|
||||
linkedin: {
|
||||
name: 'LinkedIn',
|
||||
icon: 'linkedin',
|
||||
docs: 'https://developer.linkedin.com/',
|
||||
component: Main
|
||||
},
|
||||
microsoft: {
|
||||
name: 'Microsoft',
|
||||
icon: 'microsoft',
|
||||
docs: 'https://developer.microsoft.com/en-us/',
|
||||
component: Microsoft
|
||||
},
|
||||
notion: {
|
||||
name: 'Notion',
|
||||
icon: 'notion',
|
||||
docs: 'https://developers.notion.com/docs',
|
||||
component: Main
|
||||
},
|
||||
oidc: {
|
||||
name: 'OIDC',
|
||||
icon: 'oidc',
|
||||
docs: 'https://openid.net/connect/faq/',
|
||||
component: Oidc
|
||||
},
|
||||
okta: {
|
||||
name: 'Okta',
|
||||
icon: 'okta',
|
||||
docs: 'https://developer.okta.com',
|
||||
component: Okta
|
||||
},
|
||||
paypal: {
|
||||
name: 'Paypal',
|
||||
icon: 'paypal',
|
||||
docs: 'https://developer.paypal.com/docs/api/overview/',
|
||||
component: Main
|
||||
},
|
||||
paypalsandbox: {
|
||||
name: 'Paypal Sandbox',
|
||||
icon: 'paypal',
|
||||
docs: 'https://developer.paypal.com/docs/api/overview/',
|
||||
component: Main
|
||||
},
|
||||
podio: {
|
||||
name: 'Podio',
|
||||
icon: 'podio',
|
||||
docs: 'https://developers.podio.com/doc/oauth-authorization',
|
||||
component: Main
|
||||
},
|
||||
salesforce: {
|
||||
name: 'Salesforce',
|
||||
icon: 'salesforce',
|
||||
docs: 'https://developer.salesforce.com/docs/',
|
||||
component: Main
|
||||
},
|
||||
slack: {
|
||||
name: 'Slack',
|
||||
icon: 'slack',
|
||||
docs: 'https://api.slack.com/',
|
||||
component: Main
|
||||
},
|
||||
spotify: {
|
||||
name: 'Spotify',
|
||||
icon: 'spotify',
|
||||
docs: 'https://developer.spotify.com/documentation/general/guides/authorization-guide/',
|
||||
component: Main
|
||||
},
|
||||
stripe: {
|
||||
name: 'Stripe',
|
||||
icon: 'stripe',
|
||||
docs: 'https://stripe.com/docs/api',
|
||||
component: Main
|
||||
},
|
||||
tradeshift: {
|
||||
name: 'Tradeshift',
|
||||
icon: 'tradeshift',
|
||||
docs: 'https://developers.tradeshift.com/docs/api',
|
||||
component: Main
|
||||
},
|
||||
tradeshiftBox: {
|
||||
name: 'Tradeshift Sandbox',
|
||||
icon: 'tradeshift',
|
||||
docs: 'https://developers.tradeshift.com/docs/api',
|
||||
component: Main
|
||||
},
|
||||
twitch: {
|
||||
name: 'Twitch',
|
||||
icon: 'twitch',
|
||||
docs: 'https://dev.twitch.tv/docs/auth',
|
||||
component: Main
|
||||
},
|
||||
wordpress: {
|
||||
name: 'Wordpress',
|
||||
icon: 'wordpress',
|
||||
docs: 'https://developer.wordpress.com/docs/oauth2/',
|
||||
component: Main
|
||||
},
|
||||
yahoo: {
|
||||
name: 'Yahoo',
|
||||
icon: 'yahoo',
|
||||
docs: 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
|
||||
component: Main
|
||||
},
|
||||
yammer: {
|
||||
name: 'Yammer',
|
||||
icon: 'yammer',
|
||||
docs: 'https://developer.yammer.com/docs/oauth-2',
|
||||
component: Main
|
||||
},
|
||||
yandex: {
|
||||
name: 'Yandex',
|
||||
icon: 'yandex',
|
||||
docs: 'https://tech.yandex.com/oauth/',
|
||||
component: Main
|
||||
},
|
||||
zoho: {
|
||||
name: 'Zoho',
|
||||
icon: 'zoho',
|
||||
docs: 'https://www.zoho.com/crm/developer/docs/api/oauth-overview.html',
|
||||
component: Main
|
||||
},
|
||||
zoom: {
|
||||
name: 'Zoom',
|
||||
icon: 'zoom',
|
||||
docs: 'https://marketplace.zoom.us/docs/guides/auth/oauth/',
|
||||
component: Main
|
||||
}
|
||||
};
|
||||
|
||||
const setProviders = (project: Models.Project): Provider[] => {
|
||||
return (
|
||||
project?.oAuthProviders.map((n) => {
|
||||
let docs: Provider['docs'];
|
||||
let icon: Provider['icon'] = n.key.toLowerCase();
|
||||
let component: Provider['component'] = Main;
|
||||
|
||||
switch (n.key.toLowerCase()) {
|
||||
case 'amazon':
|
||||
docs = 'https://developer.amazon.com/apps-and-games/services-and-apis';
|
||||
break;
|
||||
case 'apple':
|
||||
docs = 'https://developer.apple.com/';
|
||||
component = Apple;
|
||||
break;
|
||||
case 'auth0':
|
||||
docs = 'https://auth0.com/developers';
|
||||
component = Auth0;
|
||||
break;
|
||||
case 'authentik':
|
||||
docs = 'https://goauthentik.io/integrations/sources/oauth/';
|
||||
component = Authentik;
|
||||
break;
|
||||
case 'autodesk':
|
||||
docs = 'https://forge.autodesk.com/en/docs/oauth/v2/developers_guide/overview/';
|
||||
break;
|
||||
case 'bitbucket':
|
||||
docs = 'https://developer.atlassian.com/bitbucket';
|
||||
break;
|
||||
case 'bitly':
|
||||
docs = 'https://dev.bitly.com/v4_documentation.html';
|
||||
break;
|
||||
case 'box':
|
||||
docs = 'https://developer.box.com/reference/';
|
||||
break;
|
||||
case 'dailymotion':
|
||||
docs = 'https://developers.dailymotion.com/api/';
|
||||
break;
|
||||
case 'discord':
|
||||
docs = 'https://discordapp.com/developers/docs/topics/oauth2';
|
||||
break;
|
||||
case 'disqus':
|
||||
docs = 'https://disqus.com/api/docs/auth/';
|
||||
break;
|
||||
case 'dropbox':
|
||||
docs = 'https://www.dropbox.com/developers/documentation';
|
||||
break;
|
||||
case 'etsy':
|
||||
docs = 'https://developers.etsy.com/';
|
||||
break;
|
||||
case 'facebook':
|
||||
docs = 'https://developers.facebook.com/';
|
||||
break;
|
||||
case 'github':
|
||||
docs = 'https://developer.github.com';
|
||||
break;
|
||||
case 'gitlab':
|
||||
docs = 'https://docs.gitlab.com/ee/api/';
|
||||
component = GitLab;
|
||||
break;
|
||||
case 'google':
|
||||
docs = 'https://support.google.com/googleapi/answer/6158849';
|
||||
component = Google;
|
||||
break;
|
||||
case 'linkedin':
|
||||
docs = 'https://developer.linkedin.com/';
|
||||
break;
|
||||
case 'microsoft':
|
||||
docs = 'https://developer.microsoft.com/en-us/';
|
||||
component = Microsoft;
|
||||
break;
|
||||
case 'notion':
|
||||
docs = 'https://developers.notion.com/docs';
|
||||
break;
|
||||
case 'oidc':
|
||||
docs = 'https://openid.net/connect/faq/';
|
||||
component = Oidc;
|
||||
break;
|
||||
case 'okta':
|
||||
docs = 'https://developer.okta.com';
|
||||
component = Okta;
|
||||
break;
|
||||
case 'paypal':
|
||||
docs = 'https://developer.paypal.com/docs/api/overview/';
|
||||
break;
|
||||
case 'paypalsandbox':
|
||||
icon = 'paypal';
|
||||
docs = 'https://developer.paypal.com/docs/api/overview/';
|
||||
break;
|
||||
case 'podio':
|
||||
docs = 'https://developers.podio.com/doc/oauth-authorization';
|
||||
break;
|
||||
case 'salesforce':
|
||||
docs = 'https://developer.salesforce.com/docs/';
|
||||
break;
|
||||
case 'slack':
|
||||
docs = 'https://api.slack.com/';
|
||||
break;
|
||||
case 'spotify':
|
||||
docs =
|
||||
'https://developer.spotify.com/documentation/general/guides/authorization-guide/';
|
||||
break;
|
||||
case 'stripe':
|
||||
docs = 'https://stripe.com/docs/api';
|
||||
break;
|
||||
case 'tradeshift':
|
||||
docs = 'https://developers.tradeshift.com/docs/api';
|
||||
break;
|
||||
case 'tradeshiftbox':
|
||||
icon = 'tradeshift';
|
||||
docs = 'https://developers.tradeshift.com/docs/api';
|
||||
break;
|
||||
case 'twitch':
|
||||
docs = 'https://dev.twitch.tv/docs/auth';
|
||||
break;
|
||||
case 'wordpress':
|
||||
docs = 'https://developer.wordpress.com/docs/oauth2/';
|
||||
break;
|
||||
case 'yahoo':
|
||||
docs = 'https://developer.yahoo.com/oauth2/guide/flows_authcode/';
|
||||
break;
|
||||
case 'yammer':
|
||||
docs = 'https://developer.yammer.com/docs/oauth-2';
|
||||
break;
|
||||
case 'yandex':
|
||||
docs = 'https://tech.yandex.com/oauth/';
|
||||
break;
|
||||
case 'zoho':
|
||||
docs = 'https://www.zoho.com/accounts/protocol/oauth/sign-in-using-zoho.html';
|
||||
break;
|
||||
case 'zoom':
|
||||
docs = 'https://marketplace.zoom.us/docs/guides/auth/oauth/';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
...n,
|
||||
icon,
|
||||
docs,
|
||||
component
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
function createOAuthProviders() {
|
||||
const { subscribe, set } = writable<Providers>({
|
||||
providers: setProviders(null)
|
||||
});
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
load: (project: Models.Project) => {
|
||||
const providers = setProviders(project);
|
||||
|
||||
set({ providers });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const OAuthProviders = createOAuthProviders();
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
variables[variable.name] = variable.value ?? '';
|
||||
});
|
||||
|
||||
if (!runtime) {
|
||||
runtime = template.runtimes[0].name;
|
||||
}
|
||||
|
||||
templateStore.set(template);
|
||||
templateConfig.set({
|
||||
$id: null,
|
||||
|
||||
@@ -103,25 +103,23 @@
|
||||
</div>
|
||||
{:then installations}
|
||||
{#if hasInstallations}
|
||||
<div class="u-flex u-gap-16">
|
||||
<div class="u-width-full-line">
|
||||
<InputSelect
|
||||
id="installation"
|
||||
label="Git organization"
|
||||
options={installations.map((entry) => {
|
||||
return {
|
||||
label: entry.organization,
|
||||
value: entry.$id
|
||||
};
|
||||
})}
|
||||
on:change={() => {
|
||||
$installation = installations.find(
|
||||
(entry) => entry.$id === selectedInstallationId
|
||||
);
|
||||
}}
|
||||
bind:value={selectedInstallationId} />
|
||||
</div>
|
||||
</div>
|
||||
<FormList>
|
||||
<InputSelect
|
||||
id="installation"
|
||||
label="Git organization"
|
||||
options={installations.map((entry) => {
|
||||
return {
|
||||
label: entry.organization,
|
||||
value: entry.$id
|
||||
};
|
||||
})}
|
||||
on:change={() => {
|
||||
$installation = installations.find(
|
||||
(entry) => entry.$id === selectedInstallationId
|
||||
);
|
||||
}}
|
||||
bind:value={selectedInstallationId} />
|
||||
</FormList>
|
||||
{:else}
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-gap-16">
|
||||
<Button href={connectGitHub().toString()} fullWidth secondary>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
id="runtime"
|
||||
placeholder="Select runtime"
|
||||
required
|
||||
disabled={options.length <= 1}
|
||||
disabled={options.length < 1}
|
||||
{options}
|
||||
bind:value={$templateConfig.runtime} />
|
||||
{/await}
|
||||
|
||||
@@ -42,12 +42,13 @@ export const load: LayoutLoad = async ({ depends, url }) => {
|
||||
const path = url.search ? `${url.search}&${redirectUrl}` : `?${redirectUrl}`;
|
||||
|
||||
if (error.type === 'user_more_factors_required') {
|
||||
if (url.pathname === '/mfa') return;
|
||||
if (url.pathname === '/mfa') return {}; // Ensure any previous account/organizations are cleared
|
||||
redirect(303, `/mfa${path}`);
|
||||
}
|
||||
|
||||
if (!acceptedRoutes.some((n) => url.pathname.startsWith(n))) {
|
||||
redirect(303, `/login${path}`);
|
||||
}
|
||||
return {}; // Ensure any previous account/organizations are cleared
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,9 @@ import { sdk } from '$lib/stores/sdk';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch, depends }) => {
|
||||
export const load: LayoutLoad = async ({ fetch, depends, parent }) => {
|
||||
await parent(); // ensure user is authenticated before proceeding
|
||||
|
||||
depends(Dependencies.RUNTIMES);
|
||||
depends(Dependencies.CONSOLE_VARIABLES);
|
||||
|
||||
|
||||
@@ -6,9 +6,18 @@ import { Dependencies } from '$lib/constants';
|
||||
|
||||
export const load: LayoutLoad = async ({ depends }) => {
|
||||
depends(Dependencies.FACTORS);
|
||||
depends(Dependencies.IDENTITIES);
|
||||
|
||||
const promises = [
|
||||
sdk.forConsole.account.listMfaFactors(),
|
||||
sdk.forConsole.account.listIdentities()
|
||||
];
|
||||
|
||||
const [factors, identities] = await Promise.all(promises);
|
||||
|
||||
return {
|
||||
factors: await sdk.forConsole.account.listMfaFactors(),
|
||||
factors,
|
||||
identities,
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs
|
||||
};
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
import UpdateEmail from './updateEmail.svelte';
|
||||
import DeleteAccount from './deleteAccount.svelte';
|
||||
import UpdateMfa from './updateMfa.svelte';
|
||||
import Identities from './identities.svelte';
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<UpdateName />
|
||||
<UpdateEmail />
|
||||
<UpdatePassword />
|
||||
<Identities />
|
||||
<UpdateMfa />
|
||||
<DeleteAccount />
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { identities } from './store';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { oAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import EmptySearch from '$lib/components/emptySearch.svelte';
|
||||
|
||||
async function deleteIdentity(id: string) {
|
||||
try {
|
||||
await sdk.forConsole.account.deleteIdentity(id);
|
||||
addNotification({
|
||||
message: 'Identity has been deleted',
|
||||
type: 'success'
|
||||
});
|
||||
await invalidate(Dependencies.IDENTITIES);
|
||||
trackEvent(Submit.AccountDeleteIdentity);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.AccountDeleteIdentity);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Identities</Heading>
|
||||
<p class="text">
|
||||
Identities are your connected GitHub accounts. You can sign in using these accounts.
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $identities.length === 0}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="u-text-center">
|
||||
No identities are currently available. Once you sign in via GitHub, you'll see
|
||||
it here.
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead width={100}>Provider</TableCellHead>
|
||||
<TableCellHead>Email</TableCellHead>
|
||||
<TableCellHead>Created At</TableCellHead>
|
||||
<TableCellHead>Expiry Date</TableCellHead>
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each $identities as identity (identity.$id)}
|
||||
{@const provider = oAuthProviders[identity.provider]}
|
||||
<TableRow>
|
||||
<TableCell title="Provider">
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
<div class="avatar is-size-small">
|
||||
<span
|
||||
class={`icon-${provider.icon}`}
|
||||
style="font-size: var(--icon-size-medium)!important" />
|
||||
</div>
|
||||
{provider.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCellText title="Email">
|
||||
{identity.providerEmail}
|
||||
</TableCellText>
|
||||
<TableCellText title="Created at">
|
||||
{toLocaleDateTime(identity.$createdAt)}
|
||||
</TableCellText>
|
||||
<TableCellText title="Expiry date">
|
||||
{toLocaleDateTime(identity.providerAccessTokenExpiry)}
|
||||
</TableCellText>
|
||||
<TableCell title="Remove">
|
||||
<Button
|
||||
text
|
||||
class="is-only-icon"
|
||||
on:click={() => deleteIdentity(identity.$id)}
|
||||
><span class="icon-trash" aria-hidden="true" /></Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -27,7 +27,7 @@
|
||||
<ul class="buttons-list">
|
||||
<li class="buttons-list-item">
|
||||
<Button
|
||||
download="backups.txt"
|
||||
download="appwrite-backups.txt"
|
||||
href={`data:application/octet-stream;charset=utf-8,${formattedBackupCodes}`}
|
||||
text>
|
||||
<span class="icon-download u-font-size-20" aria-hidden="true" />
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, FormList, InputDigits } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { AuthenticationFactor } from '@appwrite.io/console';
|
||||
import MfaChallengeFormList, { verify } from '$lib/components/mfaChallengeFormList.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
export let regenerateRecoveryCodes: () => Promise<void>;
|
||||
export let factors: Models.MfaFactors & { recoveryCode: boolean };
|
||||
|
||||
let code = '';
|
||||
let error = '';
|
||||
let challenge: Models.MfaChallenge = null;
|
||||
let code = '';
|
||||
|
||||
async function verify() {
|
||||
async function submit() {
|
||||
try {
|
||||
const challenge = await sdk.forConsole.account.createMfaChallenge(
|
||||
AuthenticationFactor.Totp
|
||||
);
|
||||
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
|
||||
await verify(challenge, code);
|
||||
await regenerateRecoveryCodes();
|
||||
show = false;
|
||||
regenerateRecoveryCodes();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
$: if (show) {
|
||||
error = '';
|
||||
challenge = null;
|
||||
code = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
@@ -30,16 +35,18 @@
|
||||
state="warning"
|
||||
headerDivider={false}
|
||||
{error}
|
||||
onSubmit={verify}
|
||||
onSubmit={submit}
|
||||
bind:show>
|
||||
<p>
|
||||
Are you sure you want to regenerate all recovery codes? All <b
|
||||
>previously generated recovery codes will become invalid.</b>
|
||||
</p>
|
||||
<p>Enter the 6-digit verification code generated by your authenticator app to continue.</p>
|
||||
<FormList>
|
||||
<InputDigits bind:value={code} required autofocus autoSubmit={false} />
|
||||
</FormList>
|
||||
<MfaChallengeFormList
|
||||
factors={{ ...factors, recoveryCode: false }}
|
||||
bind:challenge
|
||||
bind:code
|
||||
showVerifyButton={false}
|
||||
autoSubmit={false} />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
|
||||
@@ -6,3 +6,7 @@ export const factors = derived(
|
||||
page,
|
||||
($page) => $page.data.factors as Models.MfaFactors & { recoveryCode: boolean }
|
||||
);
|
||||
export const identities = derived(
|
||||
page,
|
||||
($page) => $page.data.identities.identities as Models.Identity[]
|
||||
);
|
||||
|
||||
@@ -20,10 +20,19 @@
|
||||
let showRegenerateRecoveryCodes = false;
|
||||
let codes: Models.MfaRecoveryCodes = null;
|
||||
|
||||
$: enabledFactors = Object.entries($factors)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([factor, _]) => factor);
|
||||
$: enabledMethods = enabledFactors.filter((factor) => factor !== 'recoveryCode');
|
||||
|
||||
async function updateMfa() {
|
||||
try {
|
||||
await sdk.forConsole.account.updateMFA(!$user.mfa);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
addNotification({
|
||||
message: `Multi-factor authentication has been ${$user.mfa ? 'enabled' : 'disabled'}`,
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.AccountUpdateMfa, { mfa: !$user.mfa });
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
@@ -63,6 +72,10 @@
|
||||
trackError(error, Submit.AccountRecoveryCodesUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: if (!showRecoveryCodes) {
|
||||
codes = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
@@ -87,99 +100,119 @@
|
||||
href="https://appwrite.io/docs/products/auth/mfa">Learn more.</Button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<div class="u-sep-block-end" style="padding-block-end: 8px;">
|
||||
<EyebrowHeading tag="h6" size={3} class="u-normal">Methods</EyebrowHeading>
|
||||
</div>
|
||||
<div
|
||||
class="method u-flex u-flex-vertical-mobile u-gap-16 u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-device-mobile" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">Authenticator app</span>
|
||||
<span
|
||||
>Use an authentication app to generate two-factor authentication
|
||||
codes.</span>
|
||||
</div>
|
||||
{#if $user.mfa}
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<div class="u-sep-block-end" style="padding-block-end: 8px;">
|
||||
<EyebrowHeading tag="h6" size={3} class="u-normal">Methods</EyebrowHeading>
|
||||
</div>
|
||||
<div class="method-button">
|
||||
{#if $factors.totp}
|
||||
<Button text secondary on:click={() => (showDelete = true)}
|
||||
>Delete</Button>
|
||||
{:else}
|
||||
<Button secondary on:click={() => (showSetup = true)}>Add</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $factors.email}
|
||||
<div
|
||||
class="u-flex u-main-space-between u-sep-block-end"
|
||||
class="method u-flex u-flex-vertical-mobile u-gap-16 u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-mail" aria-hidden="true" />
|
||||
<span class="icon-device-mobile" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">Email verification</span>
|
||||
<span>One-time codes will be sent to: {$user.email}</span>
|
||||
<span class="u-bold">Authenticator app</span>
|
||||
<span
|
||||
>Use an authentication app to generate two-factor authentication
|
||||
codes.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-button">
|
||||
{#if $factors.totp}
|
||||
<Button
|
||||
text
|
||||
class="is-not-mobile"
|
||||
on:click={() => (showDelete = true)}>Delete</Button>
|
||||
<Button
|
||||
text
|
||||
class="is-only-mobile"
|
||||
noMargin
|
||||
on:click={() => (showDelete = true)}>Delete</Button>
|
||||
{:else}
|
||||
<Button secondary on:click={() => (showSetup = true)}>Add</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $factors.email}
|
||||
<div
|
||||
class="u-flex u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-mail" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">Email verification</span>
|
||||
<span>One-time codes will be sent to: {$user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $factors.phone}
|
||||
<div
|
||||
class="u-flex u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-send" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">SMS verification</span>
|
||||
<span>One-time codes will be sent to: {$user.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if enabledMethods.length > 0}
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<div class="u-sep-block-end" style="padding-block-end: 8px;">
|
||||
<EyebrowHeading tag="h6" size={3} class="u-normal"
|
||||
>Recovery</EyebrowHeading>
|
||||
</div>
|
||||
<div
|
||||
class="method u-flex u-flex-vertical-mobile u-gap-16 u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-lock-open" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">Recovery codes</span>
|
||||
<span
|
||||
>Use in case you can't receive two-factor authentication
|
||||
codes.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-button">
|
||||
{#if $factors.recoveryCode}
|
||||
<Button
|
||||
class="method-button is-not-mobile"
|
||||
text
|
||||
on:click={() => (showRegenerateRecoveryCodes = true)}
|
||||
>Regenerate</Button>
|
||||
<Button
|
||||
class="method-button is-only-mobile"
|
||||
text
|
||||
noMargin
|
||||
on:click={() => (showRegenerateRecoveryCodes = true)}
|
||||
>Regenerate</Button>
|
||||
{:else}
|
||||
<Button
|
||||
class="method-button"
|
||||
secondary
|
||||
on:click={createRecoveryCodes}>View</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $factors.phone}
|
||||
<div
|
||||
class="u-flex u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-send" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">SMS verification</span>
|
||||
<span>One-time codes will be sent to: {$user.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<div class="u-sep-block-end" style="padding-block-end: 8px;">
|
||||
<EyebrowHeading tag="h6" size={3} class="u-normal">Recovery</EyebrowHeading>
|
||||
</div>
|
||||
<div
|
||||
class="method u-flex u-flex-vertical-mobile u-gap-16 u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-lock-open" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">Recovery codes</span>
|
||||
<span
|
||||
>Use in case you can't receive two-factor authentication codes.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-button">
|
||||
{#if $factors.recoveryCode}
|
||||
<Button
|
||||
class="method-button"
|
||||
text
|
||||
secondary
|
||||
on:click={() => (showRegenerateRecoveryCodes = true)}
|
||||
>Regenerate</Button>
|
||||
{:else}
|
||||
<Button class="method-button" secondary on:click={createRecoveryCodes}
|
||||
>View</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -190,7 +223,10 @@
|
||||
<DeleteMfa bind:showDelete />
|
||||
|
||||
<MfaRecoveryCodes bind:showRecoveryCodes {codes} />
|
||||
<MfaRegenerateCodes bind:show={showRegenerateRecoveryCodes} {regenerateRecoveryCodes} />
|
||||
<MfaRegenerateCodes
|
||||
bind:show={showRegenerateRecoveryCodes}
|
||||
{regenerateRecoveryCodes}
|
||||
factors={$factors} />
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
|
||||
@@ -47,7 +47,6 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
const prefs = await sdk.forConsole.account.getPrefs();
|
||||
const newPrefs = { ...prefs, organization: null };
|
||||
sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
localStorage.removeItem('organization');
|
||||
error(e.code, e.message);
|
||||
}
|
||||
};
|
||||
|
||||
+7
-12
@@ -24,22 +24,17 @@
|
||||
} else {
|
||||
await sdk.forConsole.teams.delete($organization.$id);
|
||||
}
|
||||
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
trackEvent(Submit.OrganizationDelete);
|
||||
const prefs = await sdk.forConsole.account.getPrefs();
|
||||
const newPrefs = { ...prefs, organization: null };
|
||||
await sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
if ($organizationList?.total > 1) {
|
||||
const account = await sdk.forConsole.account.get();
|
||||
const teamId =
|
||||
account.prefs.organization ??
|
||||
$organizationList.teams[0].$id === $organization.$id
|
||||
? $organizationList.teams[1].$id
|
||||
: $organizationList.teams[0].$id;
|
||||
|
||||
await goto(`${base}/console/organization-${teamId}`);
|
||||
await goto(`${base}/console/account/organizations`);
|
||||
} else {
|
||||
await goto(`${base}/console/onboarding`);
|
||||
}
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
trackEvent(Submit.OrganizationDelete);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import type { OrganizationUsage } from '$lib/sdk/billing';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import TotalMembers from './totalMembers.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -298,6 +299,7 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<TotalMembers members={data?.organizationMembers} />
|
||||
|
||||
<p class="text common-section u-color-text-gray">
|
||||
Metrics are estimates updated every 24 hours and may not accurately reflect your invoice.
|
||||
|
||||
@@ -36,9 +36,10 @@ export const load: PageLoad = async ({ params, parent }) => {
|
||||
endDate = currentInvoice.to;
|
||||
}
|
||||
|
||||
const [invoices, usage] = await Promise.all([
|
||||
const [invoices, usage, organizationMembers] = await Promise.all([
|
||||
sdk.forConsole.billing.listInvoices(org.$id, [Query.orderDesc('from')]),
|
||||
sdk.forConsole.billing.listUsage(params.organization, startDate, endDate)
|
||||
sdk.forConsole.billing.listUsage(params.organization, startDate, endDate),
|
||||
sdk.forConsole.teams.listMemberships(params.organization)
|
||||
]);
|
||||
|
||||
const queries: string[] = [];
|
||||
@@ -58,6 +59,7 @@ export const load: PageLoad = async ({ params, parent }) => {
|
||||
organizationUsage: usage,
|
||||
projectNames: projectNames.projects,
|
||||
invoices,
|
||||
currentInvoice
|
||||
currentInvoice,
|
||||
organizationMembers
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { AvatarInitials, CardGrid, Heading, Paginator } from '$lib/components';
|
||||
import { EmptyCardCloud } from '$lib/components/billing';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { plansInfo, tierToPlan } from '$lib/stores/billing';
|
||||
import { newMemberModal, organization } from '$lib/stores/organization';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let members: Models.MembershipList;
|
||||
|
||||
$: total = members?.total ?? 0;
|
||||
$: plan = $plansInfo?.get($organization?.billingPlan);
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Members</Heading>
|
||||
|
||||
<p class="text">The number of members in your organization.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $organization.billingPlan !== BillingPlan.STARTER}
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p>
|
||||
<span class="heading-level-4">{total}</span>
|
||||
<span class="body-text-1 u-bold">Members</span>
|
||||
</p>
|
||||
<Button secondary on:click={() => newMemberModal.set(true)}>
|
||||
<span class="icon-plus"></span> <span class="text">Invite</span>
|
||||
</Button>
|
||||
</div>
|
||||
<p class="body-text-2">
|
||||
Unlimited
|
||||
<span
|
||||
class="icon-info"
|
||||
use:tooltip={{
|
||||
content: `You can add unlimited organization members on the ${tierToPlan($organization.billingPlan).name} plan for ${formatCurrency(plan.addons.member.price)} each per billing period.`
|
||||
}}></span>
|
||||
</p>
|
||||
</div>
|
||||
<Paginator
|
||||
items={members.memberships}
|
||||
let:paginatedItems
|
||||
hideFooter={members?.total <= 5}>
|
||||
<TableScroll noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>Members</TableCellHead>
|
||||
<TableCellHead>Joined</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each paginatedItems as member}
|
||||
<TableRow>
|
||||
<TableCell title="name">
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
<AvatarInitials size={32} name={member.userName} />
|
||||
<span class="text u-trim">
|
||||
{member.userName ? member.userName : member.userEmail}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCellText title="joined">
|
||||
{member.joined
|
||||
? toLocaleDate(member.joined)
|
||||
: toLocaleDate(member.$createdAt)}
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
</Paginator>
|
||||
{:else}
|
||||
<EmptyCardCloud service="members" eventSource="organization_usage" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -2,14 +2,15 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputSwitch, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
export let provider: Provider;
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
@@ -18,11 +19,13 @@
|
||||
let teamID: string = null;
|
||||
let p8: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) ({ keyID, teamID, p8 } = JSON.parse(provider.secret));
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
@@ -44,7 +47,7 @@
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer">
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer">
|
||||
visit the docs.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let provider: Provider;
|
||||
const projectId = $page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
@@ -15,14 +18,15 @@
|
||||
let clientSecret: string = null;
|
||||
let auth0Domain: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) ({ clientSecret, auth0Domain } = JSON.parse(provider.secret));
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const projectId = $page.params.project;
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
@@ -45,7 +49,7 @@
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let provider: Provider;
|
||||
const projectId = $page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
@@ -15,14 +18,15 @@
|
||||
let clientSecret: string = null;
|
||||
let authentikDomain: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) ({ clientSecret, authentikDomain } = JSON.parse(provider.secret));
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const projectId = $page.params.project;
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
@@ -45,7 +49,7 @@
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let provider: Provider;
|
||||
const projectId = $page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
@@ -15,14 +18,15 @@
|
||||
let clientSecret: string = null;
|
||||
let endpoint: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) ({ clientSecret, endpoint } = JSON.parse(provider.secret));
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const projectId = $page.params.project;
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
@@ -43,7 +47,7 @@
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
|
||||
@@ -2,24 +2,27 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
|
||||
export let provider: Provider;
|
||||
export let show = false;
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let enabled: boolean = null;
|
||||
let appId: string = null;
|
||||
let secret: string = null;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
enabled ??= provider.enabled;
|
||||
appId ??= provider.appId;
|
||||
secret ??= provider.secret;
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
let error: string;
|
||||
@@ -41,7 +44,7 @@
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
|
||||
@@ -2,24 +2,27 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
|
||||
export let provider: Provider;
|
||||
export let show = false;
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let enabled: boolean = null;
|
||||
let appId: string = null;
|
||||
let secret: string = null;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
enabled ??= provider.enabled;
|
||||
appId ??= provider.appId;
|
||||
secret ??= provider.secret;
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
let error: string;
|
||||
@@ -41,7 +44,7 @@
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let provider: Provider;
|
||||
const projectId = $page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
@@ -15,15 +18,15 @@
|
||||
let clientSecret: string = null;
|
||||
let tenantID: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) ({ clientSecret, tenantID } = JSON.parse(provider.secret));
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
@@ -44,7 +47,7 @@
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer">
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer">
|
||||
visit the docs.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
export let provider: Provider;
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
@@ -20,6 +21,7 @@
|
||||
let tokenEndpoint: string = null;
|
||||
let userinfoEndpoint: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
// secret is valid if clientSecret is set and either wellKnownEndpoint or all of the other endpoints are set
|
||||
$: isValidSecret =
|
||||
@@ -38,6 +40,7 @@
|
||||
userinfoEndpoint
|
||||
} = JSON.parse(provider.secret));
|
||||
}
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
@@ -67,7 +70,7 @@
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
export let provider: Provider;
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
@@ -18,6 +19,7 @@
|
||||
let oktaDomain: string = null;
|
||||
let authorizationServerId: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
@@ -25,6 +27,7 @@
|
||||
if (provider.secret) {
|
||||
({ clientSecret, oktaDomain, authorizationServerId } = JSON.parse(provider.secret));
|
||||
}
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
@@ -49,7 +52,7 @@
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
|
||||
@@ -8,18 +8,15 @@
|
||||
import { app } from '$lib/stores/app';
|
||||
import { authMethods, type AuthMethod } from '$lib/stores/auth-methods';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { OAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import { oAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { project } from '../../store';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
let showProvider = false;
|
||||
|
||||
$: {
|
||||
authMethods.load($project);
|
||||
OAuthProviders.load($project);
|
||||
}
|
||||
let selectedProvider: Models.AuthProvider | null = null;
|
||||
|
||||
async function authUpdate(box: AuthMethod) {
|
||||
try {
|
||||
@@ -42,10 +39,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
let selectedProvider: Provider | null = null;
|
||||
$: authMethods.load($project);
|
||||
</script>
|
||||
|
||||
{#if $authMethods && $OAuthProviders}
|
||||
{#if $authMethods && $project}
|
||||
<Container>
|
||||
<CardGrid>
|
||||
<Heading tag="h2" size="7">Auth methods</Heading>
|
||||
@@ -67,34 +64,37 @@
|
||||
<section class="common-section">
|
||||
<h2 class="heading-level-6 common-section">OAuth2 Providers</h2>
|
||||
<ul class="grid-box common-section">
|
||||
{#each $OAuthProviders.providers
|
||||
{#each $project.oAuthProviders
|
||||
.filter((p) => p.name !== 'Mock')
|
||||
.sort((a, b) => (a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1)) as provider}
|
||||
<li class="grid-box-item">
|
||||
<button
|
||||
class="card u-flex u-flex-vertical u-cross-center u-width-full-line"
|
||||
on:click={() => {
|
||||
selectedProvider = provider;
|
||||
showProvider = true;
|
||||
trackEvent(`click_select_provider`, {
|
||||
provider: provider.key.toLowerCase()
|
||||
});
|
||||
}}>
|
||||
<div class="avatar">
|
||||
<img
|
||||
height="20"
|
||||
width="20"
|
||||
src={`/icons/${$app.themeInUse}/color/${provider.icon}.svg`}
|
||||
alt={provider.name} />
|
||||
</div>
|
||||
<p class="u-margin-block-start-8">{provider.name}</p>
|
||||
<div class="u-margin-block-start-24">
|
||||
<Pill success={provider.enabled}>
|
||||
{provider.enabled ? 'enabled' : 'disabled'}
|
||||
</Pill>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{@const oAuthProvider = oAuthProviders[provider.key]}
|
||||
{#if oAuthProvider}
|
||||
<li class="grid-box-item">
|
||||
<button
|
||||
class="card u-flex u-flex-vertical u-cross-center u-width-full-line"
|
||||
on:click={() => {
|
||||
selectedProvider = provider;
|
||||
showProvider = true;
|
||||
trackEvent(`click_select_provider`, {
|
||||
provider: provider.key.toLowerCase()
|
||||
});
|
||||
}}>
|
||||
<div class="avatar">
|
||||
<img
|
||||
height="20"
|
||||
width="20"
|
||||
src={`/icons/${$app.themeInUse}/color/${oAuthProvider.icon}.svg`}
|
||||
alt={provider.name} />
|
||||
</div>
|
||||
<p class="u-margin-block-start-8">{provider.name}</p>
|
||||
<div class="u-margin-block-start-24">
|
||||
<Pill success={provider.enabled}>
|
||||
{provider.enabled ? 'enabled' : 'disabled'}
|
||||
</Pill>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
@@ -102,8 +102,9 @@
|
||||
{/if}
|
||||
|
||||
{#if selectedProvider && showProvider}
|
||||
{@const oAuthProvider = oAuthProviders[selectedProvider.key]}
|
||||
<svelte:component
|
||||
this={selectedProvider.component}
|
||||
this={oAuthProvider.component}
|
||||
bind:provider={selectedProvider}
|
||||
bind:show={showProvider}
|
||||
on:close={() => {
|
||||
|
||||
@@ -3,13 +3,12 @@ import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { OAuthProvider } from '@appwrite.io/console';
|
||||
import { OAuthProvider, type Models } from '@appwrite.io/console';
|
||||
|
||||
type Args = {
|
||||
projectId: string;
|
||||
provider: Provider;
|
||||
provider: Models.AuthProvider;
|
||||
appId: string;
|
||||
secret: string;
|
||||
enabled: boolean;
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { OAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { oAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import { app } from '$lib/stores/app';
|
||||
|
||||
export let columns: Column[];
|
||||
@@ -30,7 +29,6 @@
|
||||
let selectedIds: string[] = [];
|
||||
let showDelete = false;
|
||||
let deleting = false;
|
||||
let providerLookup: Record<string, Provider> = {};
|
||||
|
||||
async function handleDelete() {
|
||||
showDelete = false;
|
||||
@@ -58,12 +56,6 @@
|
||||
showDelete = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: OAuthProviders.load($project);
|
||||
$: providerLookup = $OAuthProviders.providers.reduce((acc, provider) => {
|
||||
acc[provider.key] = provider;
|
||||
return acc;
|
||||
}, {});
|
||||
</script>
|
||||
|
||||
<TableScroll>
|
||||
@@ -93,7 +85,7 @@
|
||||
</TableCell>
|
||||
{/key}
|
||||
{:else if column.id === 'provider'}
|
||||
{@const provider = providerLookup[identity[column.id]]}
|
||||
{@const provider = oAuthProviders[identity[column.id]]}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
<div class="u-inline-flex u-cross-center u-gap-8">
|
||||
<div class="avatar is-size-small">
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<script context="module" lang="ts">
|
||||
import { Query, type Models, RelationshipType, RelationMutate } from '@appwrite.io/console';
|
||||
import { ID, Query, type Models, RelationshipType, RelationMutate } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
export async function submitRelationship(
|
||||
@@ -21,7 +21,7 @@
|
||||
data.relationType,
|
||||
data.twoWay,
|
||||
data.key,
|
||||
data.twoWayKey,
|
||||
data.twoWayKey ?? ID.unique(),
|
||||
data.onDelete
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export const runtimesList = derived(page, async ($page) => {
|
||||
const { runtimes } = (await $page.data.runtimesList) as Models.RuntimeList;
|
||||
const filtered = runtimes.filter((runtime) => !runtime.$id.startsWith('python-ml'));
|
||||
return { runtimes: filtered, total: filtered.length };
|
||||
});
|
||||
export const runtimesList = derived(
|
||||
page,
|
||||
async ($page) => (await $page.data.runtimesList) as Models.RuntimeList
|
||||
);
|
||||
|
||||
export const baseRuntimesList = derived(runtimesList, async ($runtimesList) => {
|
||||
const baseRuntimes = new Map<string, Models.Runtime>();
|
||||
|
||||
@@ -24,16 +24,20 @@
|
||||
import Status from '$lib/components/status.svelte';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { readOnly } from '$lib/stores/billing';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let data;
|
||||
let details: string | null = null;
|
||||
let migration: Models.Migration = null;
|
||||
let showExport = false;
|
||||
let showMigration = false;
|
||||
|
||||
const getStatus = (status: string) => {
|
||||
if (status === 'failed') {
|
||||
return 'failed';
|
||||
} else if (status === 'completed') {
|
||||
return 'completed';
|
||||
} else if (status === 'processing') {
|
||||
return 'processing';
|
||||
}
|
||||
|
||||
return 'pending';
|
||||
@@ -129,6 +133,11 @@
|
||||
// JSON.stringify(migrationData)
|
||||
// )}`;
|
||||
};
|
||||
|
||||
function showDetails(m: Models.Migration) {
|
||||
showMigration = true;
|
||||
migration = m;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -182,7 +191,7 @@
|
||||
</TableCell>
|
||||
<TableCell title="">
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary on:click={() => (details = entry.$id)}
|
||||
<Button secondary on:click={() => showDetails(entry)}
|
||||
>Details</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -296,7 +305,7 @@
|
||||
|
||||
<ExportModal bind:show={showExport} />
|
||||
|
||||
<Details bind:migrationId={details} migrations={data.migrations} />
|
||||
<Details {migration} bind:show={showMigration} />
|
||||
|
||||
<style lang="scss">
|
||||
.import-box {
|
||||
|
||||
@@ -8,12 +8,8 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { ResourcesFriendly } from '$lib/stores/migration';
|
||||
|
||||
export let migrations: Models.Migration[] = [];
|
||||
export let migrationId: string = null;
|
||||
|
||||
$: details = migrations.find((migration) => migration.$id === migrationId);
|
||||
|
||||
$: show = !!details && migrationId !== null;
|
||||
export let migration: Models.Migration = null;
|
||||
export let show = false;
|
||||
|
||||
type StatusCounters = {
|
||||
[resource in 'Database' | 'Collection' | 'Function' | 'Users']?: StatusCounter;
|
||||
@@ -23,7 +19,7 @@
|
||||
};
|
||||
|
||||
$: statusCounters = parseIfString(
|
||||
(details?.statusCounters as unknown as string) || '{}'
|
||||
(migration?.statusCounters as unknown as string) || '{}'
|
||||
) as StatusCounters;
|
||||
|
||||
const hasError = (counter: StatusCounter) => {
|
||||
@@ -49,9 +45,9 @@
|
||||
let tab = 'details' as 'details' | 'logs';
|
||||
</script>
|
||||
|
||||
<Modal bind:show on:close={() => (migrationId = null)} size="big">
|
||||
<Modal bind:show size="big">
|
||||
<svelte:fragment slot="title">
|
||||
{#if details.status === 'failed'}
|
||||
{#if migration.status === 'failed'}
|
||||
Resolve migration issues
|
||||
{:else}
|
||||
Migration details
|
||||
@@ -63,13 +59,13 @@
|
||||
</Tabs>
|
||||
|
||||
{#if tab === 'logs'}
|
||||
<Code code={JSON.stringify(details, null, 2)} language="json" allowScroll />
|
||||
<Code code={JSON.stringify(migration, null, 2)} language="json" allowScroll />
|
||||
{:else if tab === 'details'}
|
||||
<div class="box meta">
|
||||
<span>Date</span>
|
||||
<span>{toLocaleDateTime(details.$createdAt)}</span>
|
||||
<span>{toLocaleDateTime(migration.$createdAt)}</span>
|
||||
<span>Source</span>
|
||||
<span>{details.source}</span>
|
||||
<span>{migration.source}</span>
|
||||
</div>
|
||||
|
||||
{#if Object.values(statusCounters).some(hasError)}
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { VARS } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import {
|
||||
localeTimezoneName,
|
||||
utcHourToLocaleHour,
|
||||
utcWeekDayToLocaleWeekDay,
|
||||
type WeekDay
|
||||
} from '$lib/helpers/date';
|
||||
|
||||
onDestroy(() => {
|
||||
$supportData = {
|
||||
@@ -76,6 +82,16 @@
|
||||
}
|
||||
|
||||
$wizard.finalAction = handleSubmit;
|
||||
|
||||
const workTimings = {
|
||||
start: '09:00',
|
||||
end: '17:00',
|
||||
startDay: 'Monday' as WeekDay,
|
||||
endDay: 'Friday' as WeekDay
|
||||
};
|
||||
|
||||
$: supportTimings = `${utcHourToLocaleHour(workTimings.start)} - ${utcHourToLocaleHour(workTimings.end)} ${localeTimezoneName()}`;
|
||||
$: supportWeekDays = `${utcWeekDayToLocaleWeekDay(workTimings.startDay, workTimings.start)} - ${utcWeekDayToLocaleWeekDay(workTimings.endDay, workTimings.end)}`;
|
||||
</script>
|
||||
|
||||
<Wizard title="Contact us" steps={stepsComponents} finalAction="Submit" on:exit={resetData}>
|
||||
@@ -85,7 +101,9 @@
|
||||
If you found a bug or have questions, please reach out to the Appwrite team. We try to
|
||||
respond to all messages within our office hours.
|
||||
</p>
|
||||
<p class="text u-margin-block-start-32">Available: <b>Mon-Fri 09:00 - 17:00 UTC</b></p>
|
||||
<p class="text u-margin-block-start-32">
|
||||
Available: <b>{supportWeekDays}, {supportTimings}</b>
|
||||
</p>
|
||||
<div class="u-flex u-gap-4 u-cross-center">
|
||||
<span>Currently:</span>
|
||||
{#if isSupportOnline()}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { OAuthProvider } from '@appwrite.io/console';
|
||||
import { redirectTo } from '$routes/store';
|
||||
import { user } from '$lib/stores/user';
|
||||
|
||||
let mail: string, pass: string, disabled: boolean;
|
||||
|
||||
@@ -26,18 +27,18 @@
|
||||
disabled = true;
|
||||
await sdk.forConsole.account.createEmailPasswordSession(mail, pass);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Successfully logged in.'
|
||||
});
|
||||
trackEvent(Submit.AccountLogin);
|
||||
if ($user) {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Successfully logged in.'
|
||||
});
|
||||
}
|
||||
if ($redirectTo) {
|
||||
window.location.href = $redirectTo;
|
||||
return;
|
||||
}
|
||||
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountCreate);
|
||||
|
||||
if ($page.url.searchParams) {
|
||||
const redirect = $page.url.searchParams.get('redirect');
|
||||
$page.url.searchParams.delete('redirect');
|
||||
@@ -55,7 +56,7 @@
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AccountCreate);
|
||||
trackError(error, Submit.AccountLogin);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-123
@@ -1,58 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { Button, Form, FormItem, FormList, InputDigits, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Unauthenticated } from '$lib/layout';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import MfaChallengeFormList, { verify } from '$lib/components/mfaChallengeFormList.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
|
||||
import { addNotification } from '$lib/stores/notifications.js';
|
||||
|
||||
export let data;
|
||||
|
||||
let code: string;
|
||||
let disabled: boolean;
|
||||
let challenge: Models.MfaChallenge;
|
||||
let challengeType: AuthenticationFactor;
|
||||
|
||||
const factors = data.factors as Models.MfaFactors & { recoveryCode: boolean };
|
||||
const enabledFactors = Object.entries(factors).filter(([_, enabled]) => enabled);
|
||||
|
||||
async function createChallenge(factor: AuthenticationFactor) {
|
||||
challengeType = factor;
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const enabledNonRecoveryFactors = enabledFactors.filter(
|
||||
([factor, _]) => factor != 'recoveryCode'
|
||||
);
|
||||
if (enabledNonRecoveryFactors.length == 1) {
|
||||
if (factors.phone) {
|
||||
createChallenge(AuthenticationFactor.Phone);
|
||||
} else if (factors.email) {
|
||||
createChallenge(AuthenticationFactor.Email);
|
||||
}
|
||||
}
|
||||
});
|
||||
let disabled = false;
|
||||
let challenge: Models.MfaChallenge = null;
|
||||
let code = '';
|
||||
|
||||
async function back() {
|
||||
await sdk.forConsole.account.deleteSession('current');
|
||||
await goto(`${base}/`);
|
||||
}
|
||||
|
||||
async function verify() {
|
||||
async function submit() {
|
||||
disabled = true;
|
||||
try {
|
||||
disabled = true;
|
||||
if (challenge == null) {
|
||||
await createChallenge(AuthenticationFactor.Totp);
|
||||
}
|
||||
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountCreate);
|
||||
await verify(challenge, code);
|
||||
if ($page.url.searchParams) {
|
||||
const redirect = $page.url.searchParams.get('redirect');
|
||||
$page.url.searchParams.delete('redirect');
|
||||
@@ -65,12 +38,11 @@
|
||||
await goto(`${base}/console`);
|
||||
}
|
||||
} catch (error) {
|
||||
disabled = false;
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AccountCreate);
|
||||
disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -91,89 +63,8 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">Verify your identity</svelte:fragment>
|
||||
<svelte:fragment>
|
||||
<Form onSubmit={verify}>
|
||||
<FormList>
|
||||
{#if factors.totp && (challengeType == AuthenticationFactor.Totp || challengeType == null)}
|
||||
<p class="body-text-1">
|
||||
Enter below a 6-digit one-time code generated by your authentication app.
|
||||
</p>
|
||||
<InputDigits bind:value={code} required autofocus />
|
||||
{:else if challengeType == AuthenticationFactor.Email}
|
||||
<p class="body-text-1">
|
||||
A 6-digit verification code was sent to your email, enter it below.
|
||||
</p>
|
||||
<InputDigits bind:value={code} required autofocus />
|
||||
{:else if challengeType == AuthenticationFactor.Phone}
|
||||
<p class="body-text-1">
|
||||
A 6-digit verification code was sent to your phone, enter it below.
|
||||
</p>
|
||||
<InputDigits bind:value={code} required autofocus />
|
||||
{:else if challengeType == AuthenticationFactor.Recoverycode}
|
||||
<p class="body-text-1">
|
||||
Enter below one of the recovery codes you received when enabling MFA for
|
||||
this account.
|
||||
</p>
|
||||
<InputText id="recovery-code" bind:value={code} required autofocus />
|
||||
{/if}
|
||||
<FormItem>
|
||||
<Button fullWidth submit {disabled}>Verify</Button>
|
||||
</FormItem>
|
||||
{#if enabledFactors.length > 1}
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
{#if factors.totp && challengeType != null && challengeType != AuthenticationFactor.Totp}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Totp)}>
|
||||
<span
|
||||
class="icon-device-mobile u-font-size-20"
|
||||
aria-hidden="true" />
|
||||
<span class="text">Authenticator app</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.email && challengeType != AuthenticationFactor.Email}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Email)}>
|
||||
<span class="icon-mail u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Email verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.phone && challengeType != AuthenticationFactor.Phone}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Phone)}>
|
||||
<span class="icon-chat-alt u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Phone verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.recoveryCode && challengeType != AuthenticationFactor.Recoverycode}
|
||||
<FormItem>
|
||||
<Button
|
||||
text
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() =>
|
||||
createChallenge(AuthenticationFactor.Recoverycode)}>
|
||||
<span class="text">Use recovery code</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
<Form onSubmit={submit} class="body-text-1">
|
||||
<MfaChallengeFormList {factors} bind:challenge bind:code bind:disabled />
|
||||
</Form>
|
||||
</svelte:fragment>
|
||||
</Unauthenticated>
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
export function getOrganizationIdFromUrl(pathname: string) {
|
||||
const regex = /\/console\/organization-([^/]+)(\/.*)?/;
|
||||
const match = pathname.match(regex);
|
||||
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
throw new Error('Organization ID not found in pathname');
|
||||
}
|
||||
|
||||
export function getProjectIdFromUrl(pathname: string) {
|
||||
const regex = /\/console\/project-([^/]+)(\/.*)?/;
|
||||
const match = pathname.match(regex);
|
||||
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
throw new Error('Project ID not found in pathname');
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { test } from '@playwright/test';
|
||||
import { registerUserStep } from '../steps/account';
|
||||
import { createFreeProject } from '../steps/free-project';
|
||||
|
||||
test('onboarding - free tier', async ({ page }) => {
|
||||
await registerUserStep(page);
|
||||
await createFreeProject(page);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { test } from '@playwright/test';
|
||||
import { registerUserStep } from '../steps/account';
|
||||
import { createProProject } from '../steps/pro-project';
|
||||
|
||||
test('onboarding - pro', async ({ page }) => {
|
||||
await registerUserStep(page);
|
||||
await createProProject(page);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { test } from '@playwright/test';
|
||||
import { registerUserStep } from '../steps/account';
|
||||
import { createFreeProject } from '../steps/free-project';
|
||||
import { enterAddress, enterCreditCard } from '../steps/pro-project';
|
||||
|
||||
test('upgrade - free tier', async ({ page }) => {
|
||||
await registerUserStep(page);
|
||||
await createFreeProject(page);
|
||||
await test.step('upgrade project', async () => {
|
||||
await page.getByRole('button', { name: 'upgrade' }).click();
|
||||
await page.locator('input[value="tier-1"]').click();
|
||||
await page.getByRole('button', { name: 'next' }).click();
|
||||
await enterCreditCard(page);
|
||||
await enterAddress(page);
|
||||
// skip members
|
||||
await page.getByRole('button', { name: 'next' }).click();
|
||||
await page.getByRole('button', { name: 'create' }).click();
|
||||
await page.waitForURL('/console/organization-**');
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
/*TODO: Things to test in login:
|
||||
- presence of forgot password link
|
||||
- validation message on wrong input
|
||||
- correct response and redirect after login
|
||||
- logout works
|
||||
- back button does not log out user
|
||||
- forward button does not log in user after logout
|
||||
- limit to total number of login attempts
|
||||
-
|
||||
*/
|
||||
|
||||
test('login page has inputs and button', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
const mail = page.locator('id=email');
|
||||
const pass = page.locator('id=password');
|
||||
const button = page.locator('button:has-text("Sign in")');
|
||||
expect(await mail.isVisible());
|
||||
expect(await pass.isVisible());
|
||||
expect(await button.isVisible());
|
||||
});
|
||||
|
||||
test('login page has a working sign up link', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.waitForTimeout(100);
|
||||
const signup = page.locator('a[href="/register"]');
|
||||
expect(await signup.isVisible());
|
||||
});
|
||||
|
||||
test('login page inputs are navigable by keyboard', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
const mail = page.locator('id=email');
|
||||
await mail.focus();
|
||||
await page.keyboard.type('wrongemail@apppwrite.io');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.type('password');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Enter');
|
||||
expect(await page.locator('.toaster-item').isVisible());
|
||||
expect(await page.locator('text=Invalid credentials').isVisible());
|
||||
});
|
||||
|
||||
test('login page shows error & response is 401 with wrong inputs', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('id=email', 'wrongemail@apppwrite.io');
|
||||
await page.fill('id=password', 'wrongpassword');
|
||||
await page.click('button:has-text("Sign in")');
|
||||
page.on('response', (response) => {
|
||||
expect(response.status()).toBe(401);
|
||||
});
|
||||
expect(await page.locator('.toaster-item').isVisible());
|
||||
expect(await page.locator('text=Invalid credentials').isVisible());
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('register page has inputs', async ({ page }) => {
|
||||
await page.goto('/register');
|
||||
const name = page.locator('id=name');
|
||||
const mail = page.locator('id=email');
|
||||
const pass = page.locator('id=password');
|
||||
expect(await name.isVisible());
|
||||
expect(await mail.isVisible());
|
||||
expect(await pass.isVisible());
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { test, type Page } from '@playwright/test';
|
||||
|
||||
type Metadata = {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export function registerUserStep(page: Page): Promise<Metadata> {
|
||||
return test.step('register user', async () => {
|
||||
const seed = crypto.randomUUID();
|
||||
await page.goto('/register');
|
||||
await page.getByRole('button', { name: 'only required' }).click();
|
||||
const inputs = {
|
||||
name: page.locator('id=name'),
|
||||
email: page.locator('id=email'),
|
||||
password: page.locator('id=password'),
|
||||
terms: page.locator('id=terms')
|
||||
};
|
||||
const values = {
|
||||
name: 'testuser ' + seed,
|
||||
email: 'testuser+' + seed + '@apppwrite.io',
|
||||
password: 'testuser+' + seed + '@apppwrite.io'
|
||||
};
|
||||
await inputs.name.fill(values.name);
|
||||
await inputs.email.fill(values.email);
|
||||
await inputs.password.fill(values.password);
|
||||
await inputs.terms.check();
|
||||
await page.getByRole('button', { name: 'Sign up', exact: true }).click();
|
||||
await page.waitForURL('/console/onboarding');
|
||||
|
||||
return values;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url';
|
||||
|
||||
type Metadata = {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
export async function createFreeProject(page: Page): Promise<Metadata> {
|
||||
const organizationId = await test.step('create organization', async () => {
|
||||
await page.goto('/console');
|
||||
await page.waitForURL('/console/onboarding');
|
||||
await page.locator('id=name').fill('test org');
|
||||
await page.locator('id=plan').selectOption('tier-0');
|
||||
await page.getByRole('button', { name: 'get started' }).click();
|
||||
await page.waitForURL('/console/organization-**');
|
||||
return getOrganizationIdFromUrl(page.url());
|
||||
});
|
||||
|
||||
const projectId = await test.step('create project', async () => {
|
||||
await page.waitForURL('/console/organization-**');
|
||||
await page.getByRole('button', { name: 'create project' }).first().click();
|
||||
await page.locator('id=name').fill('test project');
|
||||
await page.getByRole('button', { name: 'next' }).click();
|
||||
await page.locator('label').filter({ hasText: 'Frankfurt' }).click();
|
||||
await page.getByRole('button', { name: 'create' }).click();
|
||||
await page.waitForURL('/console/project-**/overview/platforms');
|
||||
expect(page.url()).toContain('/console/project-');
|
||||
|
||||
return getProjectIdFromUrl(page.url());
|
||||
});
|
||||
|
||||
return {
|
||||
id: projectId,
|
||||
organizationId
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url';
|
||||
|
||||
type Metadata = {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
export async function enterCreditCard(page: Page) {
|
||||
await page.getByPlaceholder('cardholder').fill('Test User');
|
||||
const stripe = page.frameLocator('[title="Secure payment input frame"]');
|
||||
await stripe.locator('id=Field-numberInput').fill('4242424242424242');
|
||||
await stripe.locator('id=Field-expiryInput').fill('1250');
|
||||
await stripe.locator('id=Field-cvcInput').fill('123');
|
||||
await stripe.locator('id=Field-countryInput').selectOption('DE');
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
}
|
||||
|
||||
export async function enterAddress(page: Page) {
|
||||
await page.locator('id=country').selectOption('US');
|
||||
await page.locator('id=address').fill('123 Test St');
|
||||
await page.locator('id=city').fill('Test City');
|
||||
await page.locator('id=state').fill('Test State');
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
}
|
||||
|
||||
export async function createProProject(page: Page): Promise<Metadata> {
|
||||
const organizationId = await test.step('create organization', async () => {
|
||||
await page.goto('/console');
|
||||
await page.waitForURL('/console/onboarding');
|
||||
await page.locator('id=name').fill('test org');
|
||||
await page.locator('id=plan').selectOption('tier-1');
|
||||
await page.getByRole('button', { name: 'get started' }).click();
|
||||
await enterCreditCard(page);
|
||||
await enterAddress(page);
|
||||
// skip members
|
||||
await page.getByRole('button', { name: 'next' }).click();
|
||||
// start pro trial
|
||||
await page.getByRole('button', { name: 'create' }).click();
|
||||
await page.waitForURL('/console/organization-**');
|
||||
|
||||
return getOrganizationIdFromUrl(page.url());
|
||||
});
|
||||
|
||||
const projectId = await test.step('create project', async () => {
|
||||
await page.waitForURL('/console/organization-**');
|
||||
await page.getByRole('button', { name: 'create project' }).first().click();
|
||||
await page.getByPlaceholder('project name').fill('test project');
|
||||
await page.getByRole('button', { name: 'next' }).click();
|
||||
await page.locator('label').filter({ hasText: 'frankfurt' }).click();
|
||||
await page.getByRole('button', { name: 'create' }).click();
|
||||
await page.waitForURL('/console/project-**/overview/platforms');
|
||||
expect(page.url()).toContain('/console/project-');
|
||||
|
||||
return getProjectIdFromUrl(page.url());
|
||||
});
|
||||
|
||||
return {
|
||||
id: projectId,
|
||||
organizationId
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import type { WeekDay } from '$lib/helpers/date';
|
||||
import {
|
||||
toLocaleDate,
|
||||
toLocaleDateTime,
|
||||
@@ -6,7 +7,9 @@ import {
|
||||
isValidDate,
|
||||
diffDays,
|
||||
toLocaleDateISO,
|
||||
toLocaleTimeISO
|
||||
toLocaleTimeISO,
|
||||
utcHourToLocaleHour,
|
||||
utcWeekDayToLocaleWeekDay
|
||||
} from '$lib/helpers/date';
|
||||
|
||||
describe('local date', () => {
|
||||
@@ -121,3 +124,35 @@ describe('diff days', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('utc hour to local hour', () => {
|
||||
const entries: Array<[string, string]> = [
|
||||
['09:00', '04:00'],
|
||||
['10:45', '05:45'],
|
||||
['11:59', '06:59'],
|
||||
['17:00', '12:00'],
|
||||
['22:36', '17:36']
|
||||
];
|
||||
|
||||
entries.forEach(([value, expected]) => {
|
||||
it(value, () => {
|
||||
expect(utcHourToLocaleHour(value)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('utc week day to local week day', () => {
|
||||
const entries: Array<[WeekDay, string, string]> = [
|
||||
['Sunday', '00:00', 'Sat'],
|
||||
['Sunday', '22:00', 'Sun'],
|
||||
['Wednesday', '13:00', 'Wed'],
|
||||
['Friday', '23:59', 'Fri'],
|
||||
['Saturday', '10:00', 'Sat']
|
||||
];
|
||||
|
||||
entries.forEach(([weekDay, utcTimeString, expected]) => {
|
||||
it(`${weekDay} ${utcTimeString}`, () => {
|
||||
expect(utcWeekDayToLocaleWeekDay(weekDay, utcTimeString)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user