Skip to content

Commit 740a735

Browse files
authored
Merge pull request #120 from devforth/next
Next
2 parents 6641e18 + ca7d409 commit 740a735

File tree

20 files changed

+408
-69
lines changed

20 files changed

+408
-69
lines changed

adapters/install-adapters.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ADAPTERS="adminforth-completion-adapter-open-ai-chat-gpt adminforth-email-adapter-aws-ses"
1+
ADAPTERS="adminforth-completion-adapter-open-ai-chat-gpt adminforth-email-adapter-aws-ses adminforth-google-oauth-adapter adminforth-github-oauth-adapter"
22

33
# for each plugin
44
for adapter in $ADAPTERS; do

adminforth/documentation/blog/2024-11-14-compose-ec2-deployment-ci/index.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,20 +365,19 @@ terraform apply -auto-approve
365365

366366
## Step 6 - Migrate state to the cloud
367367

368-
First deployment had to create S3 bucket and DynamoDB table for storing Terraform state. Now we need to migrate the state to the cloud.
368+
First deployment had to create S3 bucket for storing Terraform state. Now we need to migrate the state to the cloud.
369369

370370
Add to the end of `main.tf`:
371371

372372
```hcl title="main.tf"
373373
374-
# Configure the backend to use the S3 bucket and DynamoDB table
374+
# Configure the backend to use the S3 bucket
375375
terraform {
376376
backend "s3" {
377377
bucket = "<your_app_name>-terraform-state"
378378
key = "state.tfstate" # Define a specific path for the state file
379379
region = "eu-central-1"
380380
profile = "myaws"
381-
dynamodb_table = "<your_app_name>-terraform-lock-table"
382381
use_lockfile = true
383382
}
384383
}
@@ -423,7 +422,7 @@ jobs:
423422
- name: Set up Terraform
424423
uses: hashicorp/setup-terraform@v2
425424
with:
426-
terraform_version: 1.4.6
425+
terraform_version: 1.10.1
427426
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
428427
- name: Start building
429428
env:

adminforth/documentation/docs/tutorial/03-Customization/06-customPages.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,4 +419,44 @@ mongoose, or just use raw SQL queries against your tables.
419419
420420
Demo:
421421
422-
![alt text](dashDemo.gif)
422+
![alt text](dashDemo.gif)
423+
424+
## Custom pages without menu item
425+
426+
Sometimes you might need to add custom page but don't want to add it to the menu.
427+
428+
In this case you can add custom page using `customization.customPages` option:
429+
430+
```ts title="/index.ts"
431+
new AdminForth({
432+
// ...
433+
customization: {
434+
customPages: [
435+
{
436+
path: '/setup2fa', // route path
437+
component: {
438+
file: '@@/pages/TwoFactorsSetup.vue',
439+
meta: {
440+
title: 'Setup 2FA', // meta title for this page
441+
customLayout: true // don't include default layout like menu/header
442+
}
443+
}
444+
}
445+
]
446+
}
447+
})
448+
```
449+
450+
This will register custom page with path `/setup2fa` and will not include it in the menu.
451+
452+
You can navigate user to this page using any router link, e.g.:
453+
454+
```html
455+
<template>
456+
<Link to="/setup2fa">Setup 2FA</Link>
457+
</template>
458+
```
459+
460+
```ts
461+
import { Link } from '@/afcl';
462+
```
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# OAuth Authentication
2+
3+
The OAuth plugin enables OAuth2-based authentication in AdminForth, allowing users to sign in using their Google, GitHub, or other OAuth2 provider accounts.
4+
5+
## Installation
6+
7+
To install the plugin:
8+
9+
```bash
10+
npm install @adminforth/oauth --save
11+
npm install @adminforth/google-oauth-adapter --save # for Google OAuth
12+
```
13+
14+
## Configuration
15+
16+
### 1. OAuth Provider Setup
17+
18+
You need to get the client ID and client secret from your OAuth2 provider.
19+
20+
For Google:
21+
1. Go to the [Google Cloud Console](https://console.cloud.google.com)
22+
2. Create a new project or select an existing one
23+
3. Go to "APIs & Services" → "Credentials"
24+
4. Create credentials for OAuth 2.0 client IDs
25+
5. Select application type: "Web application"
26+
6. Add your application's name and redirect URI
27+
7. Set the redirect URI to `http://your-domain/oauth/callback`
28+
8. Add the credentials to your `.env` file:
29+
30+
```bash
31+
GOOGLE_CLIENT_ID=your_google_client_id
32+
GOOGLE_CLIENT_SECRET=your_google_client_secret
33+
```
34+
35+
### 2. Plugin Configuration
36+
37+
Configure the plugin in your user resource file:
38+
39+
```typescript title="./resources/adminuser.ts"
40+
import OAuth2Plugin from '@adminforth/oauth';
41+
import AdminForthAdapterGoogleOauth2 from '@adminforth/google-oauth-adapter';
42+
43+
// ... existing resource configuration ...
44+
45+
plugins: [
46+
new OAuth2Plugin({
47+
adapters: [
48+
new AdminForthAdapterGoogleOauth2({
49+
clientID: process.env.GOOGLE_CLIENT_ID,
50+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
51+
redirectUri: 'http://localhost:3000/oauth/callback',
52+
}),
53+
],
54+
emailField: 'email', // Required: field that stores the user's email
55+
emailConfirmedField: 'email_confirmed' // Optional: field to track email verification
56+
}),
57+
]
58+
```
59+
60+
### 3. Email Confirmation
61+
62+
The plugin supports automatic email confirmation for OAuth users. To enable this:
63+
64+
1. Add the `email_confirmed` field to your database schema:
65+
66+
```prisma title='./schema.prisma'
67+
model adminuser {
68+
// ... existing fields ...
69+
email_confirmed Boolean @default(false)
70+
}
71+
```
72+
73+
2. Run the migration:
74+
75+
```bash
76+
npx prisma migrate dev --name add-email-confirmed-to-adminuser
77+
```
78+
79+
3. Configure the plugin with `emailConfirmedField`:
80+
81+
```typescript title="./resources/adminuser.ts"
82+
new OAuth2Plugin({
83+
// ... adapters configuration ...
84+
emailField: 'email',
85+
emailConfirmedField: 'email_confirmed' // Enable email confirmation tracking
86+
}),
87+
```
88+
89+
When using OAuth:
90+
- New users will have their email automatically confirmed (`email_confirmed = true`)
91+
- Existing users will have their email marked as confirmed upon successful OAuth login
92+
- The `email_confirmed` field must be a boolean type

adminforth/index.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import AdminForthAuth from './auth.js';
32
import MongoConnector from './dataConnectors/mongo.js';
43
import PostgresConnector from './dataConnectors/postgres.js';
@@ -414,6 +413,15 @@ class AdminForth implements IAdminForth {
414413
return { error: err };
415414
}
416415

416+
for (const column of resource.columns) {
417+
const fieldName = column.name;
418+
if (fieldName in record) {
419+
if (!column.showIn?.create || column.backendOnly) {
420+
return { error: `Field "${fieldName}" cannot be modified as it is restricted from creation` };
421+
}
422+
}
423+
}
424+
417425
// execute hook if needed
418426
for (const hook of listify(resource.hooks?.create?.beforeSave)) {
419427
console.log('🪲 Hook beforeSave', hook);
@@ -490,6 +498,15 @@ class AdminForth implements IAdminForth {
490498
delete record[column.name];
491499
}
492500

501+
for (const column of resource.columns) {
502+
const fieldName = column.name;
503+
if (fieldName in record) {
504+
if (!column.showIn?.edit || column.editReadonly || column.backendOnly) {
505+
return { error: `Field "${fieldName}" cannot be modified as it is restricted from editing` };
506+
}
507+
}
508+
}
509+
493510
// execute hook if needed
494511
for (const hook of listify(resource.hooks?.edit?.beforeSave)) {
495512
const resp = await hook({

adminforth/modules/restApi.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,14 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
458458
}
459459
});
460460

461+
if (resource.options.fieldGroups) {
462+
resource.options.fieldGroups.forEach((group, i) => {
463+
if (group.groupName) {
464+
translateRoutines[`fieldGroup${i}`] = tr(group.groupName, `resource.${resource.resourceId}.fieldGroup`);
465+
}
466+
});
467+
}
468+
461469
const translated: Record<string, string> = {};
462470
await Promise.all(
463471
Object.entries(translateRoutines).map(async ([key, value]) => {
@@ -531,6 +539,10 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
531539
),
532540
options: {
533541
...resource.options,
542+
fieldGroups: resource.options.fieldGroups?.map((group, i) => ({
543+
...group,
544+
groupName: translated[`fieldGroup${i}`] || group.groupName,
545+
})),
534546
bulkActions: allowedBulkActions.map(
535547
(action, i) => ({
536548
...action,

adminforth/spa/src/afcl/Select.vue

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,32 @@
3232
/>
3333
</div>
3434
</div>
35-
<div v-if="showDropdown" ref="dropdownEl" :style="dropdownStyle" :class="{'shadow-none': isTop}"
36-
class="absolute z-10 mt-1 w-full bg-white shadow-lg dark:shadow-black dark:bg-gray-700
35+
<teleport to="body" v-if="teleportToBody && showDropdown">
36+
<div ref="dropdownEl" :style="getDropdownPosition" :class="{'shadow-none': isTop}"
37+
class="fixed z-50 w-full bg-white shadow-lg dark:shadow-black dark:bg-gray-700
38+
dark:border-gray-600 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm max-h-48">
39+
<div
40+
v-for="item in filteredItems"
41+
:key="item.value"
42+
class="px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400"
43+
:class="{ 'bg-lightPrimaryOpacity dark:bg-darkPrimaryOpacity': selectedItems.includes(item) }"
44+
@click="toogleItem(item)"
45+
>
46+
<slot name="item" :option="item"></slot>
47+
<label v-if="!$slots.item" :for="item.value">{{ item.label }}</label>
48+
</div>
49+
<div v-if="!filteredItems.length" class="px-4 py-2 cursor-pointer text-gray-400 dark:text-gray-300">
50+
{{ options.length ? $t('No results found') : $t('No items here') }}
51+
</div>
52+
53+
<div v-if="$slots['extra-item']" class="px-4 py-2 dark:text-gray-400">
54+
<slot name="extra-item"></slot>
55+
</div>
56+
</div>
57+
</teleport>
58+
59+
<div v-if="!teleportToBody && showDropdown" ref="dropdownEl" :style="dropdownStyle" :class="{'shadow-none': isTop}"
60+
class="absolute z-10 mt-1 w-full bg-white shadow-lg dark:shadow-black dark:bg-gray-700
3761
dark:border-gray-600 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm max-h-48">
3862
<div
3963
v-for="item in filteredItems"
@@ -106,6 +130,10 @@ const props = defineProps({
106130
type: Boolean,
107131
default: false,
108132
},
133+
teleportToBody: {
134+
type: Boolean,
135+
default: false,
136+
},
109137
});
110138
111139
const emit = defineEmits(['update:modelValue']);
@@ -242,4 +270,20 @@ onUnmounted(() => {
242270
removeClickListener();
243271
});
244272
273+
const getDropdownPosition = computed(() => {
274+
if (!inputEl.value) return {};
275+
const rect = inputEl.value.getBoundingClientRect();
276+
const style: { left: string; top: string; width: string } = {
277+
left: `${rect.left}px`,
278+
top: `${rect.bottom + 8}px`,
279+
width: `${rect.width}px`
280+
};
281+
282+
if (isTop.value && dropdownHeight.value) {
283+
style.top = `${rect.top - dropdownHeight.value - 8}px`;
284+
}
285+
286+
return style;
287+
});
288+
245289
</script>

adminforth/spa/src/components/ResourceListTable.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@
271271
272272
import { computed, onMounted, ref, watch, type Ref } from 'vue';
273273
import { callAdminForthApi } from '@/utils';
274-
274+
import { useI18n } from 'vue-i18n';
275275
import ValueRenderer from '@/components/ValueRenderer.vue';
276276
import { getCustomComponent } from '@/utils';
277277
import { useCoreStore } from '@/stores/core';
@@ -293,7 +293,7 @@ import type { AdminForthResourceCommon } from '@/types/Common';
293293
import adminforth from '@/adminforth';
294294
295295
const coreStore = useCoreStore();
296-
296+
const { t } = useI18n();
297297
const props = defineProps<{
298298
page: number,
299299
resource: AdminForthResourceCommon,
@@ -456,9 +456,9 @@ async function onClick(e,row) {
456456
457457
async function deleteRecord(row) {
458458
const data = await adminforth.confirm({
459-
message: 'Are you sure you want to delete this item?',
460-
yes: 'Delete',
461-
no: 'Cancel',
459+
message: t('Are you sure you want to delete this item?'),
460+
yes: t('Delete'),
461+
no: t('Cancel'),
462462
});
463463
if (data) {
464464
try {
@@ -472,13 +472,13 @@ async function deleteRecord(row) {
472472
});
473473
if (!res.error){
474474
emits('update:records', true)
475-
showSuccesTost('Record deleted successfully')
475+
showSuccesTost(t('Record deleted successfully'))
476476
} else {
477477
showErrorTost(res.error)
478478
}
479479
480480
} catch (e) {
481-
showErrorTost(`Something went wrong, please try again later`);
481+
showErrorTost(t('Something went wrong, please try again later'));
482482
console.error(e);
483483
};
484484
}

0 commit comments

Comments
 (0)