|
|
@@ -10,6 +10,44 @@ export const Method = z
|
|
|
.object({
|
|
|
type: z.union([z.literal("oauth"), z.literal("api")]),
|
|
|
label: z.string(),
|
|
|
+ prompts: z
|
|
|
+ .array(
|
|
|
+ z.union([
|
|
|
+ z.object({
|
|
|
+ type: z.literal("text"),
|
|
|
+ key: z.string(),
|
|
|
+ message: z.string(),
|
|
|
+ placeholder: z.string().optional(),
|
|
|
+ when: z
|
|
|
+ .object({
|
|
|
+ key: z.string(),
|
|
|
+ op: z.union([z.literal("eq"), z.literal("neq")]),
|
|
|
+ value: z.string(),
|
|
|
+ })
|
|
|
+ .optional(),
|
|
|
+ }),
|
|
|
+ z.object({
|
|
|
+ type: z.literal("select"),
|
|
|
+ key: z.string(),
|
|
|
+ message: z.string(),
|
|
|
+ options: z.array(
|
|
|
+ z.object({
|
|
|
+ label: z.string(),
|
|
|
+ value: z.string(),
|
|
|
+ hint: z.string().optional(),
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ when: z
|
|
|
+ .object({
|
|
|
+ key: z.string(),
|
|
|
+ op: z.union([z.literal("eq"), z.literal("neq")]),
|
|
|
+ value: z.string(),
|
|
|
+ })
|
|
|
+ .optional(),
|
|
|
+ }),
|
|
|
+ ]),
|
|
|
+ )
|
|
|
+ .optional(),
|
|
|
})
|
|
|
.meta({
|
|
|
ref: "ProviderAuthMethod",
|
|
|
@@ -43,16 +81,29 @@ export const OauthCodeMissing = NamedError.create(
|
|
|
|
|
|
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
|
|
|
|
|
|
+export const ValidationFailed = NamedError.create(
|
|
|
+ "ProviderAuthValidationFailed",
|
|
|
+ z.object({
|
|
|
+ field: z.string(),
|
|
|
+ message: z.string(),
|
|
|
+ }),
|
|
|
+)
|
|
|
+
|
|
|
export type ProviderAuthError =
|
|
|
| Auth.AuthServiceError
|
|
|
| InstanceType<typeof OauthMissing>
|
|
|
| InstanceType<typeof OauthCodeMissing>
|
|
|
| InstanceType<typeof OauthCallbackFailed>
|
|
|
+ | InstanceType<typeof ValidationFailed>
|
|
|
|
|
|
export namespace ProviderAuthService {
|
|
|
export interface Service {
|
|
|
readonly methods: () => Effect.Effect<Record<string, Method[]>>
|
|
|
- readonly authorize: (input: { providerID: ProviderID; method: number }) => Effect.Effect<Authorization | undefined>
|
|
|
+ readonly authorize: (input: {
|
|
|
+ providerID: ProviderID
|
|
|
+ method: number
|
|
|
+ inputs?: Record<string, string>
|
|
|
+ }) => Effect.Effect<Authorization | undefined, ProviderAuthError>
|
|
|
readonly callback: (input: {
|
|
|
providerID: ProviderID
|
|
|
method: number
|
|
|
@@ -80,16 +131,52 @@ export class ProviderAuthService extends ServiceMap.Service<ProviderAuthService,
|
|
|
const pending = new Map<ProviderID, AuthOuathResult>()
|
|
|
|
|
|
const methods = Effect.fn("ProviderAuthService.methods")(function* () {
|
|
|
- return Record.map(hooks, (item) => item.methods.map((method): Method => Struct.pick(method, ["type", "label"])))
|
|
|
+ return Record.map(hooks, (item) =>
|
|
|
+ item.methods.map(
|
|
|
+ (method): Method => ({
|
|
|
+ type: method.type,
|
|
|
+ label: method.label,
|
|
|
+ prompts: method.prompts?.map((prompt) => {
|
|
|
+ if (prompt.type === "select") {
|
|
|
+ return {
|
|
|
+ type: "select" as const,
|
|
|
+ key: prompt.key,
|
|
|
+ message: prompt.message,
|
|
|
+ options: prompt.options,
|
|
|
+ when: prompt.when,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ type: "text" as const,
|
|
|
+ key: prompt.key,
|
|
|
+ message: prompt.message,
|
|
|
+ placeholder: prompt.placeholder,
|
|
|
+ when: prompt.when,
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ )
|
|
|
})
|
|
|
|
|
|
const authorize = Effect.fn("ProviderAuthService.authorize")(function* (input: {
|
|
|
providerID: ProviderID
|
|
|
method: number
|
|
|
+ inputs?: Record<string, string>
|
|
|
}) {
|
|
|
const method = hooks[input.providerID].methods[input.method]
|
|
|
if (method.type !== "oauth") return
|
|
|
- const result = yield* Effect.promise(() => method.authorize())
|
|
|
+
|
|
|
+ if (method.prompts && input.inputs) {
|
|
|
+ for (const prompt of method.prompts) {
|
|
|
+ if (prompt.type === "text" && prompt.validate && input.inputs[prompt.key] !== undefined) {
|
|
|
+ const error = prompt.validate(input.inputs[prompt.key])
|
|
|
+ if (error) return yield* Effect.fail(new ValidationFailed({ field: prompt.key, message: error }))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = yield* Effect.promise(() => method.authorize(input.inputs))
|
|
|
pending.set(input.providerID, result)
|
|
|
return {
|
|
|
url: result.url,
|