feat: Add API key support
This commit is contained in:
parent
5c1ed32cb6
commit
eac88b2ec3
71
README.md
71
README.md
@ -1,55 +1,36 @@
|
|||||||
# Astro Starter Kit: Basics
|
# Fi3D Slicer as a Service
|
||||||
|
|
||||||
```
|
## API key
|
||||||
npm create astro@latest -- --template basics
|
|
||||||
```
|
|
||||||
|
|
||||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
add `Authorization: Bearer {token}`
|
||||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
|
||||||
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
## API Endpoints
|
||||||
|
|
||||||

|
| endpoint | method | permission required | cookie access | api access | Description |
|
||||||
|
| :----------------------------: | :----: | :-----------------: | :-----------: | :--------: | :---------------------------------------: |
|
||||||
|
| /api/v1/users | GET | user.list | no | no | List every user accounts |
|
||||||
|
| /api/v1/users | POST | user.create | no | no | Create a new account |
|
||||||
|
| /api/v1/users/{userId} | GET | user.get | yes | yes | Get a user's informations |
|
||||||
|
| /api/v1/users/{userId} | PUT | user.set | yes | yes | Set a user's informations |
|
||||||
|
| /api/v1/users/{userId}/configs | GET | configs.get | yes | yes | get the list of the user's configurations |
|
||||||
|
| /api/v1/users/{userId}/configs | POST | configs.create | yes | yes | Add a new configuration to the user |
|
||||||
|
| /api/v1/users/{userId}/keys | GET | keys.get | yes | no | get the list of API key for the user |
|
||||||
|
| /api/v1/users/{userId}/keys | POST | keys.create | yes | no | create a new API key for the user |
|
||||||
|
| /api/v1/users/{userId}/process | POST | slicing.slice | yes | yes | run the main website action |
|
||||||
|
|
||||||
|
endpoints not available through API can still be accessed by admins with the `admin.` prefix to the permission
|
||||||
|
|
||||||
## 🚀 Project Structure
|
## API Key Permissions
|
||||||
|
|
||||||
Inside of your Astro project, you'll see the following folders and files:
|
### `slicing:*` permissions related to the slicing
|
||||||
|
|
||||||
```
|
| name | Description |
|
||||||
/
|
| :-----------: | :---------------------: |
|
||||||
├── public/
|
| slicing:slice | Slice the specified STL |
|
||||||
│ └── favicon.svg
|
|
||||||
├── src/
|
|
||||||
│ ├── components/
|
|
||||||
│ │ └── Card.astro
|
|
||||||
│ ├── layouts/
|
|
||||||
│ │ └── Layout.astro
|
|
||||||
│ └── pages/
|
|
||||||
│ └── index.astro
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
### `configs:*` permissions related to the configuration files
|
||||||
|
|
||||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
| name | description |
|
||||||
|
| :------------: | :--------------------------------: |
|
||||||
Any static assets, like images, can be placed in the `public/` directory.
|
| configs:create | Create a new configuration file |
|
||||||
|
| configs:get | Get an existing configuration file |
|
||||||
## 🧞 Commands
|
|
||||||
|
|
||||||
All commands are run from the root of the project, from a terminal:
|
|
||||||
|
|
||||||
| Command | Action |
|
|
||||||
| :------------------------ | :----------------------------------------------- |
|
|
||||||
| `npm install` | Installs dependencies |
|
|
||||||
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
|
||||||
| `npm run build` | Build your production site to `./dist/` |
|
|
||||||
| `npm run preview` | Preview your build locally, before deploying |
|
|
||||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
|
||||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
|
||||||
|
|
||||||
## 👀 Want to learn more?
|
|
||||||
|
|
||||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config'
|
||||||
import tailwind from "@astrojs/tailwind";
|
import tailwind from "@astrojs/tailwind"
|
||||||
import node from "@astrojs/node";
|
import node from "@astrojs/node"
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -17,5 +17,13 @@ export default defineConfig({
|
|||||||
output: 'server',
|
output: 'server',
|
||||||
adapter: node({
|
adapter: node({
|
||||||
mode: "standalone"
|
mode: "standalone"
|
||||||
})
|
}),
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
watch: {
|
||||||
|
// support WSL strange things
|
||||||
|
usePolling: !!process.env.WSL_DISTRO_NAME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
16
src/components/Passthrough.astro
Normal file
16
src/components/Passthrough.astro
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
const json = JSON.stringify(Astro.props)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note: you MUST only pass simple items that can go in JSON format natively
|
||||||
|
*/
|
||||||
|
export function load<T extends {} = {}>(): T {
|
||||||
|
const tag = document.querySelector<HTMLScriptElement>('#ASTRO_DATA')
|
||||||
|
if (!tag) {
|
||||||
|
throw new Error('could not load client variables, tag not found')
|
||||||
|
}
|
||||||
|
return JSON.parse(tag.innerText)
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
<script id="ASTRO_DATA" is:inline type="application/json" set:html={json}></script>
|
56
src/libs/CookieManager.ts
Normal file
56
src/libs/CookieManager.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import type { ServerResponse } from 'node:http'
|
||||||
|
|
||||||
|
export default class CookieManager {
|
||||||
|
private cookies: Record<string, string> = {}
|
||||||
|
public constructor(data?: string | Record<string, string>) {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
data.split(';').forEach((keyValuePair) => {
|
||||||
|
const [key, value] = keyValuePair.split('=')
|
||||||
|
this.cookies[key.trim()] = value.trim().replace(/%3B/g, ';')
|
||||||
|
})
|
||||||
|
} else if (typeof data === 'object') {
|
||||||
|
this.cookies = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static addCookie(res: ServerResponse, cookie: {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
expire?: string
|
||||||
|
maxAge?: number
|
||||||
|
domain?: string
|
||||||
|
path?: string
|
||||||
|
secure?: boolean
|
||||||
|
httpOnly?: boolean
|
||||||
|
sameSite?: 'Lax' | 'None' | 'Strict'}
|
||||||
|
) {
|
||||||
|
const items: Array<string> = [`${cookie.key}=${cookie.value.replace(/;/g, '%3B')}`]
|
||||||
|
if (cookie.expire) {
|
||||||
|
items.push(`Expires=${cookie.expire}`)
|
||||||
|
}
|
||||||
|
if (cookie.maxAge) {
|
||||||
|
items.push(`Max-Age=${cookie.maxAge}`)
|
||||||
|
}
|
||||||
|
if (cookie.domain) {
|
||||||
|
items.push(`Domain=${cookie.domain}`)
|
||||||
|
}
|
||||||
|
if (cookie.path) {
|
||||||
|
items.push(`Path=${cookie.path}`)
|
||||||
|
}
|
||||||
|
if (cookie.secure) {
|
||||||
|
items.push('Secure')
|
||||||
|
}
|
||||||
|
if (cookie.httpOnly) {
|
||||||
|
items.push('HttpOnly')
|
||||||
|
}
|
||||||
|
if (cookie.sameSite) {
|
||||||
|
items.push(`SameSite=${cookie.sameSite}`)
|
||||||
|
}
|
||||||
|
res.setHeader('Set-Cookie', items.join('; '))
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(key: string): string | undefined {
|
||||||
|
return this.cookies[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -52,6 +52,11 @@ export default interface RFC7807 {
|
|||||||
instance?: string
|
instance?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param error the error (base items are type, status, title details and instance)
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export function buildRFC7807(error: RFC7807 & Record<string, any>): Response {
|
export function buildRFC7807(error: RFC7807 & Record<string, any>): Response {
|
||||||
return new Response(JSON.stringify(error), {
|
return new Response(JSON.stringify(error), {
|
||||||
headers: {
|
headers: {
|
||||||
|
123
src/libs/validateAuth.ts
Normal file
123
src/libs/validateAuth.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import DaoFactory from '../models/DaoFactory'
|
||||||
|
import CookieManager from './CookieManager'
|
||||||
|
import RFC7807, { buildRFC7807 } from './RFCs/RFC7807'
|
||||||
|
|
||||||
|
interface Permission {
|
||||||
|
name: string
|
||||||
|
/**
|
||||||
|
* if set it will be usable by users
|
||||||
|
*
|
||||||
|
* else only users with the `admin.` prefix in the key can run it
|
||||||
|
*/
|
||||||
|
api: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if set to true it will pass if a cookie authenticate a valid user
|
||||||
|
*/
|
||||||
|
cookie: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validate the authentification
|
||||||
|
* @param request the request
|
||||||
|
* @param permission the permission to validate
|
||||||
|
* @returns a Response if the request is invalid, null otherwise
|
||||||
|
*
|
||||||
|
* TODO: implement rate limiting
|
||||||
|
* http/2.0 429 TOO MANY REQUESTS
|
||||||
|
* Content-Type: application/json
|
||||||
|
* X-RateLimit-Limit: 1000
|
||||||
|
* X-RateLimit-Remaining: 0
|
||||||
|
* X-RateLimit-Reset: 123456789
|
||||||
|
* X-RateLimit-Reset-After: 9 // Number of seconds before the remaining Rate go back to 0
|
||||||
|
*/
|
||||||
|
export async function validateAuth(request: Request, permission: Permission): Promise<Response | null> {
|
||||||
|
const apiKeyHeader = request.headers.get('Authorization')
|
||||||
|
const cookieHeader = request.headers.get('Cookie')
|
||||||
|
if (apiKeyHeader) {
|
||||||
|
const apiKey = apiKeyHeader.slice(apiKeyHeader.lastIndexOf(' ') + 1)
|
||||||
|
const dao = await DaoFactory.get('apiKey').findOne({
|
||||||
|
key: apiKey
|
||||||
|
})
|
||||||
|
const perm = permission.name.split('.')
|
||||||
|
const match = dao?.permissions.find((it) => {
|
||||||
|
const itSplit = it.split('.')
|
||||||
|
if (itSplit[0] === 'admin') {
|
||||||
|
itSplit.shift()
|
||||||
|
}
|
||||||
|
for (let idx = 0; idx < itSplit.length; idx++) {
|
||||||
|
const permissionLayer = itSplit[idx]
|
||||||
|
const requestPermissionLayer = perm[idx]
|
||||||
|
if (permissionLayer === '*') {
|
||||||
|
return true
|
||||||
|
} else if (permissionLayer !== requestPermissionLayer) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return itSplit.length === perm.length
|
||||||
|
// return it.endsWith(permission.name)
|
||||||
|
})
|
||||||
|
if (match && (permission.api || match.startsWith('admin.'))) {
|
||||||
|
return null
|
||||||
|
} else if (permission.api) {
|
||||||
|
return buildRFC7807({
|
||||||
|
type: '/unauthorized-access',
|
||||||
|
status: 401,
|
||||||
|
title: 'Unauthorized access',
|
||||||
|
details: `you are missing the permission "${permission.name}" or is not an admin`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (permission.api && !permission.cookie) {
|
||||||
|
return buildRFC7807({
|
||||||
|
type: '/unauthorized-access',
|
||||||
|
status: 401,
|
||||||
|
title: 'Unauthorized access',
|
||||||
|
details: `You MUST define an API key fo use this endpoint`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookieHeader && permission.cookie) {
|
||||||
|
// TODO: make a better cookie implementation
|
||||||
|
const cookies = new CookieManager(cookieHeader)
|
||||||
|
const userCookie = cookies.get('userId')
|
||||||
|
if (!userCookie) {
|
||||||
|
return buildRFC7807({
|
||||||
|
type: '/unauthorized-access',
|
||||||
|
status: 401,
|
||||||
|
title: 'Unauthorized access',
|
||||||
|
details: `you must be connected to use this endpoint (missing the userId cookie)`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const dao = await DaoFactory.get('user').get(userCookie)
|
||||||
|
if (!dao) {
|
||||||
|
return buildRFC7807({
|
||||||
|
type: '/unauthorized-access',
|
||||||
|
status: 401,
|
||||||
|
title: 'Unauthorized access',
|
||||||
|
details: `the user does not exists`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} else if (!permission.api && permission.cookie) {
|
||||||
|
return buildRFC7807({
|
||||||
|
type: '/unauthorized-access',
|
||||||
|
status: 401,
|
||||||
|
title: 'Unauthorized access',
|
||||||
|
details: `You MUST be connected to your account to use this endpoint`
|
||||||
|
})
|
||||||
|
} else if (permission.api && permission.cookie) {
|
||||||
|
return buildRFC7807({
|
||||||
|
type: '/unauthorized-access',
|
||||||
|
status: 401,
|
||||||
|
title: 'Unauthorized access',
|
||||||
|
details: `You must be connected or use an API key to access this endpoint`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return buildRFC7807({
|
||||||
|
type: '/unauthorized-access',
|
||||||
|
status: 401,
|
||||||
|
title: 'Unauthorized access',
|
||||||
|
details: `the following endpoint is not currently accessible by any means`
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
79
src/models/APIKey/APIKeyDao.ts
Normal file
79
src/models/APIKey/APIKeyDao.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { objectOmit } from '@dzeio/object-util'
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
import type APIKey from '.'
|
||||||
|
import Client from '../Client'
|
||||||
|
import Dao from '../Dao'
|
||||||
|
|
||||||
|
export default class APIKeyDao extends Dao<APIKey> {
|
||||||
|
|
||||||
|
// @ts-expect-error typing fix
|
||||||
|
private model = mongoose.models['APIKey'] as null ?? mongoose.model('APIKey', new mongoose.Schema({
|
||||||
|
user: { type: String, required: true },
|
||||||
|
key: { type: String, required: true, unique: true, index: true},
|
||||||
|
permissions: [{ type: String }]
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
}))
|
||||||
|
|
||||||
|
public async create(obj: Omit<APIKey, 'id' | 'created' | 'updated'>): Promise<APIKey | null> {
|
||||||
|
await Client.get()
|
||||||
|
return this.fromSource(await this.model.create(obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findAll(query?: Partial<APIKey> | undefined): Promise<APIKey[]> {
|
||||||
|
await Client.get()
|
||||||
|
try {
|
||||||
|
if (query?.id) {
|
||||||
|
const item = await this.model.findById(new mongoose.Types.ObjectId(query.id))
|
||||||
|
if (!item) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [this.fromSource(item)]
|
||||||
|
}
|
||||||
|
const resp = await this.model.find(query ? this.toSource(query as APIKey) : {})
|
||||||
|
return resp.map(this.fromSource)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(obj: APIKey): Promise<APIKey | null> {
|
||||||
|
await Client.get()
|
||||||
|
|
||||||
|
const query = await this.model.updateOne({
|
||||||
|
_id: new mongoose.Types.ObjectId(obj.id)
|
||||||
|
}, this.toSource(obj))
|
||||||
|
if (query.matchedCount >= 1) {
|
||||||
|
obj.updated = new Date()
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
// return this.fromSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(obj: APIKey): Promise<boolean> {
|
||||||
|
await Client.get()
|
||||||
|
const res = await this.model.deleteOne({
|
||||||
|
_id: new mongoose.Types.ObjectId(obj.id)
|
||||||
|
})
|
||||||
|
return res.deletedCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private toSource(obj: APIKey): Omit<APIKey, 'id'> {
|
||||||
|
return objectOmit(obj, 'id', 'updated', 'created')
|
||||||
|
}
|
||||||
|
|
||||||
|
private fromSource(doc: mongoose.Document<any, any, APIKey>): APIKey {
|
||||||
|
return {
|
||||||
|
id: doc._id.toString(),
|
||||||
|
user: doc.get('user'),
|
||||||
|
key: doc.get('key'),
|
||||||
|
permissions: doc.get('permissions') ?? [],
|
||||||
|
updated: doc.get('updatedAt'),
|
||||||
|
created: doc.get('createdAt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,21 +0,0 @@
|
|||||||
import type APIKey from '.'
|
|
||||||
import Dao from '../Dao'
|
|
||||||
|
|
||||||
export default class APIKeyDao extends Dao<APIKey> {
|
|
||||||
private idx = 0
|
|
||||||
public async create(obj: Omit<APIKey, 'id'>): Promise<APIKey | null> {
|
|
||||||
console.log('pouet', this.idx++)
|
|
||||||
return null
|
|
||||||
// throw new Error('Method not implemented.')
|
|
||||||
}
|
|
||||||
public async findAll(query?: Partial<APIKey> | undefined): Promise<APIKey[]> {
|
|
||||||
throw new Error('Method not implemented.')
|
|
||||||
}
|
|
||||||
public async update(obj: APIKey): Promise<APIKey | null> {
|
|
||||||
throw new Error('Method not implemented.')
|
|
||||||
}
|
|
||||||
public async delete(obj: APIKey): Promise<boolean> {
|
|
||||||
throw new Error('Method not implemented.')
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,4 +1,8 @@
|
|||||||
export default interface APIKey {
|
export default interface APIKey {
|
||||||
id: string
|
id: string
|
||||||
user: string
|
user: string
|
||||||
|
key: string
|
||||||
|
permissions: Array<string>
|
||||||
|
created: Date
|
||||||
|
updated: Date
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Client from './Client'
|
import APIKeyDao from './APIKey/APIKeyDao'
|
||||||
import ConfigDao from './Config/ConfigDao'
|
import ConfigDao from './Config/ConfigDao'
|
||||||
import Dao from './Dao'
|
import Dao from './Dao'
|
||||||
import UserDao from './User/UserDao'
|
import UserDao from './User/UserDao'
|
||||||
@ -17,6 +17,7 @@ import UserDao from './User/UserDao'
|
|||||||
interface DaoItem {
|
interface DaoItem {
|
||||||
config: ConfigDao
|
config: ConfigDao
|
||||||
user: UserDao
|
user: UserDao
|
||||||
|
apiKey: APIKeyDao
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,6 +58,7 @@ export default class DaoFactory {
|
|||||||
switch (item) {
|
switch (item) {
|
||||||
case 'config': return new ConfigDao()
|
case 'config': return new ConfigDao()
|
||||||
case 'user': return new UserDao()
|
case 'user': return new UserDao()
|
||||||
|
case 'apiKey': return new APIKeyDao()
|
||||||
default: return undefined
|
default: return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,19 +6,18 @@ import Dao from '../Dao'
|
|||||||
|
|
||||||
export default class UserDao extends Dao<User> {
|
export default class UserDao extends Dao<User> {
|
||||||
|
|
||||||
private model = mongoose.model('User', new mongoose.Schema({
|
// @ts-expect-error typing fix
|
||||||
|
private model = mongoose.models['User'] as null ?? mongoose.model('User', new mongoose.Schema({
|
||||||
email: { type: String, required: true }
|
email: { type: String, required: true }
|
||||||
}, {
|
}, {
|
||||||
timestamps: true
|
timestamps: true
|
||||||
}))
|
}))
|
||||||
|
|
||||||
private collection = 'users'
|
|
||||||
|
|
||||||
private idx = 0
|
|
||||||
public async create(obj: Omit<User, 'id' | 'created' | 'updated'>): Promise<User | null> {
|
public async create(obj: Omit<User, 'id' | 'created' | 'updated'>): Promise<User | null> {
|
||||||
await Client.get()
|
await Client.get()
|
||||||
return this.fromSource(await this.model.create(obj))
|
return this.fromSource(await this.model.create(obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findAll(query?: Partial<User> | undefined): Promise<User[]> {
|
public async findAll(query?: Partial<User> | undefined): Promise<User[]> {
|
||||||
await Client.get()
|
await Client.get()
|
||||||
if (query?.id) {
|
if (query?.id) {
|
||||||
@ -32,6 +31,7 @@ export default class UserDao extends Dao<User> {
|
|||||||
return resp.map(this.fromSource)
|
return resp.map(this.fromSource)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(obj: User): Promise<User | null> {
|
public async update(obj: User): Promise<User | null> {
|
||||||
await Client.get()
|
await Client.get()
|
||||||
|
|
||||||
@ -45,6 +45,7 @@ export default class UserDao extends Dao<User> {
|
|||||||
return null
|
return null
|
||||||
// return this.fromSource()
|
// return this.fromSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete(obj: User): Promise<boolean> {
|
public async delete(obj: User): Promise<boolean> {
|
||||||
await Client.get()
|
await Client.get()
|
||||||
const res = await this.model.deleteOne({
|
const res = await this.model.deleteOne({
|
||||||
|
41
src/pages/admin.astro
Normal file
41
src/pages/admin.astro
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
import Passthrough from '../components/Passthrough.astro'
|
||||||
|
import Layout from '../layouts/Layout.astro'
|
||||||
|
import DaoFactory from '../models/DaoFactory'
|
||||||
|
|
||||||
|
const user = await DaoFactory.get('user').get('648f81f857503c7d29465318')
|
||||||
|
const list = await DaoFactory.get('apiKey').findAll({
|
||||||
|
user: user!.id
|
||||||
|
})
|
||||||
|
const userId = user?.id ?? 'unknown'
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Welcome to Astro.">
|
||||||
|
<main>
|
||||||
|
<ul>
|
||||||
|
{list.map((it) => (
|
||||||
|
<li>
|
||||||
|
<p>access key: {it.key}</p>
|
||||||
|
<p>permissions: {it.permissions}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button>Create a new API Key</button>
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
<Passthrough userId={userId} />
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { load } from '../components/Passthrough.astro'
|
||||||
|
|
||||||
|
const vars = load<{userId: string}>()
|
||||||
|
console.log(vars)
|
||||||
|
document.querySelector('button')?.addEventListener('click', async () => {
|
||||||
|
await fetch(`/api/users/${vars.userId}/keys`, {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
</script>
|
19
src/pages/api/users/[userId]/keys/index.ts
Normal file
19
src/pages/api/users/[userId]/keys/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
import DaoFactory from '../../../../../models/DaoFactory'
|
||||||
|
|
||||||
|
export const post: APIRoute = async ({ params, request }) => {
|
||||||
|
const userId = params.userId as string
|
||||||
|
|
||||||
|
const dao = await DaoFactory.get('apiKey').create({
|
||||||
|
user: userId,
|
||||||
|
key: crypto.randomUUID(),
|
||||||
|
permissions: [
|
||||||
|
'admin.user.list'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
status: 201,
|
||||||
|
body: JSON.stringify(dao)
|
||||||
|
}
|
||||||
|
}
|
17
src/pages/api/users/index.ts
Normal file
17
src/pages/api/users/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import { validateAuth } from '../../../libs/validateAuth'
|
||||||
|
|
||||||
|
export const get: APIRoute = async ({ params, request }) => {
|
||||||
|
const requestInvalid = await validateAuth(request, {
|
||||||
|
name: 'user.list',
|
||||||
|
api: false,
|
||||||
|
cookie: true
|
||||||
|
})
|
||||||
|
if (requestInvalid) {
|
||||||
|
return requestInvalid
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: JSON.stringify({iam: true})
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user