Compare commits
10 commits
f9016ffc10
...
dda72bf67d
Author | SHA1 | Date | |
---|---|---|---|
Julien Oculi | dda72bf67d | ||
Julien Oculi | fa0b692b62 | ||
Julien Oculi | 6280b02934 | ||
Julien Oculi | 03aff06804 | ||
Julien Oculi | 0fc2da45ab | ||
Julien Oculi | e2b44cb662 | ||
Julien Oculi | 59bf2cd44d | ||
Julien Oculi | f7f3bbf0e0 | ||
Julien Oculi | 8ae76d8254 | ||
Julien Oculi | 0d78bc48ae |
55
deno.lock
Normal file
55
deno.lock
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"version": "3",
|
||||||
|
"packages": {
|
||||||
|
"specifiers": {
|
||||||
|
"jsr:@std/json@^0.223.0": "jsr:@std/json@0.223.0",
|
||||||
|
"jsr:@std/streams@^0.223.0": "jsr:@std/streams@0.223.0",
|
||||||
|
"npm:superjson@1.13.3": "npm:superjson@1.13.3"
|
||||||
|
},
|
||||||
|
"jsr": {
|
||||||
|
"@std/json@0.223.0": {
|
||||||
|
"integrity": "9a4a255931dd0397924c6b10bb6a72fe3e28ddd876b981ada2e3b8dd0764163f",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/streams@^0.223.0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@std/streams@0.223.0": {
|
||||||
|
"integrity": "d6b28e498ced3960b04dc5d251f2dcfc1df244b5ec5a48dc23a8f9b490be3b99"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"copy-anything@3.0.5": {
|
||||||
|
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-what": "is-what@4.1.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-what@4.1.16": {
|
||||||
|
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"superjson@1.13.3": {
|
||||||
|
"integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-anything": "copy-anything@3.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remote": {
|
||||||
|
"https://deno.land/std@0.203.0/dotenv/load.ts": "0636983549b98f29ab75c9a22a42d9723f0a389ece5498fe971e7bb2556a12e2",
|
||||||
|
"https://deno.land/std@0.203.0/dotenv/mod.ts": "1da8c6d0e7f7d8a5c2b19400b763bc11739df24acec235dda7ea2cfd3d300057",
|
||||||
|
"https://deno.land/std@0.216.0/json/_common.ts": "6444c6ea166514eb379f778d9216c1d8eb159f97c753aeeb1fc44cd091e30544",
|
||||||
|
"https://deno.land/std@0.216.0/json/common.ts": "33f1a4f39a45e2f9f357823fd0b5cf88b63fb4784b8c9a28f8120f70a20b23e9",
|
||||||
|
"https://deno.land/std@0.216.0/json/concatenated_json_parse_stream.ts": "13a707615e03e5ea93ac81e5f00420e3b7764c4e3fa88043e63bbac87ebe62ce",
|
||||||
|
"https://deno.land/std@0.216.0/json/json_parse_stream.ts": "2740652ea73726cd0f2edc89188b35d64a1ec15dc8cf7fd87db52a0170bc182c",
|
||||||
|
"https://deno.land/std@0.216.0/json/json_stringify_stream.ts": "269633e63d4e38ab3ba31e76a4292d11b9eb3151e26ff4f49dee1c264b4878fa",
|
||||||
|
"https://deno.land/std@0.216.0/json/mod.ts": "acd3e39a6c68c70ee7fa93991a8a8f02d799f7394db3cef09594e2dd8fd69814",
|
||||||
|
"https://deno.land/std@0.216.0/streams/to_transform_stream.ts": "4c4836455ef89bab9ece55975ee3a819f07d3d8b0e43101ec7f4ed033c8a2b61"
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/json@^0.223.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
128
src/db/mod.ts
Normal file
128
src/db/mod.ts
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import { Credential, Group, Machine, Ressource, Service, User } from '@models'
|
||||||
|
|
||||||
|
//!TODO link ressources (get, list)
|
||||||
|
//!TODO Purge unused ressources (delete)
|
||||||
|
|
||||||
|
export class Db {
|
||||||
|
#kv: Deno.Kv
|
||||||
|
static async init(path?: string) {
|
||||||
|
const kv = await Deno.openKv(path ?? Deno.env.get('DB_PATH'))
|
||||||
|
return new Db(kv)
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(kv: Deno.Kv) {
|
||||||
|
this.#kv = kv
|
||||||
|
}
|
||||||
|
|
||||||
|
get storage() {
|
||||||
|
return this.#kv
|
||||||
|
}
|
||||||
|
|
||||||
|
get prefix() {
|
||||||
|
return {
|
||||||
|
ressource: 'ressource',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get ressource() {
|
||||||
|
return {
|
||||||
|
credential: this.#ressourceStorage<Credential>('credential', Credential),
|
||||||
|
group: this.#ressourceStorage<Group>('group', Group),
|
||||||
|
machine: this.#ressourceStorage<Machine>('machine', Machine),
|
||||||
|
service: this.#ressourceStorage<Service>('service', Service),
|
||||||
|
user: this.#ressourceStorage<User>('user', User),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ressourceStorage<T extends Ressource>(
|
||||||
|
type: RessourceType<T>,
|
||||||
|
Builder: RessourceBuilder<T>,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
get: (ressource: Pick<T, 'uuid'>) =>
|
||||||
|
this.#get<T>(type, Builder, ressource),
|
||||||
|
set: (ressources: T[]) => this.#set<T>(type, ressources),
|
||||||
|
delete: (ressources: Pick<T, 'uuid'>[]) =>
|
||||||
|
this.#delete<T>(type, ressources),
|
||||||
|
list: (filter: (ressource: T) => boolean = () => true) =>
|
||||||
|
this.#list<T>(type, Builder, filter),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #get<T extends Ressource>(
|
||||||
|
type: RessourceType<T>,
|
||||||
|
Builder: RessourceBuilder<T>,
|
||||||
|
entry: Pick<T, 'uuid'>,
|
||||||
|
): Promise<T> {
|
||||||
|
const json = await this.#kv.get<RessourceJson<T>>([
|
||||||
|
this.prefix.ressource,
|
||||||
|
type,
|
||||||
|
entry.uuid,
|
||||||
|
])
|
||||||
|
if (json.value) {
|
||||||
|
//@ts-expect-error Type union of Ressource types for Builder
|
||||||
|
return Builder.fromJSON(json.value)
|
||||||
|
}
|
||||||
|
throw new ReferenceError(
|
||||||
|
`no ressource.${type} was found with uuid: "${entry.uuid}"`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#set<T extends Ressource>(type: RessourceType<T>, entries: T[]) {
|
||||||
|
const atomic = this.#kv.atomic()
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
//! TODO check if `refs` exists
|
||||||
|
const key = [this.prefix.ressource, type, entry.uuid]
|
||||||
|
atomic.set(key, entry.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
return atomic.commit()
|
||||||
|
}
|
||||||
|
#delete<T extends Ressource>(
|
||||||
|
type: RessourceType<T>,
|
||||||
|
entries: Pick<T, 'uuid'>[],
|
||||||
|
): Promise<Deno.KvCommitResult | Deno.KvCommitError> {
|
||||||
|
const atomic = this.#kv.atomic()
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
//! TODO check if `refs` exists
|
||||||
|
atomic.delete([this.prefix.ressource, type, entry.uuid])
|
||||||
|
}
|
||||||
|
|
||||||
|
return atomic.commit()
|
||||||
|
}
|
||||||
|
async *#list<T extends Ressource>(
|
||||||
|
type: RessourceType<T>,
|
||||||
|
Builder: RessourceBuilder<T>,
|
||||||
|
filter: (entry: T) => boolean,
|
||||||
|
): AsyncGenerator<T, void, void> {
|
||||||
|
const list = this.#kv.list<RessourceJson<T>>({
|
||||||
|
prefix: [this.prefix.ressource, type],
|
||||||
|
})
|
||||||
|
for await (const entry of list) {
|
||||||
|
const value = entry.value
|
||||||
|
//@ts-expect-error Type union of Ressource types for Builder
|
||||||
|
const ressource = Builder.fromJSON(value) as T
|
||||||
|
if (filter(ressource)) {
|
||||||
|
yield ressource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RessourceType<T extends Ressource> = T extends Credential ? 'credential'
|
||||||
|
: T extends Group ? 'group'
|
||||||
|
: T extends Machine ? 'machine'
|
||||||
|
: T extends Service ? 'service'
|
||||||
|
: T extends User ? 'user'
|
||||||
|
: never
|
||||||
|
|
||||||
|
type RessourceBuilder<T extends Ressource> = T extends Credential
|
||||||
|
? typeof Credential
|
||||||
|
: T extends Group ? typeof Group
|
||||||
|
: T extends Machine ? typeof Machine
|
||||||
|
: T extends Service ? typeof Service
|
||||||
|
: T extends User ? typeof User
|
||||||
|
: never
|
||||||
|
|
||||||
|
type RessourceJson<T extends Ressource> = ReturnType<T['toJSON']>
|
11
src/models/mod.ts
Normal file
11
src/models/mod.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export {
|
||||||
|
Ref,
|
||||||
|
type RefResolver,
|
||||||
|
type RefString,
|
||||||
|
} from '@/src/models/utils/ref.ts'
|
||||||
|
export { Credential } from '@models/credential.ts'
|
||||||
|
export { Group } from '@models/group.ts'
|
||||||
|
export { Machine } from '@models/machine.ts'
|
||||||
|
export type { Ressource } from '@models/ressource.ts'
|
||||||
|
export { Service } from '@models/service.ts'
|
||||||
|
export { User } from '@models/user.ts'
|
105
src/models/src/credential.ts
Normal file
105
src/models/src/credential.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { ToJson } from '@/types.ts'
|
||||||
|
import { Ressource } from '@models/ressource.ts'
|
||||||
|
|
||||||
|
export class Credential extends Ressource {
|
||||||
|
static fromJSON(
|
||||||
|
json: ToJson<Credential>,
|
||||||
|
): Credential {
|
||||||
|
return new Credential(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(
|
||||||
|
{ category, store, name }: Pick<Credential, 'category' | 'store' | 'name'>,
|
||||||
|
): Credential {
|
||||||
|
const { uuid, createdAt, updatedAt } = super.create({ name })
|
||||||
|
return new Credential({ uuid, createdAt, updatedAt, name, category, store })
|
||||||
|
}
|
||||||
|
|
||||||
|
#category: 'password' | 'ssh' | 'passkey'
|
||||||
|
#store: CredentialCategory<Credential['category']>
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
{ category, store, ...props }:
|
||||||
|
& Pick<Credential, 'category' | 'store'>
|
||||||
|
& Pick<Ressource, 'name' | 'uuid' | 'createdAt' | 'updatedAt'>,
|
||||||
|
) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
if (!['password', 'ssh', 'passkey'].includes(category)) {
|
||||||
|
throw new TypeError(
|
||||||
|
`category is "${category}" but ('password' | 'ssh' | 'passkey') is required`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.#category = category
|
||||||
|
this.#store = Object.freeze(store)
|
||||||
|
}
|
||||||
|
|
||||||
|
get type(): 'credential' {
|
||||||
|
return 'credential'
|
||||||
|
}
|
||||||
|
|
||||||
|
get category() {
|
||||||
|
return this.#category
|
||||||
|
}
|
||||||
|
|
||||||
|
get store() {
|
||||||
|
return Object.freeze(this.#store)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(
|
||||||
|
props: Partial<Omit<Credential, 'type' | 'uuid' | 'createdAt'>>,
|
||||||
|
): Credential {
|
||||||
|
const { updatedAt } = super.update(props)
|
||||||
|
const credential = new Credential({ ...this, ...props, updatedAt })
|
||||||
|
return credential
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
...super.toJSON(),
|
||||||
|
type: this.type,
|
||||||
|
category: this.category,
|
||||||
|
store: Object.freeze(this.store),
|
||||||
|
} as const
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `Credential (${JSON.stringify(this)})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Credential extends Ressource {
|
||||||
|
type: 'credential'
|
||||||
|
category: 'password' | 'ssh' | 'passkey'
|
||||||
|
store: Readonly<CredentialCategory<Credential['category']>>
|
||||||
|
}
|
||||||
|
|
||||||
|
type CredentialCategory<T extends 'password' | 'ssh' | 'passkey'> = T extends
|
||||||
|
'password' ? {
|
||||||
|
store: {
|
||||||
|
hash: string //hex or b64 of Uint8Array
|
||||||
|
alg: string
|
||||||
|
salt: string //hex or b64 of Uint8Array
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: T extends 'ssh' ? {
|
||||||
|
store: {
|
||||||
|
publicKey: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: T extends 'passkey' ? {
|
||||||
|
store: Record<string, unknown>
|
||||||
|
}
|
||||||
|
: never
|
||||||
|
|
||||||
|
/*
|
||||||
|
PassKey store:
|
||||||
|
{
|
||||||
|
publicKey: Uint8Array
|
||||||
|
keyId: string
|
||||||
|
transport: string
|
||||||
|
counter: number
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
//new Uint8Array(Object.values(JSON.parse(JSON.stringify(new Uint8Array([1, 2, 3])))))
|
113
src/models/src/machine.ts
Normal file
113
src/models/src/machine.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import { ToJson } from '@/types.ts'
|
||||||
|
import { Group } from '@models/group.ts'
|
||||||
|
import { Ressource } from '@models/ressource.ts'
|
||||||
|
import { Ref } from '@/src/models/utils/ref.ts'
|
||||||
|
|
||||||
|
export class Machine extends Ressource {
|
||||||
|
static fromJSON(
|
||||||
|
json: ToJson<Machine>,
|
||||||
|
): Machine {
|
||||||
|
const url = new URL(json.url)
|
||||||
|
const avatar = new URL(json.avatar)
|
||||||
|
const groups = json.groups.map((group) => Ref.fromString<Group>(group))
|
||||||
|
|
||||||
|
return new Machine({ ...json, url, avatar, groups })
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(
|
||||||
|
{ tags, url, status, avatar, groups, ...props }:
|
||||||
|
& Pick<Machine, 'tags' | 'url' | 'status' | 'avatar' | 'groups'>
|
||||||
|
& Pick<Ressource, 'name' | 'uuid' | 'createdAt' | 'updatedAt'>,
|
||||||
|
): Machine {
|
||||||
|
return new Machine({ ...props, tags, url, status, avatar, groups })
|
||||||
|
}
|
||||||
|
|
||||||
|
#tags: readonly string[]
|
||||||
|
#url: URL
|
||||||
|
#status:
|
||||||
|
| 'ready'
|
||||||
|
| 'busy'
|
||||||
|
| 'unavailable'
|
||||||
|
| 'discontinued'
|
||||||
|
| 'error'
|
||||||
|
| 'unknown'
|
||||||
|
#avatar: URL
|
||||||
|
#groups: readonly Ref<Group>[]
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
{ tags, url, status, avatar, groups, ...props }:
|
||||||
|
& Pick<Machine, 'tags' | 'url' | 'status' | 'avatar' | 'groups'>
|
||||||
|
& Pick<Ressource, 'name' | 'uuid' | 'createdAt' | 'updatedAt'>,
|
||||||
|
) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.#tags = Object.freeze(tags)
|
||||||
|
this.#url = new URL(url)
|
||||||
|
if (!['working', 'pending', 'ready', 'unavailable'].includes(status)) {
|
||||||
|
throw new TypeError(
|
||||||
|
`status is "${status}" but ('ready' | 'busy' | 'unavailable' | 'discontinued' | 'error' | 'unknown') is required`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.#status = status
|
||||||
|
this.#avatar = new URL(avatar)
|
||||||
|
this.#groups = Object.freeze(groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
get type(): 'machine' {
|
||||||
|
return 'machine'
|
||||||
|
}
|
||||||
|
get tags() {
|
||||||
|
return this.#tags.slice()
|
||||||
|
}
|
||||||
|
get url() {
|
||||||
|
return new URL(this.#url)
|
||||||
|
}
|
||||||
|
get status() {
|
||||||
|
return this.#status
|
||||||
|
}
|
||||||
|
get avatar() {
|
||||||
|
return new URL(this.#avatar)
|
||||||
|
}
|
||||||
|
get groups() {
|
||||||
|
return this.#groups
|
||||||
|
}
|
||||||
|
|
||||||
|
update(
|
||||||
|
props: Partial<Omit<Machine, 'type' | 'uuid' | 'createdAt'>>,
|
||||||
|
): Machine {
|
||||||
|
const { updatedAt } = super.update(props)
|
||||||
|
const machine = new Machine({ ...this, ...props, updatedAt })
|
||||||
|
return machine
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
...super.toJSON(),
|
||||||
|
type: this.type,
|
||||||
|
tags: this.tags.slice(),
|
||||||
|
url: this.url.toJSON(),
|
||||||
|
status: this.status,
|
||||||
|
avatar: this.avatar.toJSON(),
|
||||||
|
groups: Object.freeze(this.groups.map((group) => group.toJSON())),
|
||||||
|
} as const
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `Machine (${JSON.stringify(this)})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Machine extends Ressource {
|
||||||
|
type: 'machine'
|
||||||
|
tags: string[]
|
||||||
|
url: URL
|
||||||
|
status:
|
||||||
|
| 'ready'
|
||||||
|
| 'busy'
|
||||||
|
| 'unavailable'
|
||||||
|
| 'discontinued'
|
||||||
|
| 'error'
|
||||||
|
| 'unknown'
|
||||||
|
avatar: URL
|
||||||
|
groups: readonly Ref<Group>[]
|
||||||
|
}
|
|
@ -37,6 +37,40 @@ export class Ref<T extends Ressource> extends String {
|
||||||
return new Ref<T>(Ref.parse(string))
|
return new Ref<T>(Ref.parse(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static dbResolver(db: Db) {
|
||||||
|
return <T extends Ressource>(ref: RefString<T>) => {
|
||||||
|
const { type, uuid } = Ref.parse(ref)
|
||||||
|
return db.ressource[type].get({ uuid })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static restResolver(endpoint: string | URL) {
|
||||||
|
return async <T extends Ressource>(ref: RefString<T>) => {
|
||||||
|
const { type, uuid } = Ref.parse(ref)
|
||||||
|
const url = new URL(`${type}s/${uuid}`, endpoint)
|
||||||
|
const response = await fetch(url)
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
if (type === 'user') {
|
||||||
|
return User.fromJSON(json)
|
||||||
|
}
|
||||||
|
if (type === 'machine') {
|
||||||
|
return Machine.fromJSON(json)
|
||||||
|
}
|
||||||
|
if (type === 'service') {
|
||||||
|
return Service.fromJSON(json)
|
||||||
|
}
|
||||||
|
if (type === 'group') {
|
||||||
|
return Group.fromJSON(json)
|
||||||
|
}
|
||||||
|
if (type === 'credential') {
|
||||||
|
return Credential.fromJSON(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError(`unknown ref type "${type}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private constructor({ uuid, type }: { uuid: UUID; type: T['type'] }) {
|
private constructor({ uuid, type }: { uuid: UUID; type: T['type'] }) {
|
||||||
super(Ref.#toString({ uuid, type }))
|
super(Ref.#toString({ uuid, type }))
|
||||||
this.#type = type
|
this.#type = type
|
||||||
|
@ -66,37 +100,3 @@ export class Ref<T extends Ressource> extends String {
|
||||||
return this.toString()
|
return this.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RefDbResolver(db: Db) {
|
|
||||||
return <T extends Ressource>(ref: RefString<T>) => {
|
|
||||||
const { type, uuid } = Ref.parse(ref)
|
|
||||||
return db.ressource[type].get({ uuid })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RefRestResolver(endpoint: string) {
|
|
||||||
return async <T extends Ressource>(ref: RefString<T>) => {
|
|
||||||
const { type, uuid } = Ref.parse(ref)
|
|
||||||
const url = new URL(`${type}/${uuid}`, endpoint)
|
|
||||||
const response = await fetch(url)
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
if (type === 'user') {
|
|
||||||
return User.fromJSON(json)
|
|
||||||
}
|
|
||||||
if (type === 'machine') {
|
|
||||||
return Machine.fromJSON(json)
|
|
||||||
}
|
|
||||||
if (type === 'service') {
|
|
||||||
return Service.fromJSON(json)
|
|
||||||
}
|
|
||||||
if (type === 'group') {
|
|
||||||
return Group.fromJSON(json)
|
|
||||||
}
|
|
||||||
if (type === 'credential') {
|
|
||||||
return Credential.fromJSON(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TypeError(`unknown ref type "${type}"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
28
utils.ts
Normal file
28
utils.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Login } from '@/types.ts'
|
||||||
|
|
||||||
|
export function toLogin({
|
||||||
|
firstname,
|
||||||
|
lastname,
|
||||||
|
}: {
|
||||||
|
firstname: string
|
||||||
|
lastname: string
|
||||||
|
}): Login {
|
||||||
|
return `${sanitizeString(firstname)}.${sanitizeString(lastname)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeString(str: string): string {
|
||||||
|
return str.toLocaleLowerCase().split('').map(sanitizeChar).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeChar(char: string): string {
|
||||||
|
//decompose unicode and remove diacritical marks
|
||||||
|
char = char.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||||
|
if (char.match(/[a-zA-Z0-9]|-|_/)) return char
|
||||||
|
return '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const regex = {
|
||||||
|
uuidV4: /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/,
|
||||||
|
isoDateString: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z$/,
|
||||||
|
mailAddress: /\S+@\S+\.\S+/,
|
||||||
|
}
|
Loading…
Reference in a new issue