diff --git a/assets/scripts/GitAgent.ts b/assets/scripts/GitAgent.ts index 0d4e8f16..ff0319d0 100644 --- a/assets/scripts/GitAgent.ts +++ b/assets/scripts/GitAgent.ts @@ -13,16 +13,15 @@ import FS from '@isomorphic-git/lightning-fs' import git from 'isomorphic-git' import http from 'isomorphic-git/http/web' -const DEFAULT_CORS_PROXY_URL = 'https://cors.isomorphic-git.org' - -import type { CommitObject, GitAuth } from 'isomorphic-git' +import type { CommitObject } from 'isomorphic-git' import type { ResolutionOption } from './store.ts' +import type { OAuthServiceAPI } from './types/git.ts' export default class GitAgent { #fs #remoteURL #repoId - #corsProxyURL + #corsProxyURL: string | undefined = undefined #onAuth #onMergeConflict @@ -34,14 +33,14 @@ export default class GitAgent { constructor({ repoId, remoteURL, - corsProxyURL = DEFAULT_CORS_PROXY_URL, - auth, + corsProxyURL, + gitServiceProvider, onMergeConflict, }: { repoId: string remoteURL: string corsProxyURL?: string - auth: GitAuth + gitServiceProvider: OAuthServiceAPI onMergeConflict?: | ((resolutionOptions: ResolutionOption[]) => void) | undefined @@ -50,6 +49,8 @@ export default class GitAgent { this.#repoId = repoId this.#remoteURL = remoteURL + const auth = gitServiceProvider.getOauthUsernameAndPassword() + this.#onAuth = () => auth this.#onMergeConflict = onMergeConflict this.#corsProxyURL = corsProxyURL @@ -83,6 +84,7 @@ export default class GitAgent { singleBranch: true, corsProxy: this.#corsProxyURL, depth: 5, + onAuth: this.#onAuth, }) } @@ -191,6 +193,7 @@ export default class GitAgent { singleBranch: false, // we want all the branches dir: this.#repoDirectory, corsProxy: this.#corsProxyURL, + onAuth: this.#onAuth, }) } diff --git a/assets/scripts/actions/current-repository.ts b/assets/scripts/actions/current-repository.ts index 1075b45e..52324ec4 100644 --- a/assets/scripts/actions/current-repository.ts +++ b/assets/scripts/actions/current-repository.ts @@ -2,10 +2,7 @@ import page from 'page' import yaml from 'js-yaml' import store, { type PartialStore } from './../store.ts' -import ScribouilliGitRepo, { - makeRepoId, - makePublicRepositoryURL, -} from './../scribouilliGitRepo.ts' +import ScribouilliGitRepo from './../scribouilliGitRepo.ts' import GitAgent from '../GitAgent.ts' import { handleErrors, logMessage } from './../utils.ts' import { fetchAuthenticatedUserLogin } from './current-user.ts' @@ -17,7 +14,7 @@ import { file } from './file.ts' import { getPagesList } from './page.ts' import { getArticlesList } from './article.ts' import { getOAuthServiceAPI } from '../oauth-services-api/index.ts' -import { CUSTOM_CSS_PATH } from '../config.ts' +import { CUSTOM_CSS_PATH, PROVIDERS_MAP } from '../config.ts' import type { BuildStatus } from '../types/git.ts' export const getCurrentRepoPages = () => { @@ -65,26 +62,30 @@ export const setCurrentRepositoryFromQuerystring = async ( } const origin = oAuthProvider.origin - const repoId = makeRepoId(owner, repoName) + const provider = PROVIDERS_MAP.get(oAuthProvider.id) + if (!provider) { + throw new TypeError(`Unkown provider ${oAuthProvider.id}`) + } + const oAuthServiceAPI = getOAuthServiceAPI() const scribouilliGitRepo = new ScribouilliGitRepo({ owner, repoName, - repoId, + repoType: provider.type, origin: origin, - publicRepositoryURL: makePublicRepositoryURL(owner, repoName, origin), gitServiceProvider: getOAuthServiceAPI(), }) store.mutations.setCurrentRepository(scribouilliGitRepo) const gitAgent = new GitAgent({ - repoId, - remoteURL: `${origin}/${repoId}.git`, + repoId: oAuthServiceAPI.makeRepoId(owner, repoName), + remoteURL: oAuthServiceAPI.makePublicRepositoryURL(owner, repoName), + corsProxyURL: provider.corsProxy, + gitServiceProvider: oAuthServiceAPI, onMergeConflict: resolutionOptions => { store.mutations.setConflict(resolutionOptions) }, - auth: getOAuthServiceAPI().getOauthUsernameAndPassword(), }) store.mutations.setGitAgent(gitAgent) diff --git a/assets/scripts/actions/setup.ts b/assets/scripts/actions/setup.ts index 73726ec2..3876020c 100644 --- a/assets/scripts/actions/setup.ts +++ b/assets/scripts/actions/setup.ts @@ -1,10 +1,7 @@ import page from 'page' import store, { ResolutionOption } from './../store.ts' -import ScribouilliGitRepo, { - makePublicRepositoryURL, - makeRepoId, -} from './../scribouilliGitRepo.ts' +import ScribouilliGitRepo from './../scribouilliGitRepo.ts' import { getOAuthServiceAPI } from './../oauth-services-api/index.ts' import { makeAtelierListPageURL } from './../routes/urls.ts' import { logMessage } from './../utils.ts' @@ -12,6 +9,7 @@ import { setBaseUrlInConfigIfNecessary } from './current-repository.ts' import GitAgent from '../GitAgent.ts' import git from 'isomorphic-git' import type { GitSiteTemplate } from '../types/git.ts' +import { PROVIDERS_MAP } from '../config.ts' const waitRepoReady = ( scribouilliGitRepo: ScribouilliGitRepo, @@ -130,34 +128,36 @@ export const createRepositoryForCurrentAccount = async ( } const origin = oAuthProvider.origin + const provider = PROVIDERS_MAP.get(oAuthProvider.id) + if (!provider) { + throw new TypeError(`Unkown provider ${oAuthProvider.id}`) + } + const oAuthServiceAPI = getOAuthServiceAPI() const scribouilliGitRepo = new ScribouilliGitRepo({ owner: owner, repoName: escapedRepoName, + repoType: provider.type, origin: origin, - publicRepositoryURL: makePublicRepositoryURL( - owner, - escapedRepoName, - origin, - ), - gitServiceProvider: getOAuthServiceAPI(), + gitServiceProvider: oAuthServiceAPI, }) store.mutations.setCurrentRepository(scribouilliGitRepo) return ( - getOAuthServiceAPI() + oAuthServiceAPI .createDefaultRepository(scribouilliGitRepo, template) .then(({ remoteURL }) => { const gitAgent = new GitAgent({ - repoId: makeRepoId(owner, escapedRepoName), + repoId: oAuthServiceAPI.makeRepoId(owner, escapedRepoName), remoteURL: remoteURL, + corsProxyURL: provider.corsProxy, onMergeConflict: ( resolutionOptions: ResolutionOption[] | undefined, ) => { store.mutations.setConflict(resolutionOptions) }, - auth: getOAuthServiceAPI().getOauthUsernameAndPassword(), + gitServiceProvider: oAuthServiceAPI, }) store.mutations.setGitAgent(gitAgent) diff --git a/assets/scripts/components/Header.svelte b/assets/scripts/components/Header.svelte index 2eae015a..1b982e9e 100644 --- a/assets/scripts/components/Header.svelte +++ b/assets/scripts/components/Header.svelte @@ -1,6 +1,7 @@ - {#if gitProvider === 'github.com'} -
-
-

Avez-vous un compte GitHub ?

- -
- Oui, je me connecte - Non, je veux créer un compte -
-
-
- {/if} - - {#if gitProvider === 'gitlab.com'} -
-
-

Avez-vous un compte sur gitlab.com ?

- -
- Oui, je me connecte - Non, je veux créer un compte -
-
-
- {/if} - - {#if gitProvider === 'git.scribouilli.org'}
-

Avez-vous un compte sur git.scribouilli.org ?

+ {#if provider.type === 'github' } +

Avez-vous un compte GitHub ?

+ {:else} +

Avez-vous un compte sur { provider.id } ?

+ {/if}
- Oui, je me connecte - Oui, je me connecte + Non, je veux créer un compte
- {/if} - -
diff --git a/assets/scripts/components/screens/CreateNewSite.svelte b/assets/scripts/components/screens/CreateNewSite.svelte index d09a380b..06caff7a 100644 --- a/assets/scripts/components/screens/CreateNewSite.svelte +++ b/assets/scripts/components/screens/CreateNewSite.svelte @@ -2,7 +2,7 @@ import Skeleton from './../Skeleton.svelte'; import SiteCreationLoader from './../loaders/SiteCreationLoader.svelte'; import { createRepositoryForCurrentAccount } from '../../actions/setup.ts'; - import { DEFAULT_TEMPLATE, templates } from '../../config.ts'; + import { DEFAULT_TEMPLATE, TEMPLATES } from '../../config.ts'; import type { GitSiteTemplate } from '../../types/git' let name = $state(""); @@ -24,7 +24,8 @@ // dépôt perso dans une organisation, via l'interface GitHub, pour les // utilisateurices avancé.es createRepositoryForCurrentAccount(name, selectedTemplate) - .catch(() => { + .catch((e) => { + console.warn(`Failed to create repository: ${e}`); loading = false; hasError = true; }); @@ -53,7 +54,7 @@
diff --git a/assets/scripts/components/screens/Login.svelte b/assets/scripts/components/screens/Login.svelte index 24d1fb14..a89fc1b6 100644 --- a/assets/scripts/components/screens/Login.svelte +++ b/assets/scripts/components/screens/Login.svelte @@ -1,35 +1,37 @@ - {#if gitProvider === 'github.com'} + {#if providerType === 'github'}

Super, nous allons vous demander les clefs sur la page suivante.

Je me connecte via GitHub
{/if} - {#if gitProvider === 'gitlab.com'} + {#if providerType === 'gitlab'}

Super, nous allons vous demander les clefs sur la page suivante.

- Je me connecte via gitlab.com + Je me connecte via {providerId}
{/if} - {#if gitProvider === 'git.scribouilli.org'} + {#if providerType === 'scribouilli'}

Super, nous allons vous demander les clefs sur la page suivante.

- Je me connecte via git.scribouilli.org + Je me connecte
{/if}
diff --git a/assets/scripts/components/screens/intern/Editeur.svelte b/assets/scripts/components/screens/intern/Editeur.svelte index a6e2d14d..7dcafa8f 100644 --- a/assets/scripts/components/screens/intern/Editeur.svelte +++ b/assets/scripts/components/screens/intern/Editeur.svelte @@ -7,7 +7,7 @@ import { writeFileAndCommit } from '../../../actions/file' import './../../../../styles/editeur-preview/framalibre.css' import type { EditeurFile, FileContenu } from "../../../types/atelier" - + interface Props { fileP: Promise buildStatus: any @@ -173,14 +173,14 @@ > avec du Markdown - … ou avec du HTML grâce + … ou avec du HTML grâce à nos exemples - d'encart ou de bouton, ou + d'encart ou de bouton, ou
- +

Aperçu

{#await preview} - + {:then preview} {@html preview} - {/await} + {/await}
diff --git a/assets/scripts/config.ts b/assets/scripts/config.ts index 243a0069..80effdf4 100644 --- a/assets/scripts/config.ts +++ b/assets/scripts/config.ts @@ -1,6 +1,9 @@ +import type { ScribouilliBackendProvider } from './types/atelier' import type { GitSiteTemplate } from './types/git' +export const DEFAULT_CORS_PROXY_URL = 'https://cors.isomorphic-git.org' export const OAUTH_PROVIDER_STORAGE_KEY = 'scribouilli_oauth_provider' +export const TOCTOCTOC_ORIGIN = `https://toctoctoc.lechappeebelle.team` export const TOCTOCTOC_ACCESS_TOKEN_URL_PARAMETER = 'access_token' export const TOCTOCTOC_OAUTH_PROVIDER_URL_PARAMETER = 'type' export const TOCTOCTOC_OAUTH_PROVIDER_ORIGIN_PARAMETER = 'origin' @@ -9,7 +12,7 @@ export const gitHubApiBaseUrl = 'https://api.github.com' export const CUSTOM_CSS_PATH = 'assets/css/custom.css' -export const templates: GitSiteTemplate[] = [ +export const TEMPLATES: GitSiteTemplate[] = [ { url: 'https://github.com/Scribouilli/site-template.git', description: 'mon site vitrine ou mon blog', @@ -22,6 +25,96 @@ export const templates: GitSiteTemplate[] = [ }, ] -export const DEFAULT_TEMPLATE = templates[0] +export const DEFAULT_TEMPLATE = TEMPLATES[0] + +export const PROVIDERS: ScribouilliBackendProvider[] = [ + // { + // id: 'localhost', + // type: 'scribouilli', + // origin: 'http://localhost:3000', + // name: 'Scribouilli', + // description: `Scribouilli, vous pouvez utilizer Scribouilli pour héberger directement votre site.`, + // signupEnabled: false, + // }, + { + id: 'gitlab.com', + type: 'gitlab', + origin: 'https://gitlab.com', + clientId: + 'b943c32d1a30f316cf4a72b5e40b05b6e71a1e3df34e2233c51e79838b22f7e8', + name: 'Gitlab', + description: ` + Gitlab.com qui est un hébergeur professionnel.
+ Si vous n'avez pas encore de compte, Gitlab demandera à
vérifier votre identité avec un n° de téléphone ou de carte bleue. + `, + signupEnabled: true, + signupLink: 'https://gitlab.com/users/sign_up', + corsProxy: DEFAULT_CORS_PROXY_URL, + }, + { + id: 'git.scribouilli.org', + type: 'gitlab', + origin: 'https://git.scribouilli.org', + clientId: + '3e8ac6636615d396a8f73e02fa3880e7e2140981b0ca27b0f240a450f69f1c76', + name: 'ScribouGit', + description: ` + ScribouGit, l'hébergement géré par l'équipe de Scribouilli.
+ Si vous n'avez pas encore de compte, nous prendrons le temps de le valider manuellement (cela peut prendre quelques jours). + `, + signupEnabled: false, + signupInstructions: ` +

+ Pour vérifier que vous n'êtes pas un robot, envoyez-nous un mail à + coucou@scribouilli.org en indiquant : +

+ + +

Pour information, la création du compte pourrait prendre quelques jours de notre côté, on vous préviendra par mail quand c'est fait ! +

+ `, + corsProxy: DEFAULT_CORS_PROXY_URL, + }, + { + id: 'github.com', + origin: 'https://github.com', + clientId: '64ecce0b01397c2499a6', + type: 'github', + description: `Microsoft GitHub®, si vous l'utilisez déjà.`, + name: 'GitHub', + signupEnabled: true, + signupLink: 'https://github.com/signup', + signupInstructions: ` +

+ Pour pouvoir publier votre contenu, il faut que Scribouilli se connecte + à un compte GitHub. +

+

La création va se passer sur GitHub.com.

+

Elle comporte 3 étapes :

+
    +
  1. + Rentrez votre mail, mot de passe, et votre nom d'utilisateur·ice +
  2. +
  3. + Ouvrez le mail que GitHub vous a envoyé, et copiez le code pour + confirmer votre compte +
  4. +
  5. + Dès que le code est validé, revenez sur Scribouilli et + cliquez sur "J'ai créé un compte" +
  6. +
+ `, + corsProxy: DEFAULT_CORS_PROXY_URL, + }, +] +export const PROVIDERS_MAP = new Map( + PROVIDERS.map(provider => [provider.id, provider]), +) export const svelteTarget: Element = document.body diff --git a/assets/scripts/oauth-services-api/github.ts b/assets/scripts/oauth-services-api/github.ts index 69222e14..9456cba5 100644 --- a/assets/scripts/oauth-services-api/github.ts +++ b/assets/scripts/oauth-services-api/github.ts @@ -1,6 +1,7 @@ import { gitHubApiBaseUrl } from './../config.ts' import type { GitSiteTemplate, OAuthServiceAPI } from '../types/git.ts' import ScribouilliGitRepo from '../scribouilliGitRepo.ts' +import { defaultMakePublicRepositoryURL, defaultMakeRepoId } from './index.ts' const GITHUB_JSON_ACCEPT_HEADER = 'application/vnd.github+json' @@ -195,4 +196,10 @@ export default class GitHubAPI implements OAuthServiceAPI { return httpResp }) } + + makeRepoId = defaultMakeRepoId + + makePublicRepositoryURL(owner: string, repoName: string): string { + return defaultMakePublicRepositoryURL(owner, repoName, 'https://github.com') + } } diff --git a/assets/scripts/oauth-services-api/gitlab.ts b/assets/scripts/oauth-services-api/gitlab.ts index 01e62473..a04a24c0 100644 --- a/assets/scripts/oauth-services-api/gitlab.ts +++ b/assets/scripts/oauth-services-api/gitlab.ts @@ -5,6 +5,7 @@ import type { GitSiteTemplate, OAuthServiceAPI, } from '../types/git.ts' +import { defaultMakePublicRepositoryURL, defaultMakeRepoId } from './index.ts' export default class GitLabAPI implements OAuthServiceAPI { #gitAgentGetter @@ -236,4 +237,10 @@ export default class GitLabAPI implements OAuthServiceAPI { } return httpResp } + + makeRepoId = defaultMakeRepoId + + makePublicRepositoryURL(owner: string, repoName: string): string { + return defaultMakePublicRepositoryURL(owner, repoName, this.origin) + } } diff --git a/assets/scripts/oauth-services-api/index.ts b/assets/scripts/oauth-services-api/index.ts index c6a31a0d..a109ac61 100644 --- a/assets/scripts/oauth-services-api/index.ts +++ b/assets/scripts/oauth-services-api/index.ts @@ -3,22 +3,33 @@ import store, { type OAuthProvider } from '../store.ts' import GitHubAPI from './github.ts' import GitlabAPI from './gitlab.ts' import type { OAuthServiceAPI } from '../types/git.ts' +import ScribouilliBackend from './scribouilli.ts' +import { PROVIDERS_MAP } from '../config.ts' const makeOAuthServiceAPI = ({ accessToken, origin, + id, }: OAuthProvider): OAuthServiceAPI => { - const hostname = new URL(origin).hostname + let provider = PROVIDERS_MAP.get(id) - if (hostname === 'github.com') return new GitHubAPI(accessToken) - else { - // assuming a gitlab instance + if (!provider) { + throw new TypeError(`Unkown provider ${id}`) + } + + if (provider.type === 'github') { + return new GitHubAPI(accessToken) + } else if (provider.type === 'gitlab') { return new GitlabAPI(accessToken, origin, () => { if (!store.state.gitAgent) { throw new TypeError('store.state.gitAgent is undefined') } return store.state.gitAgent }) + } else if (provider.type === 'scribouilli') { + return new ScribouilliBackend(accessToken, origin) + } else { + throw new Error('unreachable') } } @@ -43,3 +54,21 @@ export const getOAuthServiceAPI = (): OAuthServiceAPI => { return oAuthServiceAPI } + +/** + * @param owner may be an individual Github user or an organisation + */ +export function defaultMakeRepoId(owner: string, repoName: string): string { + return `${owner}/${repoName}` +} + +/** + * @param owner may be an individual Github user or an organisation + */ +export function defaultMakePublicRepositoryURL( + owner: string, + repoName: string, + origin: string, +): string { + return `${origin}/${owner}/${repoName}` +} diff --git a/assets/scripts/oauth-services-api/scribouilli.ts b/assets/scripts/oauth-services-api/scribouilli.ts new file mode 100644 index 00000000..31b42bcf --- /dev/null +++ b/assets/scripts/oauth-services-api/scribouilli.ts @@ -0,0 +1,179 @@ +import ScribouilliGitRepo from '../scribouilliGitRepo.ts' +import type { + BuildStatus, + GithubRepository, + GitSiteTemplate, + OAuthServiceAPI, +} from '../types/git.ts' +import z from 'zod' + +const WEBSITE_LIST_SCHEMA = z.array( + z.object({ + name: z.string(), + is_ready: z.boolean(), + role: z.union([z.literal('editor'), z.literal('owner')]), + }), +) + +export default class ScribouilliBackend implements OAuthServiceAPI { + private accessToken: string | undefined + private origin + private authenticatedUser: + | undefined + | { id: string; login: string; email: string } + + constructor(accessToken: string, origin: string) { + 7 + this.accessToken = accessToken + this.origin = origin + this.authenticatedUser = undefined + } + + get apiBaseUrl() { + return `${this.origin}/api` + } + + async callAPI( + url: string, + requestParams: RequestInit & { headers?: Record } = {}, + ) { + requestParams.headers ??= {} + requestParams.headers['Authorization'] = 'Bearer ' + this.accessToken + + const httpResp = await fetch(`${this.apiBaseUrl}${url}`, requestParams) + if (httpResp.status === 404) { + throw 'NOT_FOUND' + } + if (httpResp.status === 401) { + this.accessToken = undefined + console.debug('this accessToken : ', this.accessToken) + throw 'INVALIDATE_TOKEN' + } + return httpResp + } + + getOauthUsernameAndPassword() { + if (!this.accessToken) { + throw new TypeError('Missing accessToken') + } + + return { username: 'token', password: this.accessToken } + } + + async getAuthenticatedUser() { + if (this.authenticatedUser) { + return this.authenticatedUser + } + + const response = await this.callAPI(`/profile`) + const user = await response.json() + this.authenticatedUser = { + login: user.email, + email: user.email, + id: user.id, + } + return this.authenticatedUser + } + + async getUserEmails() { + const { email } = await this.getAuthenticatedUser() + return [ + { + email, + primary: true, + }, + ] + } + + async createDefaultRepository( + scribouilliGitRepo: ScribouilliGitRepo, + template: GitSiteTemplate, + ): Promise<{ remoteURL: string }> { + const { repoName } = scribouilliGitRepo + await this.callAPI(`/websites`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + name: repoName, + template_url: template.url, + }), + }) + + return { remoteURL: this.makePublicRepositoryURL('', repoName) } + } + + async isRepositoryReady( + scribouilliGitRepo: ScribouilliGitRepo, + ): Promise { + const response = await this.callAPI( + `/websites/${scribouilliGitRepo.repoName}/ready`, + ) + const { is_ready } = await response.json() + return is_ready + } + + async getCurrentUserRepositories(): Promise { + const response = await this.callAPI(`/websites`) + const rawRepos = await response.json() + const repos = z.parse(WEBSITE_LIST_SCHEMA, rawRepos) + const { email } = await this.getAuthenticatedUser() + const githubRepos = repos.map(repo => { + return { + id: repo.name, + name: repo.name, + owner: { + login: email, + }, + } + }) + return githubRepos + } + + async deploy(scribouilliGitRepo: ScribouilliGitRepo): Promise { + await this.callAPI(`/websites/${scribouilliGitRepo.repoName}/deployment`, { + method: 'POST', + }) + } + + async getPagesWebsiteDeploymentStatus( + scribouilliGitRepo: ScribouilliGitRepo, + ): Promise { + const data = await this.callAPI( + `/websites/${scribouilliGitRepo.repoName}/deployment`, + ) + const { status } = await data.json() + return status + } + + async isPagesWebsiteBuilt( + scribouilliGitRepo: ScribouilliGitRepo, + ): Promise { + try { + const response = + await this.getPagesWebsiteDeploymentStatus(scribouilliGitRepo) + return response === 'success' + } catch { + return false + } + } + + async getPublishedWebsiteURL( + scribouilliGitRepo: ScribouilliGitRepo, + ): Promise { + const data = await this.callAPI( + `/websites/${scribouilliGitRepo.repoName}/url`, + ) + const { url } = await data.json() + return url + } + + makeRepoId(_owner: string, repoName: string): string { + return `websites/${repoName}` + } + + makePublicRepositoryURL(_owner: string, repoName: string): string { + return `${this.origin}/websites/${repoName}` + } +} diff --git a/assets/scripts/routes/account.ts b/assets/scripts/routes/account.ts index 4993eaf7..5f32cb36 100644 --- a/assets/scripts/routes/account.ts +++ b/assets/scripts/routes/account.ts @@ -1,20 +1,27 @@ import { Context } from 'page' import Account from '../components/screens/Account.svelte' import { replaceComponent } from '../routeComponentLifeCycle.svelte.ts' +import { PROVIDERS_MAP } from '../config.ts' export default ({ querystring }: Context) => { const params = new URLSearchParams(querystring) - const gitProvider = params.get('provider') + const providerId = params.get('provider') - console.log('gitProvider', gitProvider) + console.log('providerId', providerId) - if (!gitProvider) { + if (!providerId) { throw new TypeError(`Missing 'provider' parameter`) } + const provider = PROVIDERS_MAP.get(providerId) + + if (!provider) { + throw new TypeError(`Unkown provider ${providerId}`) + } + // TODO: vérifier que c'est ok d'avoir des props qui viennent pas du state, // mais du monde extérieur (ici l'URL de la page) replaceComponent(Account, () => { - return { gitProvider } + return { provider } }) } diff --git a/assets/scripts/routes/after-oauth-login.ts b/assets/scripts/routes/after-oauth-login.ts index 82a4297c..4baf0840 100644 --- a/assets/scripts/routes/after-oauth-login.ts +++ b/assets/scripts/routes/after-oauth-login.ts @@ -15,33 +15,34 @@ import { fetchCurrentUserRepositories } from '../actions/current-user.ts' const storeOAuthProviderAccess = () => { const url = new URL(location.href) - console.log( - 'type', - url.searchParams.get(TOCTOCTOC_OAUTH_PROVIDER_URL_PARAMETER), - ) - const accessToken = url.searchParams.get(TOCTOCTOC_ACCESS_TOKEN_URL_PARAMETER) - const providerName = url.searchParams.get( + const providerType = url.searchParams.get( TOCTOCTOC_OAUTH_PROVIDER_URL_PARAMETER, ) - let origin = url.searchParams.get(TOCTOCTOC_OAUTH_PROVIDER_ORIGIN_PARAMETER) + console.log('type', providerType, 'origin', origin) + if (!origin) { - if (providerName === 'github') { + if (providerType === 'github') { origin = 'https://github.com' } else { throw new TypeError('missing origin') } } - if (accessToken && providerName) { + const providerId = new URL(origin).hostname + + if (providerType && accessToken) { const oAuthProvider = { - name: providerName, + type: providerType, accessToken, origin, + id: providerId, } + console.log(oAuthProvider) + store.mutations.setOAuthProvider(oAuthProvider) remember(OAUTH_PROVIDER_STORAGE_KEY, oAuthProvider) @@ -52,7 +53,7 @@ export default () => { storeOAuthProviderAccess() const oAuthProvider = store.state.oAuthProvider - let type = oAuthProvider?.name + let type = oAuthProvider?.type console.log('type', type) console.log('oAuthProvider', oAuthProvider) @@ -64,7 +65,7 @@ export default () => { let currentUserReposP - if (type === 'github' || type === 'gitlab') { + if (type === 'github' || type === 'gitlab' || type == 'scribouilli') { currentUserReposP = fetchCurrentUserRepositories().then(repos => { if (repos.length === 0) { page.redirect('/creer-un-nouveau-site') diff --git a/assets/scripts/routes/create-account.ts b/assets/scripts/routes/create-account.ts index eec3646b..6dc04ade 100644 --- a/assets/scripts/routes/create-account.ts +++ b/assets/scripts/routes/create-account.ts @@ -1,16 +1,23 @@ import { Context } from 'page' import CreateAccount from '../components/screens/CreateAccount.svelte' import { replaceComponent } from '../routeComponentLifeCycle.svelte' +import { PROVIDERS_MAP } from '../config' export default ({ querystring }: Context) => { const params = new URLSearchParams(querystring) - const gitProvider = params.get('provider') + const providerId = params.get('provider') - if (!gitProvider) { + if (!providerId) { throw new TypeError(`Missing 'provider' parameter`) } + const provider = PROVIDERS_MAP.get(providerId) + + if (!provider) { + throw new TypeError(`Unkown provider ${providerId}`) + } + replaceComponent(CreateAccount, () => { - return { gitProvider } + return { provider } }) } diff --git a/assets/scripts/routes/login.ts b/assets/scripts/routes/login.ts index 11c056c4..4d506e7a 100644 --- a/assets/scripts/routes/login.ts +++ b/assets/scripts/routes/login.ts @@ -2,54 +2,39 @@ import { replaceComponent } from '../routeComponentLifeCycle.svelte' import store from '../store' import Login from '../components/screens/Login.svelte' import { Context } from 'page' +import { + TOCTOCTOC_ORIGIN, + TOCTOCTOC_OAUTH_PROVIDER_ORIGIN_PARAMETER, + TOCTOCTOC_OAUTH_PROVIDER_URL_PARAMETER, + PROVIDERS_MAP, +} from '../config' +import { ScribouilliBackendProvider } from '../types/atelier' -const TOCTOCTOC_ORIGIN = `https://toctoctoc.lechappeebelle.team` - -const oAuthAppByProvider = new Map([ - [ - 'github.com', - { - origin: 'https://github.com', - client_id: '64ecce0b01397c2499a6', - }, - ], - [ - 'gitlab.com', - { - origin: 'https://gitlab.com', - client_id: - 'b943c32d1a30f316cf4a72b5e40b05b6e71a1e3df34e2233c51e79838b22f7e8', - }, - ], - [ - 'git.scribouilli.org', - { - origin: 'https://git.scribouilli.org', - client_id: - '3e8ac6636615d396a8f73e02fa3880e7e2140981b0ca27b0f240a450f69f1c76', - }, - ], -]) - -function redirectURLByProvider(gitProvider: string, destination: string) { - if (gitProvider === 'github.com') { +function redirectURLByProvider( + { type: providerType, origin }: ScribouilliBackendProvider, + destination: string, +) { + if (providerType === 'github') { return `${TOCTOCTOC_ORIGIN}/github-callback?destination=${destination}` + } else if (providerType === 'gitlab') { + return `${TOCTOCTOC_ORIGIN}/gitlab-callback/${origin}/?destination=${destination}` + } else if (providerType === 'scribouilli') { + return `${destination}?${TOCTOCTOC_OAUTH_PROVIDER_URL_PARAMETER}=scribouilli&${TOCTOCTOC_OAUTH_PROVIDER_ORIGIN_PARAMETER}=${origin}` } else { - // assume Gitlab and assume HTTPS - return `${TOCTOCTOC_ORIGIN}/gitlab-callback/https://${gitProvider}/?destination=${destination}` + throw new Error('unreachable') } } function makeLoginHref( - gitProvider: string, - client_id: string, - redirect_url: string, + { clientId, type: providerType, origin }: ScribouilliBackendProvider, + redirectUrl: string, ) { - if (gitProvider === 'github.com') { - return `https://github.com/login/oauth/authorize?client_id=${client_id}&scope=public_repo,user:email&redirect_uri=${redirect_url}` + if (providerType === 'github') { + return `${origin}/login/oauth/authorize?client_id=${clientId}&scope=public_repo,user:email&redirect_uri=${redirectUrl}` + } else if (providerType == 'gitlab') { + return `${origin}/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=api+read_api` } else { - // assume HTTPS - return `https://${gitProvider}/oauth/authorize?client_id=${client_id}&redirect_uri=${redirect_url}&response_type=code&scope=api+read_api` + return `${origin}/login?callback=${encodeURIComponent(redirectUrl)}` } } @@ -65,18 +50,21 @@ export default ({ querystring }: Context) => { const destination = location.origin + store.state.basePath + '/after-oauth-login' - const client_id = oAuthAppByProvider.get(gitProvider)?.client_id - if (!client_id) { - throw new TypeError(`Missing client_id`) + + const provider = PROVIDERS_MAP.get(gitProvider) + + if (!provider) { + throw new TypeError(`Unknown provider ${gitProvider}`) } - const redirect_url = redirectURLByProvider(gitProvider, destination) - const loginHref = makeLoginHref(gitProvider, client_id, redirect_url) + const redirectUrl = redirectURLByProvider(provider, destination) + const loginHref = makeLoginHref(provider, redirectUrl) replaceComponent(Login, () => { return { href: loginHref, - gitProvider, + providerType: provider.type, + providerId: provider.id, } }) } diff --git a/assets/scripts/scribouilliGitRepo.ts b/assets/scripts/scribouilliGitRepo.ts index 493c6b54..0066d925 100644 --- a/assets/scripts/scribouilliGitRepo.ts +++ b/assets/scripts/scribouilliGitRepo.ts @@ -1,3 +1,4 @@ +import type { BackendType } from './types/atelier.ts' import type { OAuthServiceAPI } from './types/git.ts' export default class ScribouilliGitRepo { @@ -6,30 +7,37 @@ export default class ScribouilliGitRepo { public publicRepositoryURL public owner public repoName + public repoType: BackendType public repoId public publishedWebsiteURL: Promise constructor({ repoId, origin, - publicRepositoryURL, owner, repoName, + repoType, gitServiceProvider, }: { repoId?: string origin: string - publicRepositoryURL: string owner: string repoName: string + repoType: BackendType gitServiceProvider: OAuthServiceAPI }) { this.origin = origin - this.publicRepositoryURL = publicRepositoryURL + this.publicRepositoryURL = gitServiceProvider.makePublicRepositoryURL( + owner, + repoName, + ) this.owner = owner this.repoName = repoName + this.repoType = repoType - this.repoId = repoId ? repoId : makeRepoId(owner, repoName) + this.repoId = repoId + ? repoId + : gitServiceProvider.makeRepoId(owner, repoName) this.publishedWebsiteURL = new Promise(resolve => { const interval = setInterval(() => { @@ -43,21 +51,3 @@ export default class ScribouilliGitRepo { }) } } - -/** - * @param owner may be an individual Github user or an organisation - */ -export function makeRepoId(owner: string, repoName: string): string { - return `${owner}/${repoName}` -} - -/** - * @param owner may be an individual Github user or an organisation - */ -export function makePublicRepositoryURL( - owner: string, - repoName: string, - origin: string, -): string { - return `${origin}/${owner}/${repoName}` -} diff --git a/assets/scripts/store.ts b/assets/scripts/store.ts index 90875ab5..cbc4d483 100644 --- a/assets/scripts/store.ts +++ b/assets/scripts/store.ts @@ -22,9 +22,10 @@ export interface ResolutionOption { } export interface OAuthProvider { - name: string + type: string accessToken: string origin: string + id: string } export interface ScribouilliState { diff --git a/assets/scripts/types/atelier.ts b/assets/scripts/types/atelier.ts index 69daccee..d9aa8d04 100644 --- a/assets/scripts/types/atelier.ts +++ b/assets/scripts/types/atelier.ts @@ -31,3 +31,18 @@ export interface FileContenu { inMenu: boolean blogIndex: boolean } + +export type BackendType = 'github' | 'gitlab' | 'scribouilli' + +export interface ScribouilliBackendProvider { + id: string + origin: string + clientId?: string + type: BackendType + description: string + name: string + signupInstructions?: string + signupEnabled: boolean + signupLink?: string + corsProxy?: string +} diff --git a/assets/scripts/types/git.ts b/assets/scripts/types/git.ts index aae18fdc..e2d2dc98 100644 --- a/assets/scripts/types/git.ts +++ b/assets/scripts/types/git.ts @@ -1,7 +1,10 @@ +import type { BackendType } from './atelier' + interface ScribouilliGitRepo { repoId: string owner: string repoName: string + repoType: BackendType origin: string publishedWebsiteURL: Promise publicRepositoryURL: string @@ -20,7 +23,10 @@ export type BuildStatus = | 'needs_account_verification' export interface OAuthServiceAPI { - callAPI: (url: string, requestParams?: RequestInit) => Promise + callAPI: ( + url: string, + requestParams?: RequestInit & { headers?: Record }, + ) => Promise getOauthUsernameAndPassword: () => { username: string; password: string } getAuthenticatedUser: () => Promise getUserEmails: () => Promise @@ -42,6 +48,8 @@ export interface OAuthServiceAPI { getPublishedWebsiteURL: ( scribouilliGitRepo: ScribouilliGitRepo, ) => Promise + makeRepoId: (owner: string, repoName: string) => string + makePublicRepositoryURL: (owner: string, repoName: string) => string } interface AuthenticatedUserEmails { diff --git a/assets/styles/styles.css b/assets/styles/styles.css index b4d6569f..ac52d154 100644 --- a/assets/styles/styles.css +++ b/assets/styles/styles.css @@ -357,4 +357,25 @@ a[href]:not(:where([href^="#"], ul.list-with-dot { padding-left: 1rem; list-style-type: disc; -} \ No newline at end of file +} + +.config-content { + ol { + text-align: left; + + li { + margin-bottom: 1rem; + } + } + + .text-align-start { + text-align: start; + } + + .simple-list { + padding: 1rem 5rem; + text-align: left; + list-style-type: disc; + list-style-position: inside; + } +} diff --git a/package-lock.json b/package-lock.json index 4ce00845..567cc4b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "js-yaml": "^3.14.1", "marked": "^11.0.0", "page": "^1.11.6", - "remember": "github:DavidBruant/remember#v1.0.2" + "remember": "github:DavidBruant/remember#v1.0.2", + "zod": "^4.5.2" }, "devDependencies": { "@rollup/plugin-commonjs": "^29.0.3", @@ -8041,6 +8042,15 @@ "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "dev": true, "license": "MIT" + }, + "node_modules/zod": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.2.tgz", + "integrity": "sha512-XkYXCol10+ba/6F/cueWV+TezUeOqXW0hdeJt5CdXjTYeAgAQg5N03RQdJ80mhfFE72+pblvYMW4wy2Qp4Qbrg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 36e3371f..5fb6a562 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "js-yaml": "^3.14.1", "marked": "^11.0.0", "page": "^1.11.6", - "remember": "github:DavidBruant/remember#v1.0.2" + "remember": "github:DavidBruant/remember#v1.0.2", + "zod": "^4.5.2" } } diff --git a/tests/actions/current-repository.test.ts b/tests/actions/current-repository.test.ts index 4f433773..1eb56521 100644 --- a/tests/actions/current-repository.test.ts +++ b/tests/actions/current-repository.test.ts @@ -6,6 +6,7 @@ import { describe } from 'mocha' import sinon from 'sinon' import { expect } from 'chai' import GitAgent from '../../assets/scripts/GitAgent.ts' +import GitHubAPI from '../../assets/scripts/oauth-services-api/github.ts' describe('actions/current-repository.ts', () => { describe('saveCustomCSS', () => { @@ -22,11 +23,12 @@ describe('actions/current-repository.ts', () => { repoName: 'site', repoId: 'test-site', publishedWebsiteURL: Promise.resolve('https://test.github.io/site'), + repoType: 'github' as const }, gitAgent: new GitAgent({ - auth: {}, remoteURL: 'https://github.com', repoId: 'test-site', + gitServiceProvider: new GitHubAPI('fake token') }), }, subscribe: sinon.stub(),