lowered the default mfa duration and reorganized internal record pre/post handling

This commit is contained in:
Gani Georgiev committed 2026-04-26 16:46:16 +03:00
1 parent 37b258810a
commit 555a4f1a1e
10 files changed
+107 -69

No files matched your search

+16 -7
View File
@@ -406,10 +406,10 @@ func TestRecordAuthWithOTP(t *testing.T) {
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
// OTP delete + 2 ExternalAuth delete
"OnModelDelete": 3,
"OnModelDeleteExecute": 3,
"OnModelAfterDeleteSuccess": 3,
// 2 record OTPs + 2 ExternalAuths delete
"OnModelDelete": 4,
"OnModelDeleteExecute": 4,
"OnModelAfterDeleteSuccess": 4,
// user verified update
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
@@ -419,9 +419,9 @@ func TestRecordAuthWithOTP(t *testing.T) {
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnRecordDelete": 3,
"OnRecordDeleteExecute": 3,
"OnRecordAfterDeleteSuccess": 3,
"OnRecordDelete": 4,
"OnRecordDeleteExecute": 4,
"OnRecordAfterDeleteSuccess": 4,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
@@ -436,6 +436,15 @@ func TestRecordAuthWithOTP(t *testing.T) {
t.Fatal("Expected the user to be marked as verified")
}
// ensure that all pre-existing OTPs are cleared
otps, err := app.FindAllOTPsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(otps) > 0 {
t.Fatalf("Expected all OTPs to be cleared, found %d", len(otps))
}
// ensure that all pre-existing OAuth2 links are cleared
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil {
+1 -1
View File
@@ -60,7 +60,7 @@ func (m *Collection) setDefaultAuthOptions() {
},
MFA: MFAConfig{
Enabled: false,
Duration: 1800, // 30min
Duration: 600, // 10min
},
OTP: OTPConfig{
Enabled: false,
+38
View File
@@ -137,4 +137,42 @@ func (app *BaseApp) registerExternalAuthHooks() {
},
Priority: 99,
})
// delete all pre-existing external auths on verified upgrade
app.OnRecordUpdateExecute().Bind(&hook.Handler[*RecordEvent]{
Func: func(e *RecordEvent) error {
if !e.Record.Collection().IsAuth() {
return e.Next()
}
hasUpgradedVerified := !e.Record.Original().IsNew() && !e.Record.Original().Verified() && e.Record.Verified()
if !hasUpgradedVerified {
return e.Next()
}
originalApp := e.App
return e.App.RunInTransaction(func(txApp App) error {
e.App = txApp
defer func() { e.App = originalApp }()
externalAuths, err := txApp.FindAllExternalAuthsByRecord(e.Record)
if err != nil {
return err
}
if len(externalAuths) > 0 {
// delete all pre-existing external auths
if err := txApp.DeleteAllExternalAuthsByRecord(e.Record); err != nil {
return err
}
// force refresh tokens reset (if not already)
e.Record.RefreshTokenKey()
}
return e.Next()
})
},
Priority: 99,
})
}
+6 -5
View File
@@ -3,6 +3,7 @@ package core
import (
"context"
"errors"
"fmt"
"time"
"github.com/pocketbase/pocketbase/tools/hook"
@@ -141,11 +142,11 @@ func (app *BaseApp) registerMFAHooks() {
if old != new {
err = e.App.DeleteAllMFAsByRecord(e.Record)
if err != nil {
e.App.Logger().Warn(
"Failed to delete all previous mfas",
"error", err,
"recordId", e.Record.Id,
"collectionId", e.Record.Collection().Id,
return fmt.Errorf(
"[%s] failed to delete all previos MFAs for record %q: %w",
e.Record.Collection().Name,
e.Record.Id,
err,
)
}
}
+27
View File
@@ -3,8 +3,10 @@ package core
import (
"context"
"errors"
"fmt"
"time"
"github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/types"
)
@@ -124,4 +126,29 @@ func (app *BaseApp) registerOTPHooks() {
app.Logger().Warn("Failed to delete expired OTP sessions", "error", err)
}
})
// delete all record OTPs on tokenKey change to minimize the risk of hijacking attacks
app.OnRecordUpdateExecute().Bind(&hook.Handler[*RecordEvent]{
Func: func(e *RecordEvent) error {
err := e.Next()
if err != nil || !e.Record.Collection().IsAuth() {
return err
}
if e.Record.Original().TokenKey() != e.Record.TokenKey() {
err := e.App.DeleteAllOTPsByRecord(e.Record)
if err != nil {
return fmt.Errorf(
"[%s] failed to delete all previos OTPs for record %q: %w",
e.Record.Collection().Name,
e.Record.Id,
err,
)
}
}
return nil
},
Priority: 99,
})
}
+10 -47
View File
@@ -1427,10 +1427,7 @@ func onRecordValidate(e *RecordEvent) error {
}
func onRecordSaveExecute(e *RecordEvent) error {
var needToDeleteExternalAuths bool
if e.Record.Collection().IsAuth() {
// auth resets to prevent (pre)hijacking vulnerabilities
if !e.Record.IsNew() {
lastSavedRecord, err := e.App.FindRecordById(e.Record.Collection(), e.Record.Id)
if err != nil {
@@ -1443,15 +1440,10 @@ func onRecordSaveExecute(e *RecordEvent) error {
lastSavedRecord.Email() != e.Record.Email()) {
e.Record.RefreshTokenKey()
}
// in case upgrading from "unverified" -> "verified" mark all pre-existing OAuth2 links
// for deletion since there is no reliable way to verify that they weren't created by an attacker
if !lastSavedRecord.Verified() && e.Record.Verified() {
needToDeleteExternalAuths = true
}
}
// cross-check that the auth record id is unique across all auth collections
// loosely cross-check that the auth record id is unique across all auth collections
// to minimize impact of mistakes in API rules when multiple auth collections are used
authCollections, err := e.App.FindAllCollections(CollectionTypeAuth)
if err != nil {
return fmt.Errorf("unable to fetch the auth collections for cross-id unique check: %w", err)
@@ -1469,45 +1461,16 @@ func onRecordSaveExecute(e *RecordEvent) error {
}
}
finalizer := func() error {
err := e.Next()
if err == nil {
return nil
}
return validators.NormalizeUniqueIndexError(
err,
e.Record.Collection().Name,
e.Record.Collection().Fields.FieldNames(),
)
err := e.Next()
if err == nil {
return nil
}
if needToDeleteExternalAuths {
originalApp := e.App
return e.App.RunInTransaction(func(txApp App) error {
e.App = txApp
defer func() { e.App = originalApp }()
externalAuths, err := txApp.FindAllExternalAuthsByRecord(e.Record)
if err != nil {
return err
}
if len(externalAuths) > 0 {
// delete all pre-existing external auths
if err := txApp.DeleteAllExternalAuthsByRecord(e.Record); err != nil {
return err
}
// force refresh tokens reset (if not already)
e.Record.RefreshTokenKey()
}
return finalizer()
})
}
return finalizer()
return validators.NormalizeUniqueIndexError(
err,
e.Record.Collection().Name,
e.Record.Collection().Fields.FieldNames(),
)
}
func onRecordDeleteExecute(e *RecordEvent) error {
@@ -54,7 +54,7 @@ This field is disabled if "Except domains" is set.`)})),t.input({type:`text`,id:
`,`
`).split(`
`);e.field.values=r,app.utils.deleteByPath(app.store.errors,`fields.${e.fieldIndex}.values`)},onchange:n=>{let r=new Set,i=n.target.value.split(`
`);for(let e of i)e!=``&&r.add(e);e.field.values=Array.from(r)},onblur:e=>{(!e.relatedTarget||!i.contains(e.relatedTarget))&&i.hidePopover()}}))),a=[watch(()=>{e.field.values?.length&&e.field.maxSelect>e.field.values.length&&(e.field.maxSelect=e.field.values.length)})];return app.components.fieldSettings(e,{header:[t.div({className:`field header-select field-select-choices-input`,onunmount:()=>{a.forEach(e=>e?.unwatch())}},t.input({type:`text`,placeholder:`Add choices*`,className:`txt-left inline-error`,value:()=>e.field.values?.join(` • `)||``,name:()=>`fields.${e.fieldIndex}.values`,onfocus:e=>(i?.showPopover({source:e.target}),i.querySelector(`textarea`)?.focus(),!1)}),i),t.div({className:`field header-select single-multiple-select`},app.components.select({required:!0,options:r,value:()=>e.field.maxSelect>1,onchange:n=>{n?.[0]?.value?e.field.maxSelect=e.field.values.length||2:e.field.maxSelect=1}}))],content:()=>t.div({className:`grid sm`},()=>{if(e.field.maxSelect>1)return t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSelect`},`Max select`),t.input({type:`number`,id:n+`.maxSelect`,placeholder:`Default to single`,step:1,min:2,max:()=>e.field.values?.length||2,name:()=>`fields.${e.fieldIndex}.maxSelect`,value:()=>e.field.maxSelect||``,onchange:n=>{let r=parseInt(n.target.value,10);r>1?e.field.maxSelect=r:e.field.maxSelect=1}})))},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`,textContent:()=>e.field.maxSelect>1?`(!=[])`:`(!='')`}),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(()=>`Requires the field value to be nonempty ${e.field.maxSelect>1?`array`:`string`}.`)})))]})}function An(e){return t.div({className:`record-field-view field-type-select`},t.div({className:`inline-flex gap-5`},()=>{let n=app.utils.toArray(e.record[e.field.name],!1);return n.length?n.map(e=>t.span({className:`label`,title:e,textContent:app.utils.truncate(e,100)})):t.span({className:`missing-value`})}))}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.select={icon:`ri-list-check`,label:`Select`,settings:kn,input:On,view:An,filterModifiers:e=>e.maxSelect>1?[`each`,`length`]:[],dummyData:(e,n=!1)=>e.maxSelect>1?e.values?.slice(0,2)||[]:e.values?.[0]||``};function jn(e){let n=`json_`+app.utils.randomString(),r=store({value:``}),i=[watch(()=>e.record[e.field.name],(n,i)=>{if(!(n!==``&&n===r.value)){if(typeof n==`string`&&!n.startsWith(`"`)&&!n.endsWith(`"`)){r.value=JSON.stringify(n===void 0?null:n),e.record[e.field.name]=r.value;return}typeof n==`string`&&n.startsWith(`"`)&&n.endsWith(`"`)?r.value=n:n===null?r.value=`null`:r.value=JSON.stringify(n===void 0?null:n,null,2)}})];function a(){let n=r.value.trim();if(n===``){e.record[e.field.name]=null;return}try{let r=JSON.parse(n);typeof r==`string`?e.record[e.field.name]=JSON.stringify(r):e.record[e.field.name]=r}catch{e.record[e.field.name]=n}}return t.div({className:`record-field-input field-type-json`},t.div({className:`field`,onunmount:()=>{clearTimeout(void 0),i.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},t.i({className:app.fieldTypes.json.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name),t.span({hidden:()=>Mn(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Invalid JSON`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!Mn(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Valid JSON`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({language:`js`,id:n,name:()=>e.field.name,required:()=>e.field.required,value:()=>r.value,oninput:e=>r.value=e,onblur:()=>a()})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function Mn(e){if(e===``)return!0;try{return JSON.parse(e),!0}catch{return!1}}function Nn(e){try{let n=e.record[e.field.name];typeof n==`string`&&JSON.parse(n)}catch(r){throw new n({status:400,response:{message:`Invalid JSON data`,data:{[e.field.name]:{code:`invalid_json`,message:r.toString()}}}})}}function Pn(e){let n=`f_`+app.utils.randomString(),r=store({showInfo:!1});return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},t.span(null,`Max size `),t.small(null,`(bytes)`)),t.input({type:`number`,id:n+`.maxSize`,name:()=>`fields.${e.fieldIndex}.maxSize`,min:0,step:1 Line truncated
`);for(let e of i)e!=``&&r.add(e);e.field.values=Array.from(r)},onblur:e=>{(!e.relatedTarget||!i.contains(e.relatedTarget))&&i.hidePopover()}}))),a=[watch(()=>{e.field.values?.length&&e.field.maxSelect>e.field.values.length&&(e.field.maxSelect=e.field.values.length)})];return app.components.fieldSettings(e,{header:[t.div({className:`field header-select field-select-choices-input`,onunmount:()=>{a.forEach(e=>e?.unwatch())}},t.input({type:`text`,placeholder:`Add choices*`,className:`txt-left inline-error`,value:()=>e.field.values?.join(` • `)||``,name:()=>`fields.${e.fieldIndex}.values`,onfocus:e=>(i?.showPopover({source:e.target}),i.querySelector(`textarea`)?.focus(),!1)}),i),t.div({className:`field header-select single-multiple-select`},app.components.select({required:!0,options:r,value:()=>e.field.maxSelect>1,onchange:n=>{n?.[0]?.value?e.field.maxSelect=e.field.values.length||2:e.field.maxSelect=1}}))],content:()=>t.div({className:`grid sm`},()=>{if(e.field.maxSelect>1)return t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSelect`},`Max select`),t.input({type:`number`,id:n+`.maxSelect`,placeholder:`Default to single`,step:1,min:2,max:()=>e.field.values?.length||2,name:()=>`fields.${e.fieldIndex}.maxSelect`,value:()=>e.field.maxSelect||``,onchange:n=>{let r=parseInt(n.target.value,10);r>1?e.field.maxSelect=r:e.field.maxSelect=1}})))},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`,textContent:()=>e.field.maxSelect>1?`(!=[])`:`(!='')`}),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(()=>`Requires the field value to be nonempty ${e.field.maxSelect>1?`array`:`string`}.`)})))]})}function An(e){return t.div({className:`record-field-view field-type-select`},t.div({className:`inline-flex gap-5`},()=>{let n=app.utils.toArray(e.record[e.field.name],!1);return n.length?n.map(e=>t.span({className:`label`,title:e,textContent:app.utils.truncate(e,100)})):t.span({className:`missing-value`})}))}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.select={icon:`ri-list-check`,label:`Select`,settings:kn,input:On,view:An,filterModifiers:e=>e.maxSelect>1?[`each`,`length`]:[],dummyData:(e,n=!1)=>e.maxSelect>1?e.values?.slice(0,2)||[]:e.values?.[0]||``};function jn(e){let n=`json_`+app.utils.randomString(),r=store({value:``}),i=[watch(()=>e.record[e.field.name],(n,i)=>{if(!(n!==``&&n===r.value)){if(typeof n==`string`&&!n.startsWith(`"`)&&!n.endsWith(`"`)){r.value=JSON.stringify(n===void 0?null:n),e.record[e.field.name]=r.value;return}typeof n==`string`&&n.startsWith(`"`)&&n.endsWith(`"`)?r.value=n:n===null?r.value=`null`:r.value=JSON.stringify(n===void 0?null:n,null,2)}})];function a(){let n=r.value.trim();if(n===``){e.record[e.field.name]=null;return}try{let r=JSON.parse(n);typeof r==`string`?e.record[e.field.name]=JSON.stringify(r):e.record[e.field.name]=r}catch{e.record[e.field.name]=n}}return t.div({className:`record-field-input field-type-json`},t.div({className:`field`,onunmount:()=>{clearTimeout(void 0),i.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},t.i({className:app.fieldTypes.json.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name),t.span({hidden:()=>Mn(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Invalid JSON`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!Mn(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Valid JSON`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({language:`js`,id:n,name:()=>e.field.name,required:()=>e.field.required,value:()=>r.value,oninput:e=>r.value=e,onblur:()=>a()})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function Mn(e){if(e===``)return!0;try{return JSON.parse(e),!0}catch{return!1}}function Nn(e){try{let n=e.record[e.field.name];typeof n==`string`&&JSON.parse(n)}catch(r){throw new n({status:400,response:{message:`Invalid JSON data`,data:{[e.field.name]:{code:`invalid_json`,message:r.toString()}}}})}}function Pn(e){let n=`f_`+app.utils.randomString(),r=store({showInfo:!1});return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},t.span(null,`Max size `),t.small(null,`(bytes)`)),t.input({type:`number`,id:n+`.maxSize`,name:()=>`fields.${e.fieldIndex}.maxSize`,min:0,step:1 Line truncated
To target the newly submitted ones you can use @request.body.*.`)})],name:`updateRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.updateRule,oninput:n=>e.collection.updateRule=n})),t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Delete rule`,name:`deleteRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.deleteRule,oninput:n=>e.collection.deleteRule=n}))]}),()=>{if(e.collection.type==`auth`)return[t.hr({className:`m-t-base m-b-base`}),t.button({type:`button`,onmount:()=>{n.showAuthRules=e.collection.manageRule!==null||e.collection.authRule!==``},className:()=>`btn secondary sm ${n.showAuthRules?``:`transparent`}`,onclick:()=>{n.showAuthRules=!n.showAuthRules}},t.span({className:`txt`},`Additional auth collection rules`),t.i({ariaHidden:!0,className:()=>n.showAuthRules?`ri-arrow-drop-up-line`:`ri-arrow-drop-down-line`})),app.components.slide(()=>n.showAuthRules,t.div({className:`grid sm m-t-sm`},t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Authentication rule`,name:`authRule`,placeholder:``,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.authRule,oninput:n=>e.collection.authRule=n}),t.div({className:`field-help`},t.p(null,`This rule is executed every time `,t.strong(null,`before authentication`),` allowing you to restrict who can authenticate.`),t.p(null,`For example, to allow only verified users you can set it to `,t.code(null,`verified = true`),`.`),t.p(null,`Leave it empty to allow anyone with an account to authenticate.`),t.p(null,`To disable authentication entirely you can change it to "Set superusers only".`))),t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Manage rule`,name:`manageRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.manageRule,oninput:n=>e.collection.manageRule=n}),t.div({className:`field-help`},t.p(null,`This rule is executed in addition to the `,t.strong(null,`create`),` and `,t.strong(null,`update`),` API rules.`),t.p(null,`It enables superuser-like permissions to allow fully managing the auth record(s), eg. changing the password without requiring to enter the old one, directly updating the verified state or email, etc.`)))))]})}var Xn=`test_view_query`;function Zn(e){let n=`query_`+app.utils.randomString(),r=`SELECT.FROM.WHERE.LEFT JOIN.INNER JOIN.ON.AS.GROUP BY.HAVING.ORDER BY.ASC.DESC.LIMIT.OFFSET.WITH.NOT.IN.AND.OR.EXISTS.LIKE.CAST.REAL.DECIMAL.NUMERIC.INT.TEXT.BOOL`.split(`.`),i=store({testRecords:[],testError:``,isTesting:!1});async function a(n){if(i.isTesting=!0,i.testRecords=[],(app.store.errors?.viewQuery||app.store.errors?.fields)&&(delete app.store.errors.viewQuery,delete app.store.errors.fields),!n){i.testError=``,i.isTesting=!1;return}try{let r=await app.pb.send(`/api/collections/meta/dry-run-view`,{method:`POST`,body:{query:n},requestKey:Xn});e.collection?.id?i.testRecords=r.sample.map(n=>(n.collectionId=e.collection?.id,n.collectionName=e.collection?.name,n)):i.testRecords=r.sample,i.testError=``,i.isTesting=!1}catch(e){e.isAbort||(i.testError=e.message||`Invalid query.`,i.isTesting=!1)}}let o,s=[watch(()=>e.collection?.viewQuery,e=>{clearTimeout(o),o=setTimeout(()=>a(e),200)})];return t.div({pbEvent:`collectionViewQueryTabContent`,className:`collection-tab-content collection-view-query-tab-content`,onunmount:()=>{clearTimeout(o),app.pb.cancelRequest(Xn),s.forEach(e=>e?.unwatch())}},t.div({className:`grid`},t.div({className:`col-12`},t.div({className:`txt-right txt-sm m-b-10`},t.button({type:`button`,className:`txt-bold link-hint`,"html-popovertarget":n+`caveats_dropdown`},()=>`Query caveats`)),t.div({id:n+`caveats_dropdown`,className:`dropdown sm query-caveats-dropdown`,popover:`auto`},t.ul(null,t.li(null,`Wildcard columns (*) are not supported.`),t.li(null,`The query must have a unique `,t.code(null,`id`),` column.`,t.br(),`If your query doesn't have a suitable one, you can use the universal `,t.code(null,`(ROW_NUMBER() OVER()) as id`),`.`),t.li(null,`Expressions must be aliased with a valid formatted field name, e.g. `,t.code(null,`MAX(balance) as maxBalance`),`.`),t.li(null,`Combined/multi-spaced expressions must be wrapped in parenthesis, e.g. `,t.code(null,`(MAX(balance) + 1) as maxBalance`),`.`),t.li(null,`UNION expressions are supported but the entire query must be wrapped in parenthesis.`))),t.div({className:`field`},t.label({htmlFor:n+`.viewQuery`},t.span({className:`txt`},`Select query`),t.span({hidden:()=>!i.testError,className:`query-state`,ariaDescription:app.attrs.tooltip(`Invalid query`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!!i.testError,className:`query-state`,ariaDescription:app.attrs.tooltip(`Valid query`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({id:n+`.viewQuery`,name:`viewQuery`,language:`sql`Line truncated
`+e)})},t.div({className:`btns`},t.button({type:`button`,className:()=>`btn expanded-lg ${a.isSaving?`loading`:``}`,disabled:()=>!a.canSave,onclick:()=>s(!0)},t.span({className:`txt`},()=>a.isNew?`Create`:`Save changes`)),t.button({type:`button`,title:`Save options`,className:()=>`btn p-5`,disabled:()=>!a.canSave,"html-popovertarget":i+`save_options`},t.i({className:`ri-arrow-up-s-line`,ariaHidden:!0})),t.div({id:i+`save_options`,className:`dropdown nowrap`,popover:`auto`},t.button({type:`button`,className:`dropdown-item`,onclick:e=>{e.target.closest(`.dropdown`).hidePopover(),s(!1)}},t.span({className:`txt`},`Save and continue`),t.small({className:`txt-hint`},`(Ctrl+S)`)),t.hr(),t.button({type:`button`,className:`dropdown-item`,onclick:e=>{e.target.closest(`.dropdown`).hidePopover(),u()}},t.span({className:`txt`},`Reset form`)))))),r}function $n(e){let n=JSON.parse(JSON.stringify(app.store.collectionScaffolds[e.type])),r=JSON.parse(JSON.stringify(e.fields))||[],i=r.filter(e=>!e.system);e.fields=n.fields||[];for(let n of r){if(!n.system)continue;let r=e.fields.find(e=>e.name==n.name);r&&Object.assign(r,n)}for(let n of i)e.fields.push(n);if(e.indexes=e.indexes||[],e.indexes.length){let r=n?.indexes||[];indexesLoop:for(let n=e.indexes.length-1;n>=0;n--){let i=app.utils.parseIndex(e.indexes[n]),a=i.indexName.toLowerCase();for(let i of r)if(a==app.utils.parseIndex(i).indexName.toLowerCase()){e.indexes.splice(n,1);continue indexesLoop}for(let r of i.columns)if(!e.fields.find(e=>e.name.toLowerCase()==r.name.toLowerCase())){e.indexes.splice(n,1);continue indexesLoop}}}app.utils.mergeUnique(e.indexes,n.indexes)}function er(e,n){let r=`truncate_`+app.utils.randomString(),i=store({isSubmitting:!1,nameConfirm:``});async function a(){if(i.isSubmitting||!e.originalCollection?.name||e.originalCollection.name!=i.nameConfirm)return!1;i.isSubmitting=!0;try{return await app.pb.collections.truncate(e.originalCollection.name),n.ontruncate?.(JSON.parse(JSON.stringify(e.originalCollection))),app.toasts.success(`Successfully truncated collection "${e.originalCollection.name}".`),i.isSubmitting=!1,!0}catch(e){i.isSubmitting=!1,app.checkApiError(e)}return!1}return t.button({type:`button`,className:`dropdown-item txt-danger`,disabled:()=>i.isSubmitting,onclick:n=>{n.target.closest(`.dropdown`).hidePopover(),app.modals.confirm(t.div(null,t.h6({className:`block txt-center`},`Do you really want to delete all records of the collection?`),t.div({className:`confirm-collection-label txt-bold m-t-sm m-b-sm`},`Type the collection name `,t.div({className:`label`},()=>e.originalCollection.name,app.components.copyButton(()=>e.originalCollection?.name)),` to confirm:`),t.div({className:`field`},t.label({htmlFor:r+`.confirm_name`},`Collection name`),t.input({id:r+`.confirm_name`,type:`text`,required:!0,pattern:()=>RegExp.escape(e.originalCollection.name),value:()=>i.nameConfirm,oninput:e=>i.nameConfirm=e.target.value}))),async()=>{if(document.getElementById(r+`.confirm_name`)?.reportValidity(),!await a())return!1;app.modals.close(n.target.closest(`.modal`))},()=>{i.nameConfirm=``})}},t.i({className:`ri-eraser-line`,ariaHidden:!0}),t.span({className:`txt`},`Truncate`))}function tr(e,n){let r=`delete_`+app.utils.randomString(),i=store({isSubmitting:!1,nameConfirm:``});async function a(){if(i.isSubmitting||!e.originalCollection?.name||e.originalCollection.name!=i.nameConfirm)return!1;i.isSubmitting=!0;try{return await app.pb.collections.delete(e.originalCollection.name),n.ondelete?.(JSON.parse(JSON.stringify(e.originalCollection))),app.utils.removeByKey(app.store.collections,`id`,e.originalCollection.id),app.toasts.success(`Successfully deleted collection "${e.originalCollection.name}".`),i.isSubmitting=!1,!0}catch(e){i.isSubmitting=!1,app.checkApiError(e)}return!1}return t.button({type:`button`,className:`dropdown-item txt-danger`,disabled:()=>i.isSubmitting,onclick:n=>{n.target.closest(`.dropdown`).hidePopover();let o=n.target.closest(`.modal`);app.modals.confirm(t.div({className:`block`},t.h6({className:`block txt-center`},()=>e.originalCollection.type==`view`?`Do you really want to delete the selected collection?`:`Do you really want to delete the selected collection and all its records`),t.div({className:`confirm-collection-label txt-bold m-t-sm m-b-sm`},`Type the collection name `,t.div({className:`label`},()=>e.originalCollection.name,app.components.copyButton(()=>e.originalCollection?.name)),` to confirm:`),t.div({className:`field`},t.label({htmlFor:r+`.confirm_name`},`Collection name`),t.input({id:r+`.confirm_name`,type:`text`,required:!0,pattern:()=>RegExp.escape(e.originalCollection.name),value:()=>i.nameConfirm,oninput:e=>i.nameConfirm=e.target.value}))),async()=>{if(document.getElementById(r+`.confirm_name`)?.reportValidity(),!await a())return!1;app.modals.close(o)},()=>{i.nameConfirm=``})}},t.i({className:`ri-delete-bin-7-line`,ariaHidden:!0}),t.span({className:`txt`},`Delete`))}window.app=window.app||{},window.app.components=window.a Line truncated
...
+1 -1
View File
@@ -13,7 +13,7 @@
<!-- prism -->
<script src="./libs/prism/prism.js" data-manual></script>
<script type="module" crossorigin src="./assets/index-CP6VPsEr.js"></script>
<script type="module" crossorigin src="./assets/index-DbuYk4bQ.js"></script>
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
<link rel="stylesheet" crossorigin href="./assets/index-ouas71Vg.css">
</head>
+6 -6
View File
@@ -224,9 +224,9 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
"integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -937,9 +937,9 @@
"license": "MIT"
},
"node_modules/postcss": {
"version": "8.5.9",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"dev": true,
"funding": [
{
+1 -1
View File
@@ -6,7 +6,7 @@ export function mfaAccordion(collection) {
if (!collection.mfa) {
collection.mfa = {
enabled: false,
duration: 900,
duration: 600,
rule: "",
};
}