feat: overhaul admin user management and desktop UX
This commit is contained in:
parent
7d6f3bea01
commit
ecad81b0ea
16 changed files with 1546 additions and 395 deletions
|
|
@ -1,7 +1,21 @@
|
|||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { ConvexError, v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { requireStaff } from "./rbac";
|
||||
|
||||
function normalizeSlug(input?: string | null): string | undefined {
|
||||
if (!input) return undefined
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) return undefined
|
||||
const ascii = trimmed
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[\u2013\u2014]/g, "-")
|
||||
const sanitized = ascii.replace(/[^\w\s-]/g, "").replace(/[_\s]+/g, "-")
|
||||
const collapsed = sanitized.replace(/-+/g, "-").toLowerCase()
|
||||
const normalized = collapsed.replace(/^-+|-+$/g, "")
|
||||
return normalized || undefined
|
||||
}
|
||||
|
||||
export const list = query({
|
||||
args: { tenantId: v.string(), viewerId: v.id("users") },
|
||||
handler: async (ctx, { tenantId, viewerId }) => {
|
||||
|
|
@ -13,3 +27,56 @@ export const list = query({
|
|||
return companies.map((c) => ({ id: c._id, name: c.name, slug: c.slug }))
|
||||
},
|
||||
})
|
||||
|
||||
export const ensureProvisioned = mutation({
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
slug: v.string(),
|
||||
name: v.string(),
|
||||
},
|
||||
handler: async (ctx, { tenantId, slug, name }) => {
|
||||
const normalizedSlug = normalizeSlug(slug)
|
||||
if (!normalizedSlug) {
|
||||
throw new ConvexError("Slug inválido")
|
||||
}
|
||||
const trimmedName = name.trim()
|
||||
if (!trimmedName) {
|
||||
throw new ConvexError("Nome inválido")
|
||||
}
|
||||
|
||||
const existing = await ctx.db
|
||||
.query("companies")
|
||||
.withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", normalizedSlug))
|
||||
.unique()
|
||||
|
||||
if (existing) {
|
||||
return {
|
||||
id: existing._id,
|
||||
slug: existing.slug,
|
||||
name: existing.name,
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const id = await ctx.db.insert("companies", {
|
||||
tenantId,
|
||||
name: trimmedName,
|
||||
slug: normalizedSlug,
|
||||
isAvulso: false,
|
||||
contractedHoursPerMonth: undefined,
|
||||
cnpj: undefined,
|
||||
domain: undefined,
|
||||
phone: undefined,
|
||||
description: undefined,
|
||||
address: undefined,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
return {
|
||||
id,
|
||||
slug: normalizedSlug,
|
||||
name: trimmedName,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,6 +16,20 @@ type NormalizedIdentifiers = {
|
|||
serials: string[]
|
||||
}
|
||||
|
||||
function normalizeCompanySlug(input?: string | null): string | undefined {
|
||||
if (!input) return undefined
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) return undefined
|
||||
const ascii = trimmed
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[\u2013\u2014]/g, "-")
|
||||
const sanitized = ascii.replace(/[^\w\s-]/g, "").replace(/[_\s]+/g, "-")
|
||||
const collapsed = sanitized.replace(/-+/g, "-").toLowerCase()
|
||||
const normalized = collapsed.replace(/^-+|-+$/g, "")
|
||||
return normalized || undefined
|
||||
}
|
||||
|
||||
function getProvisioningSecret() {
|
||||
const secret = process.env["MACHINE_PROVISIONING_SECRET"]
|
||||
if (!secret) {
|
||||
|
|
@ -92,10 +106,11 @@ async function ensureCompany(
|
|||
tenantId: string,
|
||||
companySlug?: string
|
||||
): Promise<{ companyId?: Id<"companies">; companySlug?: string }> {
|
||||
if (!companySlug) return {}
|
||||
const normalized = normalizeCompanySlug(companySlug)
|
||||
if (!normalized) return {}
|
||||
const company = await ctx.db
|
||||
.query("companies")
|
||||
.withIndex("by_tenant_slug", (q: any) => q.eq("tenantId", tenantId).eq("slug", companySlug))
|
||||
.withIndex("by_tenant_slug", (q: any) => q.eq("tenantId", tenantId).eq("slug", normalized))
|
||||
.unique()
|
||||
if (!company) {
|
||||
throw new ConvexError("Empresa não encontrada para o tenant informado")
|
||||
|
|
@ -317,9 +332,10 @@ export const register = mutation({
|
|||
}
|
||||
|
||||
const tenantId = args.tenantId ?? DEFAULT_TENANT_ID
|
||||
const normalizedCompanySlug = normalizeCompanySlug(args.companySlug)
|
||||
const identifiers = normalizeIdentifiers(args.macAddresses, args.serialNumbers)
|
||||
const fingerprint = computeFingerprint(tenantId, args.companySlug, args.hostname, identifiers)
|
||||
const { companyId, companySlug } = await ensureCompany(ctx, tenantId, args.companySlug)
|
||||
const fingerprint = computeFingerprint(tenantId, normalizedCompanySlug, args.hostname, identifiers)
|
||||
const { companyId, companySlug } = await ensureCompany(ctx, tenantId, normalizedCompanySlug)
|
||||
const now = Date.now()
|
||||
const metadataPatch = args.metadata && typeof args.metadata === "object" ? (args.metadata as Record<string, unknown>) : undefined
|
||||
|
||||
|
|
@ -454,9 +470,10 @@ export const upsertInventory = mutation({
|
|||
}
|
||||
|
||||
const tenantId = args.tenantId ?? DEFAULT_TENANT_ID
|
||||
const normalizedCompanySlug = normalizeCompanySlug(args.companySlug)
|
||||
const identifiers = normalizeIdentifiers(args.macAddresses, args.serialNumbers)
|
||||
const fingerprint = computeFingerprint(tenantId, args.companySlug, args.hostname, identifiers)
|
||||
const { companyId, companySlug } = await ensureCompany(ctx, tenantId, args.companySlug)
|
||||
const fingerprint = computeFingerprint(tenantId, normalizedCompanySlug, args.hostname, identifiers)
|
||||
const { companyId, companySlug } = await ensureCompany(ctx, tenantId, normalizedCompanySlug)
|
||||
const now = Date.now()
|
||||
|
||||
const metadataPatch: Record<string, unknown> = {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue