Skip to content

Commit

Permalink
feat: support i18n (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
zepatrik authored Sep 4, 2023
1 parent 356d98a commit 47ad34b
Show file tree
Hide file tree
Showing 42 changed files with 188 additions and 519 deletions.
21 changes: 17 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
"format:check": "prettier --check .",
"prepublishOnly": "npm run build"
},
"prettier": "ory-prettier-styles",
"dependencies": {
"@ory/client": "1.1.50",
"@ory/elements-markup": "0.1.0-beta.1",
"@ory/elements-markup": "0.1.0-beta.2",
"@ory/integrations": "1.1.4",
"accept-language-parser": "1.5.0",
"axios": "1.2.6",
"body-parser": "1.20.2",
"cookie-parser": "1.4.6",
Expand All @@ -42,6 +44,7 @@
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "4.0.0",
"@types/accept-language-parser": "^1.5.3",
"@types/axios": "0.14.0",
"@types/body-parser": "1.19.2",
"@types/cookie-parser": "1.4.3",
Expand Down
18 changes: 12 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import cookieParser from "cookie-parser"
import express, { Request, Response } from "express"
import { engine } from "express-handlebars"
import * as fs from "fs"
import * as https from "https"
import { addFavicon, defaultConfig, handlebarsHelpers } from "./pkg"
import {
addFavicon,
defaultConfig,
detectLanguage,
handlebarsHelpers,
} from "./pkg"
import { middleware as middlewareLogger } from "./pkg/logger"
import {
register404Route,
Expand All @@ -23,6 +23,11 @@ import {
registerVerificationRoute,
registerWelcomeRoute,
} from "./routes"
import cookieParser from "cookie-parser"
import express, { Request, Response } from "express"
import { engine } from "express-handlebars"
import * as fs from "fs"
import * as https from "https"

const baseUrl = process.env.BASE_PATH || "/"

Expand All @@ -32,6 +37,7 @@ const router = express.Router()
app.use(middlewareLogger)
app.use(cookieParser())
app.use(addFavicon(defaultConfig))
app.use(detectLanguage)
app.set("view engine", "hbs")

app.engine(
Expand Down
9 changes: 3 additions & 6 deletions src/pkg/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import { RouteOptionsCreator } from "./route"
import sdk, { apiBaseUrl } from "./sdk"
import {
UiNode,
ErrorAuthenticatorAssuranceLevelNotSatisfied,
} from "@ory/client"
import { ButtonLink, Divider, MenuLink, Typography } from "@ory/elements-markup"
import { filterNodesByGroups, getNodeLabel } from "@ory/integrations/ui"
import { filterNodesByGroups } from "@ory/integrations/ui"
import { AxiosError } from "axios"
import { NextFunction, Response } from "express"
import { RouteOptionsCreator } from "./route"
import sdk, { apiBaseUrl } from "./sdk"
import { toUiNodePartial } from "./ui"

export * from "./logger"
export * from "./middleware"
Expand Down Expand Up @@ -101,8 +100,6 @@ export const handlebarsHelpers = {
withoutDefaultAttributes,
withoutDefaultGroup,
}),
toUiNodePartial,
getNodeLabel: getNodeLabel,
divider: (fullWidth: boolean, className?: string) =>
Divider({ className, fullWidth }),
buttonLink: (text: string) =>
Expand Down
26 changes: 24 additions & 2 deletions src/pkg/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import { getUrlForFlow, isUUID } from "./index"
import { RouteOptionsCreator } from "./route"
import { Session } from "@ory/client"
import { locales } from "@ory/elements-markup"
import { pick as pickLanguage } from "accept-language-parser"
import { AxiosError } from "axios"
import { NextFunction, Request, Response } from "express"
import { getUrlForFlow, isUUID } from "./index"
import { RouteOptionsCreator } from "./route"

/**
* Checks the error returned by toSession() and initiates a 2FA flow if necessary
* or returns false.
*
* @internal
* @param req
* @param res
* @param apiBaseUrl
*/
Expand Down Expand Up @@ -125,3 +128,22 @@ export const requireNoAuth =
next()
})
}

/**
* Detects the language of the user and sets it in the response locals.
*
* @param req
* @param res
* @param next
*/
export const detectLanguage = (
req: Request,
res: Response,
next: NextFunction,
) => {
res.locals.lang = pickLanguage(
Object.keys(locales),
req.header("Accept-Language") || "en",
)
next()
}
32 changes: 0 additions & 32 deletions src/pkg/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,6 @@ import {
isUiNodeTextAttributes,
} from "@ory/integrations/ui"

// This helper function translates the html input type to the corresponding partial name.
export const toUiNodePartial = (node: UiNode) => {
if (isUiNodeAnchorAttributes(node.attributes)) {
return "ui_node_anchor"
} else if (isUiNodeImageAttributes(node.attributes)) {
return "ui_node_image"
} else if (isUiNodeInputAttributes(node.attributes)) {
switch (node.attributes && node.attributes.type) {
case "hidden":
return "ui_node_input_hidden"
case "submit":
const attrs = node.attributes as UiNodeInputAttributes
const isSocial =
(attrs.name === "provider" || attrs.name === "link") &&
node.group === "oidc"

return isSocial ? "ui_node_input_social_button" : "ui_node_input_button"
case "button":
return "ui_node_input_button"
case "checkbox":
return "ui_node_input_checkbox"
default:
return "ui_node_input_default"
}
} else if (isUiNodeScriptAttributes(node.attributes)) {
return "ui_node_script"
} else if (isUiNodeTextAttributes(node.attributes)) {
return "ui_node_text"
}
return "ui_node_input_default"
}

type NavigationMenuProps = {
navTitle: string
session?: Session
Expand Down
2 changes: 1 addition & 1 deletion src/routes/404.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import { UserErrorCard } from "@ory/elements-markup"
import { RouteRegistrator } from "../pkg"
import { UserErrorCard } from "@ory/elements-markup"

export const register404Route: RouteRegistrator = (app, createHelpers) => {
app.get("*", (req, res) => {
Expand Down
2 changes: 1 addition & 1 deletion src/routes/500.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import { RouteRegistrator } from "../pkg"
import { UserErrorCard } from "@ory/elements-markup"
import { NextFunction, Request, Response } from "express"
import { RouteRegistrator } from "../pkg"

export const register500Route: RouteRegistrator = (app, createHelpers) => {
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
Expand Down
6 changes: 3 additions & 3 deletions src/routes/consent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import { defaultConfig, RouteCreator, RouteRegistrator } from "../pkg"
import { register404Route } from "./404"
import { oidcConformityMaybeFakeSession } from "./stub/oidc-cert"
import {
AcceptOAuth2ConsentRequestSession,
IdentityApi,
Expand All @@ -8,9 +11,6 @@ import {
import { UserConsentCard } from "@ory/elements-markup"
import bodyParser from "body-parser"
import csrf from "csurf"
import { defaultConfig, RouteCreator, RouteRegistrator } from "../pkg"
import { register404Route } from "./404"
import { oidcConformityMaybeFakeSession } from "./stub/oidc-cert"

async function createOAuth2ConsentRequestSession(
grantScopes: string[],
Expand Down
7 changes: 3 additions & 4 deletions src/routes/error.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import { FlowError, FrontendApi, GenericError } from "@ory/client"
import { UserErrorCard } from "@ory/elements-markup"
import { isAxiosError } from "axios"
import {
RouteCreator,
RouteRegistrator,
defaultConfig,
isQuerySet,
} from "../pkg"
import { FlowError, FrontendApi, GenericError } from "@ory/client"
import { UserErrorCard } from "@ory/elements-markup"
import { isAxiosError } from "axios"

type OAuth2Error = {
error: string
Expand Down Expand Up @@ -78,7 +78,6 @@ export const createErrorRoute: RouteCreator =
card: UserErrorCard({
error,
cardImage: logoUrl,
title: "An error occurred",
backUrl: req.header("Referer") || "welcome",
}),
})
Expand Down
2 changes: 1 addition & 1 deletion src/routes/health.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import { Request, Response } from "express"
import { RouteRegistrator } from "../pkg"
import { Request, Response } from "express"

export const registerHealthRoute: RouteRegistrator = (app) => {
app.get("/health/alive", (_: Request, res: Response) => res.send("ok"))
Expand Down
57 changes: 24 additions & 33 deletions src/routes/login.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import { LoginFlow, UiNodeInputAttributes } from "@ory/client"
import { SelfServiceFlow, UserAuthCard } from "@ory/elements-markup"
import {
filterNodesByGroups,
isUiNodeInputAttributes,
} from "@ory/integrations/ui"
import path from "path"
import { URLSearchParams } from "url"
import {
RouteCreator,
RouteRegistrator,
defaultConfig,
getUrlForFlow,
isQuerySet,
logger,
redirectOnSoftError,
RouteCreator,
RouteRegistrator,
} from "../pkg"
import { LoginFlow, UiNodeInputAttributes } from "@ory/client"
import { UserAuthCard } from "@ory/elements-markup"
import {
filterNodesByGroups,
isUiNodeInputAttributes,
} from "@ory/integrations/ui"
import path from "path"
import { URLSearchParams } from "url"

export const createLoginRoute: RouteCreator =
(createHelpers) => async (req, res, next) => {
Expand Down Expand Up @@ -67,10 +67,9 @@ export const createLoginRoute: RouteCreator =
(return_to && return_to.toString()) || loginFlow.return_to || "",
})
.then(({ data }) => data.logout_url)
return logoutUrl
} catch (err) {
logger.error("Unable to create logout URL", { error: err })
} finally {
return logoutUrl
}
}

Expand Down Expand Up @@ -162,7 +161,7 @@ export const createLoginRoute: RouteCreator =
)
}

let logoutUrl = ""
let logoutUrl: string | undefined = ""
if (flow.requested_aal === "aal2" || flow.refresh) {
logoutUrl = await getLogoutUrl(flow)
}
Expand All @@ -181,27 +180,19 @@ export const createLoginRoute: RouteCreator =
return (attributes as UiNodeInputAttributes).onclick
})
.filter((c) => c !== undefined),
card: UserAuthCard({
title: flow.refresh
? "Confirm it's you"
: flow.requested_aal === "aal2"
? "Two-Factor Authentication"
: "Sign In",
...(flow.oauth2_login_request && {
subtitle: `To authenticate ${
flow.oauth2_login_request.client?.client_name ||
flow.oauth2_login_request.client?.client_id
}`,
}),
flow: flow,
flowType: "login",
cardImage: logoUrl,
additionalProps: {
forgotPasswordURL: initRecoveryUrl,
signupURL: initRegistrationUrl,
logoutURL: logoutUrl,
card: UserAuthCard(
{
flow,
flowType: "login",
cardImage: logoUrl,
additionalProps: {
forgotPasswordURL: initRecoveryUrl,
signupURL: initRegistrationUrl,
logoutURL: logoutUrl,
},
},
}),
{ locale: res.locals.lang },
),
})
})
.catch(redirectOnSoftError(res, next, initFlowUrl))
Expand Down
Loading

0 comments on commit 47ad34b

Please sign in to comment.