diff --git a/index.ts b/index.ts index 2d911bf..1f58a55 100644 --- a/index.ts +++ b/index.ts @@ -7,16 +7,14 @@ * file that was distributed with this source code. */ -import './src/bindings/types.js' - export { HttpClient as ApiRequest } from '@poppinss/oauth-client' export * as errors from './src/errors.js' +export { configure } from './configure.js' +export { stubsRoot } from './stubs/main.js' export { AllyManager } from './src/ally_manager.js' export { defineConfig } from './src/define_config.js' + export { RedirectRequest } from './src/redirect_request.js' export { Oauth1Driver } from './src/abstract_drivers/oauth1.js' export { Oauth2Driver } from './src/abstract_drivers/oauth2.js' -export { default as driversList } from './src/drivers_collection.js' -export { stubsRoot } from './stubs/main.js' -export { configure } from './configure.js' diff --git a/providers/ally_provider.ts b/providers/ally_provider.ts index f80277d..85df942 100644 --- a/providers/ally_provider.ts +++ b/providers/ally_provider.ts @@ -7,10 +7,19 @@ * file that was distributed with this source code. */ +import { configProvider } from '@adonisjs/core' +import { HttpContext } from '@adonisjs/core/http' +import { RuntimeException } from '@poppinss/utils' import type { ApplicationService } from '@adonisjs/core/types' -import driversList from '../src/drivers_collection.js' -import { extendHttpContext } from '../src/bindings/http_context.js' +import type { AllyService } from '../src/types.js' +import { AllyManager } from '../src/ally_manager.js' + +declare module '@adonisjs/core/http' { + export interface HttpContext { + ally: AllyService + } +} /** * AllyProvider extends the HTTP context with the "ally" property @@ -19,8 +28,27 @@ export default class AllyProvider { constructor(protected app: ApplicationService) {} async boot() { - const config = this.app.config.get('ally') - extendHttpContext(config.services) - await driversList.registerBundledDrivers(config.driversInUse) + const allyConfigProvider = this.app.config.get('ally') + + /** + * Resolve config from the provider + */ + const config = await configProvider.resolve(this.app, allyConfigProvider) + if (!config) { + throw new RuntimeException( + 'Invalid "config/ally.ts" file. Make sure you are using the "defineConfig" method' + ) + } + + /** + * Setup HTTPContext getter + */ + HttpContext.getter( + 'ally', + function (this: HttpContext) { + return new AllyManager(config, this) as unknown as AllyService + }, + true + ) } } diff --git a/src/abstract_drivers/oauth1.ts b/src/abstract_drivers/oauth1.ts index a6ded05..148a199 100644 --- a/src/abstract_drivers/oauth1.ts +++ b/src/abstract_drivers/oauth1.ts @@ -8,8 +8,8 @@ */ import { Exception } from '@poppinss/utils' -import { Oauth1Client } from '@poppinss/oauth-client/oauth1' import type { HttpContext } from '@adonisjs/core/http' +import { Oauth1Client } from '@poppinss/oauth-client/oauth1' import { AllyUserContract, diff --git a/src/abstract_drivers/oauth2.ts b/src/abstract_drivers/oauth2.ts index 7cf89e8..90ee02f 100644 --- a/src/abstract_drivers/oauth2.ts +++ b/src/abstract_drivers/oauth2.ts @@ -8,8 +8,8 @@ */ import { Exception } from '@poppinss/utils' -import { Oauth2Client } from '@poppinss/oauth-client/oauth2' import type { HttpContext } from '@adonisjs/core/http' +import { Oauth2Client } from '@poppinss/oauth-client/oauth2' import { AllyUserContract, diff --git a/src/bindings/http_context.ts b/src/bindings/http_context.ts deleted file mode 100644 index 47c0e7f..0000000 --- a/src/bindings/http_context.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { HttpContext } from '@adonisjs/core/http' - -import './types.js' -import { AllyManager } from '../ally_manager.js' -import { AllyManagerDriverFactory } from '../types.js' - -/** - * Extends HttpContext class with the ally getter - */ -export function extendHttpContext(config: Record) { - HttpContext.getter( - 'ally', - function (this: HttpContext) { - return new AllyManager(config, this) as unknown as HttpContext['ally'] - }, - true - ) -} diff --git a/src/bindings/types.ts b/src/bindings/types.ts deleted file mode 100644 index 23bfa85..0000000 --- a/src/bindings/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import type { AllyManager } from '../ally_manager.js' -import type { AllyManagerDriverFactory, SocialProviders } from '../types.js' - -/** - * In order for types to get picked up, this module must get - * imported by TypeScript. Therefore, we export this module - * from the package entrypoint - */ - -declare module '@adonisjs/core/http' { - interface HttpContext { - ally: AllyManager< - SocialProviders extends Record ? SocialProviders : never - > - } -} diff --git a/src/defaults/config.ts b/src/defaults/config.ts deleted file mode 100644 index 76af456..0000000 --- a/src/defaults/config.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -export const twitter = { - REQUEST_TOKEN_URL: 'https://api.twitter.com/oauth/request_token', - AUTHORIZE_URL: 'https://api.twitter.com/oauth/authenticate', - ACCESS_TOKEN_URL: 'https://api.twitter.com/oauth/access_token', -} - -export const github = { - AUTHORIZE_URL: 'https://github.com/login/oauth/authorize', - ACCESS_TOKEN_URL: 'https://github.com/login/oauth/access_token', - USER_INFO_URL: 'https://api.github.com/user', - USER_EMAIL_URL: 'https://api.github.com/user/emails', -} - -export const google = { - AUTHORIZE_URL: 'https://accounts.google.com/o/oauth2/v2/auth', - ACCESS_TOKEN_URL: 'https://oauth2.googleapis.com/token', - USER_INFO_URL: 'https://www.googleapis.com/oauth2/v3/userinfo', -} - -export const gitlab = { - AUTHORIZE_URL: 'https://gitlab.com/oauth/authorize', - ACCESS_TOKEN_URL: 'https://gitlab.com/oauth/token', -} - -export const linkedin = { - AUTHORIZE_URL: 'https://www.linkedin.com/oauth/v2/authorization', - ACCESS_TOKEN_URL: 'https://www.linkedin.com/oauth/v2/accessToken', -} - -export const patreon = { - AUTHORIZE_URL: 'https://www.patreon.com/oauth2/authorize', - ACCESS_TOKEN_URL: 'https://www.patreon.com/api/oauth2/token', -} - -export const discord = { - AUTHORIZE_URL: 'https://discord.com/api/oauth2/authorize', - ACCESS_TOKEN_URL: 'https://discord.com/api/oauth2/token', - USER_INFO_URL: 'https://discord.com/api/users/@me', -} - -export const microsoft = { - AUTHORIZE_URL: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize', - ACCESS_TOKEN_URL: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token', -} - -export const bitbucket = { - AUTHORIZE_URL: 'https://bitbucket.org/site/oauth2/authorize', - ACCESS_TOKEN_URL: 'https://bitbucket.org/site/oauth2/access_token', -} - -export const facebook = { - AUTHORIZE_URL: 'https://www.facebook.com/v10.0/dialog/oauth', - ACCESS_TOKEN_URL: 'https://graph.facebook.com/v10.0/dialog/oauth/access_token', -} - -export const spotify = { - AUTHORIZE_URL: 'https://accounts.spotify.com/authorize', - ACCESS_TOKEN_URL: 'https://accounts.spotify.com/api/token', - USER_INFO_URL: 'https://api.spotify.com/v1/me', -} diff --git a/src/define_config.ts b/src/define_config.ts index 3a45ff4..3810c84 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -7,10 +7,42 @@ * file that was distributed with this source code. */ +import { configProvider } from '@adonisjs/core' import type { HttpContext } from '@adonisjs/core/http' +import type { ConfigProvider } from '@adonisjs/core/types' -import type { AllyDriversList } from './types.js' -import allyDriversCollection from './drivers_collection.js' +import type { GoogleDriver } from './drivers/google.js' +import type { GithubDriver } from './drivers/github.js' +import type { SpotifyDriver } from './drivers/spotify.js' +import type { TwitterDriver } from './drivers/twitter.js' +import type { DiscordDriver } from './drivers/discord.js' +import type { FacebookDriver } from './drivers/facebook.js' +import type { LinkedInDriver } from './drivers/linked_in.js' +import type { + GoogleDriverConfig, + GithubDriverConfig, + SpotifyDriverConfig, + DiscordDriverConfig, + TwitterDriverConfig, + LinkedInDriverConfig, + FacebookDriverConfig, + AllyManagerDriverFactory, +} from '@adonisjs/ally/types' + +/** + * Shape of config after it has been resolved from + * the config provider + */ +type ResolvedConfig< + KnownSocialProviders extends Record< + string, + AllyManagerDriverFactory | ConfigProvider + >, +> = { + [K in keyof KnownSocialProviders]: KnownSocialProviders[K] extends ConfigProvider + ? A + : KnownSocialProviders[K] +} /** * Define config for the ally @@ -18,49 +50,78 @@ import allyDriversCollection from './drivers_collection.js' export function defineConfig< KnownSocialProviders extends Record< string, - { - [K in keyof AllyDriversList]: { driver: K } & Parameters[0] - }[keyof AllyDriversList] + AllyManagerDriverFactory | ConfigProvider >, ->( - config: KnownSocialProviders -): { - services: { - [K in keyof KnownSocialProviders]: ( - ctx: HttpContext - ) => ReturnType - } - driversInUse: Set -} { - /** - * Converting user defined config to an object of services - * that can be injected into the AllyManager class - */ - const driversInUse: Set = new Set() - - const services = Object.keys(config).reduce( - (result, provider: keyof KnownSocialProviders) => { - const providerConfig = config[provider] - driversInUse.add(providerConfig.driver) +>(config: KnownSocialProviders): ConfigProvider> { + return configProvider.create(async (app) => { + const serviceNames = Object.keys(config) + const services = {} as Record - result[provider] = (ctx: HttpContext) => { - return allyDriversCollection.create( - providerConfig.driver, - providerConfig, - ctx - ) + for (let serviceName of serviceNames) { + const service = config[serviceName] + if (typeof service === 'function') { + services[serviceName] = service + } else { + services[serviceName] = await service.resolver(app) } - return result - }, - {} as { - [K in keyof KnownSocialProviders]: ( - ctx: HttpContext - ) => ReturnType } - ) - return { - services, - driversInUse, - } + return services as ResolvedConfig + }) +} + +/** + * Helpers to configure social auth services + */ +export const services: { + discord: (config: DiscordDriverConfig) => ConfigProvider<(ctx: HttpContext) => DiscordDriver> + facebook: (config: FacebookDriverConfig) => ConfigProvider<(ctx: HttpContext) => FacebookDriver> + github: (config: GithubDriverConfig) => ConfigProvider<(ctx: HttpContext) => GithubDriver> + google: (config: GoogleDriverConfig) => ConfigProvider<(ctx: HttpContext) => GoogleDriver> + linkedin: (config: LinkedInDriverConfig) => ConfigProvider<(ctx: HttpContext) => LinkedInDriver> + spotify: (config: SpotifyDriverConfig) => ConfigProvider<(ctx: HttpContext) => SpotifyDriver> + twitter: (config: TwitterDriverConfig) => ConfigProvider<(ctx: HttpContext) => TwitterDriver> +} = { + discord(config) { + return configProvider.create(async () => { + const { DiscordDriver } = await import('./drivers/discord.js') + return (ctx) => new DiscordDriver(ctx, config) + }) + }, + facebook(config) { + return configProvider.create(async () => { + const { FacebookDriver } = await import('./drivers/facebook.js') + return (ctx) => new FacebookDriver(ctx, config) + }) + }, + github(config) { + return configProvider.create(async () => { + const { GithubDriver } = await import('./drivers/github.js') + return (ctx) => new GithubDriver(ctx, config) + }) + }, + google(config) { + return configProvider.create(async () => { + const { GoogleDriver } = await import('./drivers/google.js') + return (ctx) => new GoogleDriver(ctx, config) + }) + }, + linkedin(config) { + return configProvider.create(async () => { + const { LinkedInDriver } = await import('./drivers/linked_in.js') + return (ctx) => new LinkedInDriver(ctx, config) + }) + }, + spotify(config) { + return configProvider.create(async () => { + const { SpotifyDriver } = await import('./drivers/spotify.js') + return (ctx) => new SpotifyDriver(ctx, config) + }) + }, + twitter(config) { + return configProvider.create(async () => { + const { TwitterDriver } = await import('./drivers/twitter.js') + return (ctx) => new TwitterDriver(ctx, config) + }) + }, } diff --git a/src/drivers/discord.ts b/src/drivers/discord.ts index 4e88594..2866d5b 100644 --- a/src/drivers/discord.ts +++ b/src/drivers/discord.ts @@ -8,12 +8,13 @@ */ import type { HttpContext } from '@adonisjs/core/http' -import { +import type { HttpClient } from '@poppinss/oauth-client' + +import type { DiscordScopes, DiscordToken, ApiRequestContract, DiscordDriverConfig, - DiscordDriverContract, RedirectRequestContract, } from '../types.js' import { Oauth2Driver } from '../abstract_drivers/oauth2.js' @@ -21,10 +22,7 @@ import { Oauth2Driver } from '../abstract_drivers/oauth2.js' /** * Discord driver to login user via Discord */ -export class DiscordDriver - extends Oauth2Driver - implements DiscordDriverContract -{ +export class DiscordDriver extends Oauth2Driver { protected accessTokenUrl = 'https://discord.com/api/oauth2/token' protected authorizeUrl = 'https://discord.com/api/oauth2/authorize' protected userInfoUrl = 'https://discord.com/api/users/@me' @@ -117,7 +115,7 @@ export class DiscordDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `Bearer ${token}`) request.header('Accept', 'application/json') diff --git a/src/drivers/facebook.ts b/src/drivers/facebook.ts index 1ba6851..bae4224 100644 --- a/src/drivers/facebook.ts +++ b/src/drivers/facebook.ts @@ -7,15 +7,15 @@ * file that was distributed with this source code. */ +import type { HttpClient } from '@poppinss/oauth-client' import type { HttpContext } from '@adonisjs/core/http' -import { +import type { FacebookToken, FacebookScopes, LiteralStringUnion, ApiRequestContract, FacebookDriverConfig, FacebookProfileFields, - FacebookDriverContract, RedirectRequestContract, } from '../types.js' import { Oauth2Driver } from '../abstract_drivers/oauth2.js' @@ -23,10 +23,7 @@ import { Oauth2Driver } from '../abstract_drivers/oauth2.js' /** * Facebook driver to login user via Facebook */ -export class FacebookDriver - extends Oauth2Driver - implements FacebookDriverContract -{ +export class FacebookDriver extends Oauth2Driver { protected accessTokenUrl = 'https://graph.facebook.com/v10.0/oauth/access_token' protected authorizeUrl = 'https://www.facebook.com/v10.0/dialog/oauth' protected userInfoUrl = 'https://graph.facebook.com/v10.0/me' @@ -114,7 +111,7 @@ export class FacebookDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `Bearer ${token}`) request.header('Accept', 'application/json') diff --git a/src/drivers/github.ts b/src/drivers/github.ts index f6854ac..4e5129b 100644 --- a/src/drivers/github.ts +++ b/src/drivers/github.ts @@ -8,13 +8,13 @@ */ import type { HttpContext } from '@adonisjs/core/http' -import { +import type { HttpClient } from '@poppinss/oauth-client' +import type { GithubToken, GithubScopes, AllyUserContract, GithubDriverConfig, ApiRequestContract, - GithubDriverContract, RedirectRequestContract, } from '../types.js' import { Oauth2Driver } from '../abstract_drivers/oauth2.js' @@ -22,10 +22,7 @@ import { Oauth2Driver } from '../abstract_drivers/oauth2.js' /** * Github driver to login user via Github */ -export class GithubDriver - extends Oauth2Driver - implements GithubDriverContract -{ +export class GithubDriver extends Oauth2Driver { protected accessTokenUrl = 'https://github.com/login/oauth/access_token' protected authorizeUrl = 'https://github.com/login/oauth/authorize' protected userInfoUrl = 'https://api.github.com/user' @@ -119,7 +116,7 @@ export class GithubDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `token ${token}`) request.header('Accept', 'application/json') diff --git a/src/drivers/google.ts b/src/drivers/google.ts index 1283547..608d8ca 100644 --- a/src/drivers/google.ts +++ b/src/drivers/google.ts @@ -8,12 +8,12 @@ */ import type { HttpContext } from '@adonisjs/core/http' -import { +import type { HttpClient } from '@poppinss/oauth-client' +import type { GoogleToken, GoogleScopes, GoogleDriverConfig, ApiRequestContract, - GoogleDriverContract, RedirectRequestContract, } from '../types.js' import { Oauth2Driver } from '../abstract_drivers/oauth2.js' @@ -59,10 +59,7 @@ const SCOPE_PREFIXES = { /** * Google driver to login user via Google */ -export class GoogleDriver - extends Oauth2Driver - implements GoogleDriverContract -{ +export class GoogleDriver extends Oauth2Driver { protected accessTokenUrl = 'https://oauth2.googleapis.com/token' protected authorizeUrl = 'https://accounts.google.com/o/oauth2/v2/auth' protected userInfoUrl = 'https://www.googleapis.com/oauth2/v3/userinfo' @@ -146,7 +143,7 @@ export class GoogleDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `Bearer ${token}`) request.header('Accept', 'application/json') diff --git a/src/drivers/linked_in.ts b/src/drivers/linked_in.ts index c32a51b..59be455 100644 --- a/src/drivers/linked_in.ts +++ b/src/drivers/linked_in.ts @@ -9,12 +9,12 @@ import { Exception } from '@poppinss/utils' import type { HttpContext } from '@adonisjs/core/http' -import { +import type { HttpClient } from '@poppinss/oauth-client' +import type { LinkedInToken, LinkedInScopes, ApiRequestContract, LinkedInDriverConfig, - LinkedInDriverContract, RedirectRequestContract, } from '../types.js' import { Oauth2Driver } from '../abstract_drivers/oauth2.js' @@ -22,10 +22,7 @@ import { Oauth2Driver } from '../abstract_drivers/oauth2.js' /** * LinkedIn driver to login user via LinkedIn */ -export class LinkedInDriver - extends Oauth2Driver - implements LinkedInDriverContract -{ +export class LinkedInDriver extends Oauth2Driver { protected accessTokenUrl = 'https://www.linkedin.com/oauth/v2/accessToken' protected authorizeUrl = 'https://www.linkedin.com/oauth/v2/authorization' protected userInfoUrl = 'https://api.linkedin.com/v2/me' @@ -92,7 +89,7 @@ export class LinkedInDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `Bearer ${token}`) request.header('Accept', 'application/json') diff --git a/src/drivers/spotify.ts b/src/drivers/spotify.ts index b2c4841..e1aec95 100644 --- a/src/drivers/spotify.ts +++ b/src/drivers/spotify.ts @@ -8,12 +8,12 @@ */ import type { HttpContext } from '@adonisjs/core/http' -import { +import type { HttpClient } from '@poppinss/oauth-client' +import type { SpotifyScopes, SpotifyToken, ApiRequestContract, SpotifyDriverConfig, - SpotifyDriverContract, RedirectRequestContract, } from '../types.js' import { Oauth2Driver } from '../abstract_drivers/oauth2.js' @@ -21,10 +21,7 @@ import { Oauth2Driver } from '../abstract_drivers/oauth2.js' /** * Spotify driver to login user via Spotify */ -export class SpotifyDriver - extends Oauth2Driver - implements SpotifyDriverContract -{ +export class SpotifyDriver extends Oauth2Driver { protected accessTokenUrl = 'https://accounts.spotify.com/api/token' protected authorizeUrl = 'https://accounts.spotify.com/authorize' protected userInfoUrl = 'https://api.spotify.com/v1/me' @@ -96,7 +93,7 @@ export class SpotifyDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `Bearer ${token}`) request.header('Accept', 'application/json') diff --git a/src/drivers/twitter.ts b/src/drivers/twitter.ts index b8c03b9..ab83fb3 100644 --- a/src/drivers/twitter.ts +++ b/src/drivers/twitter.ts @@ -13,17 +13,13 @@ import { AllyUserContract, ApiRequestContract, TwitterDriverConfig, - TwitterDriverContract, } from '../types.js' import { Oauth1Driver } from '../abstract_drivers/oauth1.js' /** * Twitter driver to login user via twitter */ -export class TwitterDriver - extends Oauth1Driver - implements TwitterDriverContract -{ +export class TwitterDriver extends Oauth1Driver { protected requestTokenUrl = 'https://api.twitter.com/oauth/request_token' protected authorizeUrl = 'https://api.twitter.com/oauth/authenticate' protected accessTokenUrl = 'https://api.twitter.com/oauth/access_token' diff --git a/src/drivers_collection.ts b/src/drivers_collection.ts deleted file mode 100644 index c7aef0f..0000000 --- a/src/drivers_collection.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { RuntimeException } from '@poppinss/utils' -import type { HttpContext } from '@adonisjs/core/http' -import type { AllyDriversList } from './types.js' -import debug from './debug.js' - -/** - * A global collection of ally drivers. - */ -class AllyDriversCollection { - async registerBundledDrivers(drivers: Set) { - debug('drivers in use %O', drivers) - - if (drivers.has('discord') && !this.list['discord']) { - const { DiscordDriver } = await import('../src/drivers/discord.js') - this.extend('discord', (config, ctx) => new DiscordDriver(ctx, config)) - } - - if (drivers.has('facebook') && !this.list['facebook']) { - const { FacebookDriver } = await import('../src/drivers/facebook.js') - this.extend('facebook', (config, ctx) => new FacebookDriver(ctx, config)) - } - - if (drivers.has('github') && !this.list['github']) { - const { GithubDriver } = await import('../src/drivers/github.js') - this.extend('github', (config, ctx) => new GithubDriver(ctx, config)) - } - - if (drivers.has('google') && !this.list['google']) { - const { GoogleDriver } = await import('../src/drivers/google.js') - this.extend('google', (config, ctx) => new GoogleDriver(ctx, config)) - } - - if (drivers.has('linkedin') && !this.list['linkedin']) { - const { LinkedInDriver } = await import('../src/drivers/linked_in.js') - this.extend('linkedin', (config, ctx) => new LinkedInDriver(ctx, config)) - } - - if (drivers.has('spotify') && !this.list['spotify']) { - const { SpotifyDriver } = await import('../src/drivers/spotify.js') - this.extend('spotify', (config, ctx) => new SpotifyDriver(ctx, config)) - } - - if (drivers.has('twitter') && !this.list['twitter']) { - const { TwitterDriver } = await import('../src/drivers/twitter.js') - this.extend('twitter', (config, ctx) => new TwitterDriver(ctx, config)) - } - } - - /** - * List of registered drivers - */ - list: Partial = {} - - /** - * Extend drivers collection and add a custom - * driver to it. - */ - extend( - driverName: Name, - factoryCallback: AllyDriversList[Name] - ): this { - debug('registering %s driver', driverName) - this.list[driverName] = factoryCallback - return this - } - - /** - * Creates the driver instance with config - */ - create( - name: Name, - config: Parameters[0], - ctx: HttpContext - ): ReturnType { - const driverFactory = this.list[name] - if (!driverFactory) { - throw new RuntimeException( - `Unknown ally driver "${String(name)}". Make sure the driver is registered` - ) - } - - debug('creating instance of %s driver', name) - return driverFactory(config as any, ctx) as ReturnType - } -} - -const allyDriversCollection = new AllyDriversCollection() -export default allyDriversCollection diff --git a/src/types.ts b/src/types.ts index 180bbbc..d23ec54 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,7 @@ */ import { HttpContext } from '@adonisjs/core/http' +import { ConfigProvider } from '@adonisjs/core/types' import { Oauth2AccessToken, Oauth1RequestToken, @@ -17,6 +18,7 @@ import { ApiRequestContract, RedirectRequestContract as ClientRequestContract, } from '@poppinss/oauth-client/types' +import { AllyManager } from './ally_manager.js' export type { Oauth2AccessToken } export type { Oauth1AccessToken } @@ -157,6 +159,12 @@ export interface AllyDriverContract< ): Promise> } +/** + * The manager driver factory method is called by the AllyManager to create + * an instance of a driver during an HTTP request + */ +export type AllyManagerDriverFactory = (ctx: HttpContext) => AllyDriverContract + /** * ---------------------------------------- * Discord driver @@ -216,10 +224,6 @@ export type DiscordDriverConfig = Oauth2ClientConfig & { permissions?: number } -export interface DiscordDriverContract extends AllyDriverContract { - version: 'oauth2' -} - /** * ---------------------------------------- * Github driver @@ -284,10 +288,6 @@ export type GithubDriverConfig = Oauth2ClientConfig & { userEmailUrl?: string } -export interface GithubDriverContract extends AllyDriverContract { - version: 'oauth2' -} - /** * ---------------------------------------- * Twitter driver @@ -311,10 +311,6 @@ export type TwitterDriverConfig = Oauth1ClientConfig & { userInfoUrl?: string } -export interface TwitterDriverContract extends AllyDriverContract { - version: 'oauth1' -} - /** * ---------------------------------------- * Google driver @@ -392,10 +388,6 @@ export type GoogleDriverConfig = Oauth2ClientConfig & { display?: 'page' | 'popup' | 'touch' | 'wrap' } -export interface GoogleDriverContract extends AllyDriverContract { - version: 'oauth2' -} - /** * ---------------------------------------- * LinkedIn driver @@ -441,10 +433,6 @@ export type LinkedInDriverConfig = Oauth2ClientConfig & { scopes?: LiteralStringUnion[] } -export interface LinkedInDriverContract extends AllyDriverContract { - version: 'oauth2' -} - /** * ---------------------------------------- * Facebook driver @@ -540,10 +528,6 @@ export type FacebookDriverConfig = Oauth2ClientConfig & { authType?: string } -export interface FacebookDriverContract extends AllyDriverContract { - version: 'oauth2' -} - /** * ---------------------------------------- * Spotify driver @@ -593,39 +577,23 @@ export type SpotifyDriverConfig = Oauth2ClientConfig & { scopes?: LiteralStringUnion[] showDialog?: boolean } - -export interface SpotifyDriverContract extends AllyDriverContract { - version: 'oauth2' -} - /** * END OF DRIVERS */ -/** - * List of known ally drivers. The list can be extended using - * declaration merging - */ -export interface AllyDriversList { - discord: (config: DiscordDriverConfig, ctx: HttpContext) => DiscordDriverContract - facebook: (config: FacebookDriverConfig, ctx: HttpContext) => FacebookDriverContract - github: (config: GithubDriverConfig, ctx: HttpContext) => GithubDriverContract - google: (config: GoogleDriverConfig, ctx: HttpContext) => GoogleDriverContract - linkedin: (config: LinkedInDriverConfig, ctx: HttpContext) => LinkedInDriverContract - spotify: (config: SpotifyDriverConfig, ctx: HttpContext) => SpotifyDriverContract - twitter: (config: TwitterDriverConfig, ctx: HttpContext) => TwitterDriverContract -} - -/** - * The manager driver factory method is called by the AllyManager to create - * an instance of a driver during an HTTP request - */ -export type AllyManagerDriverFactory = (ctx: HttpContext) => AllyDriverContract - /** * Social providers are inferred inside the user application * from the config file */ export interface SocialProviders {} -export type InferSocialProviders }> = - T['services'] +export type InferSocialProviders< + T extends ConfigProvider>, +> = Awaited> + +/** + * Ally service is shared with the HTTP context + */ +export interface AllyService + extends AllyManager< + SocialProviders extends Record ? SocialProviders : never + > {} diff --git a/stubs/config.stub b/stubs/config.stub index b49cb87..d5ebfdd 100644 --- a/stubs/config.stub +++ b/stubs/config.stub @@ -2,16 +2,15 @@ exports({ to: app.configPath('ally.ts') }) }}} import env from '#start/env' -import { defineConfig } from '@adonisjs/ally' +import { defineConfig, services } from '@adonisjs/ally' const allyConfig = defineConfig({ {{#each providers as provider}} - {{provider.provider}}: { - driver: '{{provider.provider}}', + {{provider.provider}}: services.{{provider.provider}}({ clientId: env.get('{{provider.envPrefix}}_CLIENT_ID'), clientSecret: env.get('{{provider.envPrefix}}_CLIENT_SECRET'), callbackUrl: '', - }, + }), {{/each}} }) diff --git a/tests/ally_manager.spec.ts b/tests/ally_manager.spec.ts index ac52f0d..2e95920 100644 --- a/tests/ally_manager.spec.ts +++ b/tests/ally_manager.spec.ts @@ -12,15 +12,8 @@ import { HttpContextFactory } from '@adonisjs/core/factories/http' import { AllyManager } from '../src/ally_manager.js' import { GithubDriver } from '../src/drivers/github.js' -import allyDriversCollection from '../src/drivers_collection.js' - -test.group('Ally manager', (group) => { - group.each.setup(() => { - return () => { - allyDriversCollection.list = {} - } - }) +test.group('Ally manager', () => { test('create an instance of a driver', ({ assert, expectTypeOf }) => { const ctx = new HttpContextFactory().create() diff --git a/tests/ally_provider.spec.ts b/tests/ally_provider.spec.ts index 4c11bf2..a0fe299 100644 --- a/tests/ally_provider.spec.ts +++ b/tests/ally_provider.spec.ts @@ -12,69 +12,19 @@ import { IgnitorFactory } from '@adonisjs/core/factories' import { HttpContextFactory } from '@adonisjs/core/factories/http' import { AllyManager } from '../src/ally_manager.js' -import { defineConfig } from '../src/define_config.js' -import allyDriversCollection from '../src/drivers_collection.js' +import { defineConfig, services } from '../src/define_config.js' const BASE_URL = new URL('./tmp/', import.meta.url) const IMPORTER = (filePath: string) => { - if (filePath.startsWith('./') || filePath.startsWith('../')) { - return import(new URL(filePath, BASE_URL).href) - } return import(filePath) } -test.group('Ally provider', (group) => { - group.each.setup(() => { - return () => { - allyDriversCollection.list = {} - } - }) - - test('register drivers in use', async ({ assert }) => { - const ignitor = new IgnitorFactory() - .merge({ - rcFileContents: { - providers: ['../../providers/ally_provider.js'], - }, - }) - .withCoreConfig() - .withCoreProviders() - .merge({ - config: { - ally: defineConfig({ - github: { - driver: 'github', - clientId: '', - clientSecret: '', - callbackUrl: '', - }, - }), - }, - }) - .create(BASE_URL, { - importer: IMPORTER, - }) - - const app = ignitor.createApp('web') - await app.init() - await app.boot() - - assert.property(allyDriversCollection.list, 'github') - assert.notAllProperties(allyDriversCollection.list, [ - 'facebook', - 'linkedin', - 'google', - 'spotify', - 'twitter', - 'discord', - ]) - }) - - test('add ally to HttpContext', async ({ assert }) => { +test.group('Ally provider', () => { + test('define HttpContext.ally property', async ({ assert }) => { const ignitor = new IgnitorFactory() .merge({ rcFileContents: { - providers: ['../../providers/ally_provider.js'], + providers: [() => import('../providers/ally_provider.js')], }, }) .withCoreConfig() @@ -82,12 +32,11 @@ test.group('Ally provider', (group) => { .merge({ config: { ally: defineConfig({ - github: { - driver: 'github', + github: services.github({ clientId: '', clientSecret: '', callbackUrl: '', - }, + }), }), }, }) diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index 88dd5ed..9de185f 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -56,21 +56,19 @@ test.group('Configure', (group) => { await assert.fileContains('config/ally.ts', `declare module '@adonisjs/ally/types' {`) await assert.fileContains( 'config/ally.ts', - `github: { - driver: 'github', + `github: services.github({ clientId: env.get('GITHUB_CLIENT_ID'), clientSecret: env.get('GITHUB_CLIENT_SECRET'), callbackUrl: '', - },` + }),` ) await assert.fileContains( 'config/ally.ts', - `linkedin: { - driver: 'linkedin', + `linkedin: services.linkedin({ clientId: env.get('LINKEDIN_CLIENT_ID'), clientSecret: env.get('LINKEDIN_CLIENT_SECRET'), callbackUrl: '', - },` + }),` ) await assert.fileContains('.env', 'GITHUB_CLIENT_ID') await assert.fileContains('.env', 'GITHUB_CLIENT_SECRET') @@ -81,5 +79,5 @@ test.group('Configure', (group) => { await assert.fileContains('start/env.ts', 'GITHUB_CLIENT_SECRET: Env.schema.string()') await assert.fileContains('start/env.ts', 'LINKEDIN_CLIENT_ID: Env.schema.string()') await assert.fileContains('start/env.ts', 'LINKEDIN_CLIENT_SECRET: Env.schema.string()') - }).timeout(6000) + }).timeout(1000 * 60) }) diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts index 791bd54..3862c12 100644 --- a/tests/define_config.spec.ts +++ b/tests/define_config.spec.ts @@ -8,39 +8,174 @@ */ import { test } from '@japa/runner' +import { AppFactory } from '@adonisjs/core/factories/app' +import { ApplicationService } from '@adonisjs/core/types' import { HttpContextFactory } from '@adonisjs/core/factories/http' -import { AllyManager, defineConfig } from '../index.js' +import { AllyManager } from '../src/ally_manager.js' +import { GoogleDriver } from '../src/drivers/google.js' import { GithubDriver } from '../src/drivers/github.js' -import type { GithubDriverContract } from '../src/types.js' -import allyDriversCollection from '../src/drivers_collection.js' +import { services, defineConfig } from '../src/define_config.js' +import { DiscordDriver } from '../src/drivers/discord.js' +import { FacebookDriver } from '../src/drivers/facebook.js' +import { LinkedInDriver } from '../src/drivers/linked_in.js' +import { SpotifyDriver } from '../src/drivers/spotify.js' +import { TwitterDriver } from '../src/drivers/twitter.js' -test.group('Define config', (group) => { - group.each.setup(() => { - return () => { - allyDriversCollection.list = {} - } - }) +const BASE_URL = new URL('./', import.meta.url) +const app = new AppFactory().create(BASE_URL, () => {}) as ApplicationService - test('define manager config from user defined config', async ({ assert, expectTypeOf }) => { - const managerConfig = defineConfig({ - github: { - driver: 'github', +test.group('Define config', () => { + test('transform user defined config', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + github: services.github({ clientId: '', clientSecret: '', callbackUrl: '', scopes: ['admin:org'], - }, - }) + }), + }).resolver(app) const ctx = new HttpContextFactory().create() - const ally = new AllyManager(managerConfig.services, ctx) + const ally = new AllyManager(managerConfig, ctx) - await allyDriversCollection.registerBundledDrivers(managerConfig.driversInUse) + assert.instanceOf(ally.use('github'), GithubDriver) + assert.strictEqual(ally.use('github'), ally.use('github')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['github']>() + expectTypeOf(ally.use('github')).toMatchTypeOf() + }) +}) + +test.group('Config services', () => { + test('configure github driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + github: services.github({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) assert.instanceOf(ally.use('github'), GithubDriver) assert.strictEqual(ally.use('github'), ally.use('github')) expectTypeOf(ally.use).parameters.toEqualTypeOf<['github']>() - expectTypeOf(ally.use('github')).toMatchTypeOf() + expectTypeOf(ally.use('github')).toMatchTypeOf() + }) + + test('configure google driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + google: services.google({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('google'), GoogleDriver) + assert.strictEqual(ally.use('google'), ally.use('google')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['google']>() + expectTypeOf(ally.use('google')).toMatchTypeOf() + }) + + test('configure discord driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + discord: services.discord({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('discord'), DiscordDriver) + assert.strictEqual(ally.use('discord'), ally.use('discord')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['discord']>() + expectTypeOf(ally.use('discord')).toMatchTypeOf() + }) + + test('configure facebook driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + facebook: services.facebook({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('facebook'), FacebookDriver) + assert.strictEqual(ally.use('facebook'), ally.use('facebook')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['facebook']>() + expectTypeOf(ally.use('facebook')).toMatchTypeOf() + }) + + test('configure linkedin driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + linkedin: services.linkedin({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('linkedin'), LinkedInDriver) + assert.strictEqual(ally.use('linkedin'), ally.use('linkedin')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['linkedin']>() + expectTypeOf(ally.use('linkedin')).toMatchTypeOf() + }) + + test('configure spotify driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + spotify: services.spotify({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('spotify'), SpotifyDriver) + assert.strictEqual(ally.use('spotify'), ally.use('spotify')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['spotify']>() + expectTypeOf(ally.use('spotify')).toMatchTypeOf() + }) + + test('configure twitter driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + twitter: services.twitter({ + clientId: '', + clientSecret: '', + callbackUrl: '', + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('twitter'), TwitterDriver) + assert.strictEqual(ally.use('twitter'), ally.use('twitter')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['twitter']>() + expectTypeOf(ally.use('twitter')).toMatchTypeOf() }) }) diff --git a/tests/drivers_collection.spec.ts b/tests/drivers_collection.spec.ts deleted file mode 100644 index f6434ca..0000000 --- a/tests/drivers_collection.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { HttpContextFactory } from '@adonisjs/core/factories/http' - -import { GithubDriver } from '../src/drivers/github.js' -import { GoogleDriver } from '../src/drivers/google.js' -import { TwitterDriver } from '../src/drivers/twitter.js' -import { DiscordDriver } from '../src/drivers/discord.js' -import { SpotifyDriver } from '../src/drivers/spotify.js' -import { FacebookDriver } from '../src/drivers/facebook.js' -import { LinkedInDriver } from '../src/drivers/linked_in.js' -import allyDriversCollection from '../src/drivers_collection.js' -import type { - DiscordDriverContract, - FacebookDriverContract, - GithubDriverContract, - GoogleDriverContract, - LinkedInDriverContract, - SpotifyDriverContract, - TwitterDriverContract, -} from '../src/types.js' - -test.group('Drivers Collection', (group) => { - group.each.setup(() => { - return () => { - allyDriversCollection.list = {} - } - }) - - test('create an instance of a known driver', async ({ assert, expectTypeOf }) => { - const ctx = new HttpContextFactory().create() - await allyDriversCollection.registerBundledDrivers( - new Set(['github', 'google', 'discord', 'facebook', 'linkedin', 'spotify', 'twitter']) - ) - - const discord = allyDriversCollection.create( - 'discord', - { clientId: '', clientSecret: '', callbackUrl: '' }, - ctx - ) - assert.instanceOf(discord, DiscordDriver) - expectTypeOf(discord).toEqualTypeOf() - - const github = allyDriversCollection.create( - 'github', - { clientId: '', clientSecret: '', callbackUrl: '' }, - ctx - ) - assert.instanceOf(github, GithubDriver) - expectTypeOf(github).toEqualTypeOf() - - const google = allyDriversCollection.create( - 'google', - { clientId: '', clientSecret: '', callbackUrl: '' }, - ctx - ) - assert.instanceOf(google, GoogleDriver) - expectTypeOf(google).toEqualTypeOf() - - const facebook = allyDriversCollection.create( - 'facebook', - { clientId: '', clientSecret: '', callbackUrl: '' }, - ctx - ) - assert.instanceOf(facebook, FacebookDriver) - expectTypeOf(facebook).toEqualTypeOf() - - const linkedin = allyDriversCollection.create( - 'linkedin', - { clientId: '', clientSecret: '', callbackUrl: '' }, - ctx - ) - assert.instanceOf(linkedin, LinkedInDriver) - expectTypeOf(linkedin).toEqualTypeOf() - - const spotify = allyDriversCollection.create( - 'spotify', - { clientId: '', clientSecret: '', callbackUrl: '' }, - ctx - ) - assert.instanceOf(spotify, SpotifyDriver) - expectTypeOf(spotify).toEqualTypeOf() - - const twitter = allyDriversCollection.create( - 'twitter', - { clientId: '', clientSecret: '', callbackUrl: '' }, - ctx - ) - assert.instanceOf(twitter, TwitterDriver) - expectTypeOf(twitter).toEqualTypeOf() - }) - - test('extend drivers collection', ({ assert }) => { - class Foo {} - - allyDriversCollection.extend('foo' as any, () => { - return new Foo() - }) - - const ctx = new HttpContextFactory().create() - assert.instanceOf(allyDriversCollection.create('foo' as any, {}, ctx), Foo) - }) - - test('throw error when trying to create an unknown driver', () => { - const ctx = new HttpContextFactory().create() - allyDriversCollection.create('bar' as any, {}, ctx) - }).throws('Unknown ally driver "bar". Make sure the driver is registered') -})