mirror of
https://github.com/pockethost/pockethost.git
synced 2025-06-02 04:06:40 +00:00
feat(local-auth): Initial commit
This commit is contained in:
parent
55b43a1573
commit
5be2c9ef0f
6
.changeset/1719706755991.md
Normal file
6
.changeset/1719706755991.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
'@pockethost/plugin-local-auth': minor
|
||||
'pockethost': minor
|
||||
---
|
||||
|
||||
Initial commit
|
16
packages/plugin-local-auth/LICENSE.md
Normal file
16
packages/plugin-local-auth/LICENSE.md
Normal file
@ -0,0 +1,16 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
43
packages/plugin-local-auth/package.json
Normal file
43
packages/plugin-local-auth/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@pockethost/plugin-local-auth",
|
||||
"version": "0.0.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/pockethost/pockethost/packages/plugin-local-auth"
|
||||
},
|
||||
"description": "A pockethost plugin.",
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check-types": "tsc --noEmit "
|
||||
},
|
||||
"keywords": [
|
||||
"pockethost"
|
||||
],
|
||||
"author": {
|
||||
"name": "Ben Allfree",
|
||||
"url": "https://github.com/benallfree"
|
||||
},
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pockethost": "workspace:^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/inquirer": "^9.0.7",
|
||||
"@types/lowdb": "^1.0.15",
|
||||
"@types/node": "^20.8.10",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@s-libs/micro-dash": "^16.1.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"commander": "^11.1.0",
|
||||
"immer": "^10.1.1",
|
||||
"inflection": "^3.0.0",
|
||||
"inquirer": "^9.2.15",
|
||||
"lowdb": "^7.0.1"
|
||||
}
|
||||
}
|
67
packages/plugin-local-auth/readme.md
Normal file
67
packages/plugin-local-auth/readme.md
Normal file
@ -0,0 +1,67 @@
|
||||
# plugin-local-auth
|
||||
|
||||
A plugin for [pockethost](https://www.npmjs.com/package/pockethost).
|
||||
|
||||
This plugin implements a local user auth database.
|
||||
|
||||
## Quickstart
|
||||
|
||||
**Install**
|
||||
|
||||
```bash
|
||||
npx pockethost plugin install @pockethost/plugin-local-auth
|
||||
```
|
||||
|
||||
**Get help**
|
||||
|
||||
```bash
|
||||
npx pockethost local-auth --help
|
||||
```
|
||||
|
||||
**Create user/admin**
|
||||
|
||||
```bash
|
||||
npx pockethost local-auth create foo@bar.com myPassword
|
||||
npx pockethost local-auth create admin@bar.com myPassword --admin
|
||||
```
|
||||
|
||||
**Delete user**
|
||||
|
||||
```bash
|
||||
npx pockethost local-auth delete foo@bar.com
|
||||
```
|
||||
|
||||
**List users**
|
||||
|
||||
```bash
|
||||
npx pockethost local-auth ls
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
The following variables will be used if they are found in the shell environment. PocketHost will also load them from an `.env` file if found at load time.
|
||||
|
||||
| Name | Default | Discussion |
|
||||
| ------------------ | ------------------------------- | ------------------------------------------------------------- |
|
||||
| PH_LOCAL_AUTH_HOME | `.pockethost/plugin-local-auth` | The home directory for any data storage needs of this plugin. |
|
||||
|
||||
## Actions
|
||||
|
||||
## Filters
|
||||
|
||||
## Related Plugins
|
||||
|
||||
`local-auth` pairs well with:
|
||||
|
||||
- `@pockethost/plugin-ftp-server`
|
||||
- `@pockethost/plugin-auto-admin`
|
||||
|
||||
## Support
|
||||
|
||||
PocketHost has a thriving [Discord community](https://discord.gg/nVTxCMEcGT).
|
||||
|
||||
---
|
||||
|
||||
### Sponsored by https://pockethost.io. Instantly host your PocketBase projects.
|
||||
|
||||
---
|
85
packages/plugin-local-auth/src/AuthCommand.ts
Normal file
85
packages/plugin-local-auth/src/AuthCommand.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { forEach } from '@s-libs/micro-dash'
|
||||
import { Command } from 'commander'
|
||||
import { gracefulExit } from 'pockethost/core'
|
||||
import { DbService } from './db'
|
||||
import { error, info } from './log'
|
||||
|
||||
export const AuthCommand = async () => {
|
||||
const {
|
||||
addUser,
|
||||
getFirstUserByExactCriteria,
|
||||
getAllUsersByExactCriteria,
|
||||
authenticate,
|
||||
deleteUserByExactCriteria,
|
||||
} = await DbService()
|
||||
|
||||
const cmd = new Command(`auth`)
|
||||
.description(`Command root`)
|
||||
.addCommand(
|
||||
new Command(`add`)
|
||||
.argument(`<username>`, `User name`)
|
||||
.argument(`<password>`, `User password`)
|
||||
.alias(`a`)
|
||||
.option(`-a, --admin`, `Make the user an admin`)
|
||||
.description(`Add a user`)
|
||||
.action(async (username, password, options) => {
|
||||
if (getFirstUserByExactCriteria({ username })) {
|
||||
error(`User ${username} already exists`)
|
||||
await gracefulExit()
|
||||
}
|
||||
await addUser(username, password, options.admin)
|
||||
}),
|
||||
)
|
||||
.addCommand(
|
||||
new Command(`update`)
|
||||
.argument(`<username>`, `User name`)
|
||||
.argument(`<password>`, `User password`)
|
||||
.option(`-a, --admin`, `Make the user an admin`)
|
||||
.alias(`u`)
|
||||
.alias(`up`)
|
||||
.description(`Update a user`)
|
||||
.action(async (username: string, password: string, options) => {
|
||||
await deleteUserByExactCriteria({ username })
|
||||
await addUser(username, password, options.admin)
|
||||
}),
|
||||
)
|
||||
.addCommand(
|
||||
new Command(`delete`)
|
||||
.argument(`<username>`, `User ID`)
|
||||
.alias(`d`)
|
||||
.alias(`del`)
|
||||
.alias(`rm`)
|
||||
.description(`Delete a user`)
|
||||
.action(async (username: string, options) => {
|
||||
await deleteUserByExactCriteria({ username })
|
||||
}),
|
||||
)
|
||||
.addCommand(
|
||||
new Command(`list`)
|
||||
.alias(`ls`)
|
||||
.description(`List users`)
|
||||
.action(async (options) => {
|
||||
info(`Listing users`)
|
||||
const users = getAllUsersByExactCriteria({})
|
||||
forEach(users, (user) => {
|
||||
info(`${user.username}`, user.isAdmin ? `(admin)` : ``)
|
||||
})
|
||||
}),
|
||||
)
|
||||
.addCommand(
|
||||
new Command(`login`)
|
||||
.argument(`<username>`, `User name`)
|
||||
.argument(`<password>`, `User password`)
|
||||
.alias(`a`)
|
||||
.description(`Test the user login`)
|
||||
.action(async (username, password, options) => {
|
||||
const isAuthenticated = authenticate(username, password)
|
||||
if (isAuthenticated) {
|
||||
info(`User ${username} authenticated`)
|
||||
} else {
|
||||
error(`User ${username} not authenticated`)
|
||||
}
|
||||
}),
|
||||
)
|
||||
return cmd
|
||||
}
|
21
packages/plugin-local-auth/src/constants.ts
Normal file
21
packages/plugin-local-auth/src/constants.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { underscore } from 'inflection'
|
||||
import { dirname, join } from 'path'
|
||||
import { PH_HOME, Settings, mkPath } from 'pockethost/core'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
export const PLUGIN_NAME = `plugin-local-auth`
|
||||
|
||||
export const HOME_DIR =
|
||||
process.env.PH_LOCAL_AUTH_HOME || join(PH_HOME(), PLUGIN_NAME)
|
||||
export const PLUGIN_NAME_CONSTANT_CASE = underscore(PLUGIN_NAME, true)
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export const PLUGIN_DATA = (...paths: string[]) => join(HOME_DIR, ...paths)
|
||||
|
||||
export const PROJECT_DIR = (...paths: string[]) =>
|
||||
join(__dirname, '..', ...paths)
|
||||
|
||||
export const settings = Settings({
|
||||
PH_LOCAL_AUTH_HOME: mkPath(HOME_DIR, { create: true }),
|
||||
})
|
98
packages/plugin-local-auth/src/db.ts
Normal file
98
packages/plugin-local-auth/src/db.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { JSONFileSyncPreset } from 'lowdb/node'
|
||||
import { mkSingleton } from 'pockethost'
|
||||
import { UserId } from 'pockethost/src/common/schema/BaseFields'
|
||||
import { PLUGIN_DATA } from './constants'
|
||||
import { info } from './log'
|
||||
|
||||
export type UserFields = {
|
||||
id: UserId
|
||||
username: string
|
||||
passwordHash: string
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
export const DbService = mkSingleton(async () => {
|
||||
const db = JSONFileSyncPreset<{
|
||||
defaultUid: UserId
|
||||
users: { [_: UserId]: UserFields }
|
||||
}>(PLUGIN_DATA('users.json'), { users: {}, defaultUid: '' })
|
||||
|
||||
const getUserByExactCriteria = (criteria: Partial<UserFields>) => {
|
||||
return Object.values(db.data.users).find((user) =>
|
||||
Object.entries(criteria).every(
|
||||
([k, v]) => user[k as keyof UserFields] === v,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const deleteUserByExactCriteria = (criteria: Partial<UserFields>) => {
|
||||
const user = getUserByExactCriteria(criteria)
|
||||
if (user) {
|
||||
db.update((data) => {
|
||||
delete data.users[user.id]
|
||||
})
|
||||
info(`Deleted user ${user.username}`)
|
||||
}
|
||||
}
|
||||
|
||||
const addUser = (username: string, password: string, isAdmin = false) => {
|
||||
if (getUserByExactCriteria({ username })) {
|
||||
throw new Error(`User ${username} already exists`)
|
||||
}
|
||||
const id = username
|
||||
const passwordHash = bcrypt.hashSync(password, 10)
|
||||
db.update((data) => {
|
||||
data.users = {
|
||||
...data.users,
|
||||
[id]: { id, username, passwordHash, isAdmin },
|
||||
}
|
||||
})
|
||||
info(`Added user ${username}/${password}`)
|
||||
return getFirstUserByExactCriteria({ id })!
|
||||
}
|
||||
|
||||
const getAllUsersByExactCriteria = (criteria: Partial<UserFields>) => {
|
||||
return Object.values(db.data.users).filter((user) =>
|
||||
Object.entries(criteria).every(
|
||||
([k, v]) => user[k as keyof UserFields] === v,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const getFirstUserByExactCriteria = (criteria: Partial<UserFields>) => {
|
||||
return getAllUsersByExactCriteria(criteria)[0]
|
||||
}
|
||||
|
||||
const authenticate = (
|
||||
username: string,
|
||||
password: string,
|
||||
): UserFields | undefined => {
|
||||
const user = getFirstUserByExactCriteria({ username })
|
||||
if (user && bcrypt.compareSync(password, user.passwordHash)) {
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
const getDefaultUser = (): UserFields | undefined => {
|
||||
return getFirstUserByExactCriteria({ id: db.data.defaultUid })
|
||||
}
|
||||
|
||||
const setDefaultUser = (user: UserFields) => {
|
||||
db.update((data) => {
|
||||
data.defaultUid = user.id
|
||||
})
|
||||
info(`Set default user to ${user.username}`)
|
||||
}
|
||||
|
||||
return {
|
||||
addUser,
|
||||
getUserByExactCriteria,
|
||||
deleteUserByExactCriteria,
|
||||
getAllUsersByExactCriteria,
|
||||
getFirstUserByExactCriteria,
|
||||
authenticate,
|
||||
getDefaultUser,
|
||||
setDefaultUser,
|
||||
}
|
||||
})
|
3
packages/plugin-local-auth/src/index.ts
Normal file
3
packages/plugin-local-auth/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { plugin } from './plugin'
|
||||
|
||||
export default plugin
|
@ -0,0 +1,15 @@
|
||||
/// <reference path="../../../../pockethost/src/instance-app/types/all.d.ts" />
|
||||
|
||||
$app.onBeforeServe().add((e) => {
|
||||
const dao = $app.dao()
|
||||
const { mkLog } = /** @type {Lib} */ (require(`${__hooks}/_ph_lib.js`))
|
||||
|
||||
const log = mkLog(`plugin-local-auth`)
|
||||
|
||||
if (!$os.getenv(`PH_PLUGIN_LOCAL_AUTH_ENABLED`)) {
|
||||
log(`Starting `)
|
||||
} else {
|
||||
log(`Not enabled. Skipping...`)
|
||||
return
|
||||
}
|
||||
})
|
5
packages/plugin-local-auth/src/log.ts
Normal file
5
packages/plugin-local-auth/src/log.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { LoggerService } from 'pockethost'
|
||||
import { PLUGIN_NAME } from './constants'
|
||||
|
||||
const logger = LoggerService().create(PLUGIN_NAME)
|
||||
export const { dbg, info, error } = logger
|
89
packages/plugin-local-auth/src/plugin.ts
Normal file
89
packages/plugin-local-auth/src/plugin.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { produce } from 'immer'
|
||||
import inquirer from 'inquirer'
|
||||
import {
|
||||
PocketHostPlugin,
|
||||
onAfterPluginsLoadedAction,
|
||||
onAuthenticateUserFilter,
|
||||
onCliCommandsFilter,
|
||||
onInstanceConfigFilter,
|
||||
onNewInstanceRecordFilter,
|
||||
onSettingsFilter,
|
||||
} from 'pockethost'
|
||||
import { UserId } from 'pockethost/src/common/schema/BaseFields'
|
||||
import { AuthCommand } from './AuthCommand'
|
||||
import {
|
||||
PLUGIN_NAME,
|
||||
PLUGIN_NAME_CONSTANT_CASE,
|
||||
PROJECT_DIR,
|
||||
settings,
|
||||
} from './constants'
|
||||
import { DbService } from './db'
|
||||
import { dbg, info } from './log'
|
||||
|
||||
export type WithUid = { uid: UserId }
|
||||
|
||||
export const plugin: PocketHostPlugin = async ({}) => {
|
||||
dbg(`initializing ${PLUGIN_NAME}`)
|
||||
|
||||
const { getDefaultUser, addUser, setDefaultUser, authenticate } =
|
||||
await DbService({})
|
||||
|
||||
onInstanceConfigFilter(async (config) => {
|
||||
return produce(config, (draft) => {
|
||||
draft.env[`PH_${PLUGIN_NAME_CONSTANT_CASE}_ENABLED`] = true.toString()
|
||||
draft.binds.hooks.push({
|
||||
src: PROJECT_DIR(`src/instance-app/hooks/**/*`),
|
||||
base: PROJECT_DIR(`src/instance-app/hooks`),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
onCliCommandsFilter(async (commands) => {
|
||||
return [...commands, await AuthCommand()]
|
||||
})
|
||||
|
||||
onSettingsFilter(async (allSettings) => ({ ...allSettings, ...settings }))
|
||||
|
||||
onNewInstanceRecordFilter<Partial<WithUid>>(async (instance) => {
|
||||
const uid = instance.uid || getDefaultUser()?.id
|
||||
|
||||
return { ...instance, uid }
|
||||
})
|
||||
|
||||
onAuthenticateUserFilter(async (userId, context) => {
|
||||
const { username, password } = context
|
||||
return userId || authenticate(username, password)?.id
|
||||
})
|
||||
|
||||
onAfterPluginsLoadedAction(async () => {
|
||||
if (getDefaultUser()) return
|
||||
info(`Local auth needs a default admin user.`)
|
||||
const answers = await inquirer.prompt<{
|
||||
username: string
|
||||
password: string
|
||||
}>([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
validate: (input) => {
|
||||
if (!input) return 'Username cannot be empty'
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
validate: (input) => {
|
||||
if (!input) return 'Password cannot be empty'
|
||||
return true
|
||||
},
|
||||
},
|
||||
])
|
||||
const { username, password } = answers
|
||||
const user = addUser(username, password, true)
|
||||
setDefaultUser(user)
|
||||
info(`User ${user.username} created as admin.`)
|
||||
})
|
||||
}
|
20
packages/plugin-local-auth/tsconfig.json
Normal file
20
packages/plugin-local-auth/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2021.String"],
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"strictNullChecks": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
@ -8,6 +8,7 @@ export enum CoreFilters {
|
||||
CliCommands = 'core_cli_commands',
|
||||
ServeSlugs = 'core_serve_slugs',
|
||||
SpawnConfig = 'core_spawn_config',
|
||||
AuthenticateUser = 'core_authenticate_user',
|
||||
GetInstanceByRequestInfo = 'core_get_instance_by_request_info',
|
||||
GetInstanceById = 'core_get_instance_by_id',
|
||||
InstanceConfig = 'core_instance_config',
|
||||
|
Loading…
x
Reference in New Issue
Block a user