mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #5530 from logto-io/gao-add-oidc-standard-claims
feat: add oidc standard claims to user
This commit is contained in:
commit
a98bc3da54
35 changed files with 521 additions and 178 deletions
18
.changeset/grumpy-seas-begin.md
Normal file
18
.changeset/grumpy-seas-begin.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
"@logto/core-kit": minor
|
||||
"@logto/schemas": minor
|
||||
"@logto/core": minor
|
||||
"@logto/phrases-experience": patch
|
||||
"@logto/integration-tests": patch
|
||||
---
|
||||
|
||||
full oidc standard claims support
|
||||
|
||||
We have added support for the remaining [OpenID Connect standard claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims). Now, these claims are accessible in both ID tokens and the response from the `/me` endpoint.
|
||||
|
||||
Additionally, we adhere to the standard scopes - claims mapping. This means that you can retrieve most of the profile claims using the `profile` scope, and the `address` claim can be obtained by using the `address` scope.
|
||||
|
||||
For all newly introduced claims, we store them in the `user.profile` field.
|
||||
|
||||
> ![Note]
|
||||
> Unlike other database fields (e.g. `name`), the claims stored in the `profile` field will fall back to `undefined` rather than `null`. We refrain from using `?? null` here to reduce the size of ID tokens, since `undefined` fields will be stripped in tokens.
|
3
.github/workflows/commitlint.yml
vendored
3
.github/workflows/commitlint.yml
vendored
|
@ -24,7 +24,8 @@ jobs:
|
|||
uses: silverhand-io/actions-node-pnpm-run-steps@v4
|
||||
|
||||
- name: Commitlint
|
||||
run: npx commitlint --from HEAD~${{ github.event.pull_request.commits }} --to HEAD
|
||||
# Credit to https://stackoverflow.com/a/67365254/12514940
|
||||
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||
|
||||
- name: Commitlint on PR title
|
||||
run: echo '${{ github.event.pull_request.title }}' | npx commitlint
|
||||
|
|
|
@ -19,9 +19,11 @@ export const mockUser: User = {
|
|||
logtoConfig: {},
|
||||
mfaVerifications: [],
|
||||
customData: {},
|
||||
profile: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_789,
|
||||
createdAt: 1_650_969_000_000,
|
||||
updatedAt: 1_650_969_000_000,
|
||||
isSuspended: false,
|
||||
};
|
||||
|
||||
|
@ -73,9 +75,11 @@ export const mockUserWithPassword: User = {
|
|||
customData: {},
|
||||
logtoConfig: {},
|
||||
mfaVerifications: [],
|
||||
profile: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_789,
|
||||
createdAt: 1_650_969_000_000,
|
||||
updatedAt: 1_650_969_000_000,
|
||||
isSuspended: false,
|
||||
};
|
||||
|
||||
|
@ -94,9 +98,11 @@ export const mockUserList: User[] = [
|
|||
customData: {},
|
||||
logtoConfig: {},
|
||||
mfaVerifications: [],
|
||||
profile: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_000,
|
||||
createdAt: 1_650_969_000_000,
|
||||
updatedAt: 1_650_969_000_000,
|
||||
isSuspended: false,
|
||||
},
|
||||
{
|
||||
|
@ -113,9 +119,11 @@ export const mockUserList: User[] = [
|
|||
customData: {},
|
||||
logtoConfig: {},
|
||||
mfaVerifications: [],
|
||||
profile: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_000,
|
||||
createdAt: 1_650_969_000_000,
|
||||
updatedAt: 1_650_969_000_000,
|
||||
isSuspended: false,
|
||||
},
|
||||
{
|
||||
|
@ -132,9 +140,11 @@ export const mockUserList: User[] = [
|
|||
customData: {},
|
||||
logtoConfig: {},
|
||||
mfaVerifications: [],
|
||||
profile: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_000,
|
||||
createdAt: 1_650_969_000_000,
|
||||
updatedAt: 1_650_969_000_000,
|
||||
isSuspended: false,
|
||||
},
|
||||
{
|
||||
|
@ -151,9 +161,11 @@ export const mockUserList: User[] = [
|
|||
customData: {},
|
||||
logtoConfig: {},
|
||||
mfaVerifications: [],
|
||||
profile: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_000,
|
||||
createdAt: 1_650_969_000_000,
|
||||
updatedAt: 1_650_969_000_000,
|
||||
isSuspended: false,
|
||||
},
|
||||
{
|
||||
|
@ -170,9 +182,11 @@ export const mockUserList: User[] = [
|
|||
customData: {},
|
||||
logtoConfig: {},
|
||||
mfaVerifications: [],
|
||||
profile: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_000,
|
||||
createdAt: 1_650_969_000_000,
|
||||
updatedAt: 1_650_969_000_000,
|
||||
isSuspended: false,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -65,10 +65,12 @@ export const generateHookTestPayload = (hookId: string, event: HookEvent): HookE
|
|||
userId: 'fake-google-user-id',
|
||||
},
|
||||
},
|
||||
profile: {},
|
||||
applicationId: 'fake-application-id',
|
||||
isSuspended: false,
|
||||
lastSignInAt: now.getTime(),
|
||||
createdAt: now.getTime(),
|
||||
updatedAt: now.getTime(),
|
||||
},
|
||||
application: {
|
||||
id: 'fake-spa-application-id',
|
||||
|
|
|
@ -5,18 +5,33 @@ const use = {
|
|||
userinfo: 'userinfo',
|
||||
} as const;
|
||||
|
||||
const profileExpectation = Object.freeze([
|
||||
'name',
|
||||
'family_name',
|
||||
'given_name',
|
||||
'middle_name',
|
||||
'nickname',
|
||||
'preferred_username',
|
||||
'profile',
|
||||
'picture',
|
||||
'website',
|
||||
'gender',
|
||||
'birthdate',
|
||||
'zoneinfo',
|
||||
'locale',
|
||||
'updated_at',
|
||||
'username',
|
||||
'created_at',
|
||||
]);
|
||||
|
||||
describe('OIDC getUserClaims()', () => {
|
||||
it('should return proper ID Token claims', () => {
|
||||
expect(getAcceptedUserClaims(use.idToken, 'openid profile', {}, [])).toEqual([
|
||||
'name',
|
||||
'picture',
|
||||
'username',
|
||||
]);
|
||||
expect(getAcceptedUserClaims(use.idToken, 'openid profile', {}, [])).toEqual(
|
||||
profileExpectation
|
||||
);
|
||||
|
||||
expect(getAcceptedUserClaims(use.idToken, 'openid profile email phone', {}, [])).toEqual([
|
||||
'name',
|
||||
'picture',
|
||||
'username',
|
||||
...profileExpectation,
|
||||
'email',
|
||||
'email_verified',
|
||||
'phone_number',
|
||||
|
@ -25,23 +40,23 @@ describe('OIDC getUserClaims()', () => {
|
|||
|
||||
expect(
|
||||
getAcceptedUserClaims(use.idToken, 'openid profile custom_data identities', {}, [])
|
||||
).toEqual(['name', 'picture', 'username']);
|
||||
).toEqual(profileExpectation);
|
||||
|
||||
expect(
|
||||
getAcceptedUserClaims(use.idToken, 'openid profile email', {}, ['email_verified'])
|
||||
).toEqual(['name', 'picture', 'username', 'email']);
|
||||
).toEqual([...profileExpectation, 'email']);
|
||||
});
|
||||
|
||||
it('should return proper Userinfo claims', () => {
|
||||
expect(
|
||||
getAcceptedUserClaims(use.userinfo, 'openid profile custom_data identities', {}, [])
|
||||
).toEqual(['name', 'picture', 'username', 'custom_data', 'identities']);
|
||||
).toEqual([...profileExpectation, 'custom_data', 'identities']);
|
||||
});
|
||||
|
||||
// Ignore `_claims` since [Claims Parameter](https://github.com/panva/node-oidc-provider/tree/main/docs#featuresclaimsparameter) is not enabled
|
||||
it('should ignore claims parameter', () => {
|
||||
expect(
|
||||
getAcceptedUserClaims(use.idToken, 'openid profile custom_data', { email: null }, [])
|
||||
).toEqual(['name', 'picture', 'username']);
|
||||
).toEqual(profileExpectation);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,13 +2,17 @@ import assert from 'node:assert';
|
|||
|
||||
import type { UserClaim } from '@logto/core-kit';
|
||||
import { idTokenClaims, userinfoClaims, UserScope } from '@logto/core-kit';
|
||||
import { type User } from '@logto/schemas';
|
||||
import { type UserProfile, type User, userProfileKeys } from '@logto/schemas';
|
||||
import { pick, type Nullable, cond } from '@silverhand/essentials';
|
||||
import type { ClaimsParameterMember } from 'oidc-provider';
|
||||
import { snakeCase } from 'snake-case';
|
||||
import { type SnakeCaseKeys } from 'snakecase-keys';
|
||||
|
||||
import type Libraries from '#src/tenants/Libraries.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
|
||||
type UserProfileClaimSnakeCase = keyof SnakeCaseKeys<UserProfile>;
|
||||
|
||||
const claimToUserKey: Readonly<
|
||||
Record<
|
||||
Exclude<
|
||||
|
@ -19,6 +23,7 @@ const claimToUserKey: Readonly<
|
|||
| 'organizations'
|
||||
| 'organization_data'
|
||||
| 'organization_roles'
|
||||
| UserProfileClaimSnakeCase
|
||||
>,
|
||||
keyof User
|
||||
>
|
||||
|
@ -30,8 +35,29 @@ const claimToUserKey: Readonly<
|
|||
phone_number: 'primaryPhone',
|
||||
custom_data: 'customData',
|
||||
identities: 'identities',
|
||||
created_at: 'createdAt',
|
||||
updated_at: 'updatedAt',
|
||||
});
|
||||
|
||||
const claimToUserProfileKey: Readonly<Record<UserProfileClaimSnakeCase, keyof UserProfile>> =
|
||||
Object.freeze({
|
||||
family_name: 'familyName',
|
||||
given_name: 'givenName',
|
||||
middle_name: 'middleName',
|
||||
nickname: 'nickname',
|
||||
preferred_username: 'preferredUsername',
|
||||
profile: 'profile',
|
||||
website: 'website',
|
||||
gender: 'gender',
|
||||
birthdate: 'birthdate',
|
||||
zoneinfo: 'zoneinfo',
|
||||
locale: 'locale',
|
||||
address: 'address',
|
||||
});
|
||||
|
||||
const isUserProfileClaim = (claim: string): claim is UserProfileClaimSnakeCase =>
|
||||
userProfileKeys.some((key) => snakeCase(key) === claim);
|
||||
|
||||
/**
|
||||
* Get user claims data according to the claims.
|
||||
*
|
||||
|
@ -88,6 +114,19 @@ export const getUserClaimsData = async (
|
|||
];
|
||||
}
|
||||
default: {
|
||||
if (isUserProfileClaim(claim)) {
|
||||
// Unlike other database fields (e.g. `name`), the claims stored in the `profile` field
|
||||
// will fall back to `undefined` rather than `null`. We refrain from using `?? null`
|
||||
// here to reduce the size of ID tokens, since `undefined` fields will be stripped in
|
||||
// tokens.
|
||||
//
|
||||
// The only consideration here is the inconsistency for `name` and `picture`, which are
|
||||
// also standard claims but they will fall back to `null`. While it's possible to align
|
||||
// their behavior by having their fallback to `undefined`, it's better to maintain the
|
||||
// current setup for now to prevent breaking changes.
|
||||
return [claim, user.profile[claimToUserProfileKey[claim]]];
|
||||
}
|
||||
|
||||
return [claim, user[claimToUserKey[claim]]];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ describe('user query', () => {
|
|||
const { table, fields } = convertToIdentifiers(Users);
|
||||
const databaseValue = {
|
||||
...mockUser,
|
||||
profile: JSON.stringify({}),
|
||||
identities: JSON.stringify(mockUser.identities),
|
||||
customData: JSON.stringify(mockUser.customData),
|
||||
logtoConfig: JSON.stringify(mockUser.logtoConfig),
|
||||
|
@ -321,6 +322,7 @@ describe('user query', () => {
|
|||
const { connector1, ...restIdentities } = mockUser.identities;
|
||||
const finalDbvalue = {
|
||||
...mockUser,
|
||||
profile: JSON.stringify({}),
|
||||
identities: JSON.stringify(restIdentities),
|
||||
customData: JSON.stringify(mockUser.customData),
|
||||
logtoConfig: JSON.stringify(mockUser.logtoConfig),
|
||||
|
|
|
@ -108,6 +108,30 @@
|
|||
"description": "Update custom data for the given user ID. This method performs a partial update of the custom data object."
|
||||
}
|
||||
},
|
||||
"/api/users/{userId}/profile": {
|
||||
"patch": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"profile": {
|
||||
"description": "Partial profile object to update for the given user ID."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Updated profile in JSON for the given user ID."
|
||||
}
|
||||
},
|
||||
"summary": "Update user profile",
|
||||
"description": "Update profile for the given user ID. This method performs a partial update of the profile object."
|
||||
}
|
||||
},
|
||||
"/api/users": {
|
||||
"post": {
|
||||
"requestBody": {
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
UsersPasswordEncryptionMethod,
|
||||
jsonObjectGuard,
|
||||
userInfoSelectFields,
|
||||
userProfileGuard,
|
||||
userProfileResponseGuard,
|
||||
} from '@logto/schemas';
|
||||
import { conditional, pick, yes } from '@silverhand/essentials';
|
||||
|
@ -108,6 +109,32 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
|
|||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/users/:userId/profile',
|
||||
koaGuard({
|
||||
params: object({ userId: string() }),
|
||||
body: object({ profile: userProfileGuard }),
|
||||
response: userProfileGuard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { userId },
|
||||
body: { profile },
|
||||
} = ctx.guard;
|
||||
|
||||
await findUserById(userId);
|
||||
|
||||
const user = await updateUserById(userId, {
|
||||
profile,
|
||||
});
|
||||
|
||||
ctx.body = user.profile;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/users',
|
||||
koaGuard({
|
||||
|
@ -121,6 +148,7 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
|
|||
name: string(),
|
||||
avatar: string().url().or(literal('')).nullable(),
|
||||
customData: jsonObjectGuard,
|
||||
profile: userProfileGuard,
|
||||
}).partial(),
|
||||
response: userProfileResponseGuard,
|
||||
status: [200, 404, 422],
|
||||
|
@ -136,6 +164,7 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
|
|||
passwordAlgorithm,
|
||||
avatar,
|
||||
customData,
|
||||
profile,
|
||||
} = ctx.guard.body;
|
||||
|
||||
assertThat(!(password && passwordDigest), new RequestError('user.password_and_digest'));
|
||||
|
@ -178,6 +207,7 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
|
|||
passwordEncryptionMethod: passwordAlgorithm,
|
||||
}
|
||||
),
|
||||
...conditional(profile && { profile }),
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
@ -199,6 +229,7 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
|
|||
name: string().or(literal('')).nullable(),
|
||||
avatar: string().url().or(literal('')).nullable(),
|
||||
customData: jsonObjectGuard,
|
||||
profile: userProfileGuard,
|
||||
}).partial(),
|
||||
response: userProfileResponseGuard,
|
||||
status: [200, 404, 422],
|
||||
|
@ -317,6 +348,7 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
|
|||
|
||||
return next();
|
||||
}
|
||||
// eslint-disable-next-line max-lines
|
||||
);
|
||||
|
||||
router.delete(
|
||||
|
|
|
@ -48,6 +48,13 @@ export const updateUser = async (userId: string, payload: Partial<User>) =>
|
|||
})
|
||||
.json<User>();
|
||||
|
||||
export const updateUserProfile = async (userId: string, profile: Partial<User['profile']>) =>
|
||||
authedAdminApi
|
||||
.patch(`users/${userId}/profile`, {
|
||||
json: { profile },
|
||||
})
|
||||
.json<User['profile']>();
|
||||
|
||||
export const suspendUser = async (userId: string, isSuspended: boolean) =>
|
||||
authedAdminApi.patch(`users/${userId}/is-suspended`, { json: { isSuspended } }).json<User>();
|
||||
|
||||
|
|
|
@ -2,7 +2,11 @@ import fs from 'node:fs/promises';
|
|||
import { createServer, type RequestListener } from 'node:http';
|
||||
|
||||
import { mockConnectorFilePaths, type SendMessagePayload } from '@logto/connector-kit';
|
||||
import { type JsonObject, type UsersPasswordEncryptionMethod } from '@logto/schemas';
|
||||
import {
|
||||
type UserProfile,
|
||||
type JsonObject,
|
||||
type UsersPasswordEncryptionMethod,
|
||||
} from '@logto/schemas';
|
||||
import { RequestError } from 'got';
|
||||
|
||||
import { createUser } from '#src/api/index.js';
|
||||
|
@ -18,6 +22,7 @@ export const createUserByAdmin = async (
|
|||
passwordDigest?: string;
|
||||
passwordAlgorithm?: UsersPasswordEncryptionMethod;
|
||||
customData?: JsonObject;
|
||||
profile?: UserProfile;
|
||||
} = {}
|
||||
) => {
|
||||
const { username, name, ...rest } = payload;
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
postUserIdentity,
|
||||
verifyUserPassword,
|
||||
putUserIdentity,
|
||||
updateUserProfile,
|
||||
} from '#src/api/index.js';
|
||||
import { clearConnectorsByTypes } from '#src/helpers/connector.js';
|
||||
import { createUserByAdmin, expectRejects } from '#src/helpers/index.js';
|
||||
|
@ -55,12 +56,14 @@ describe('admin console user management', () => {
|
|||
await expect(verifyUserPassword(user.id, 'password')).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should create user with custom data successfully', async () => {
|
||||
it('should create user with custom data and profile successfully', async () => {
|
||||
const user = await createUserByAdmin({
|
||||
customData: { foo: 'bar' },
|
||||
profile: { gender: 'neutral' },
|
||||
});
|
||||
const { customData } = await getUser(user.id);
|
||||
const { customData, profile } = await getUser(user.id);
|
||||
expect(customData).toStrictEqual({ foo: 'bar' });
|
||||
expect(profile).toStrictEqual({ gender: 'neutral' });
|
||||
});
|
||||
|
||||
it('should fail when create user with conflict identifiers', async () => {
|
||||
|
@ -104,11 +107,38 @@ describe('admin console user management', () => {
|
|||
customData: {
|
||||
level: 1,
|
||||
},
|
||||
profile: {
|
||||
familyName: 'new family name',
|
||||
address: {
|
||||
formatted: 'new formatted address',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const updatedUser = await updateUser(user.id, newUserData);
|
||||
|
||||
expect(updatedUser).toMatchObject(newUserData);
|
||||
expect(updatedUser.updatedAt).toBeGreaterThan(user.updatedAt);
|
||||
});
|
||||
|
||||
it('should able to update profile partially', async () => {
|
||||
const user = await createUserByAdmin();
|
||||
const profile = {
|
||||
familyName: 'new family name',
|
||||
address: {
|
||||
formatted: 'new formatted address',
|
||||
},
|
||||
};
|
||||
|
||||
const updatedProfile = await updateUserProfile(user.id, profile);
|
||||
expect(updatedProfile).toStrictEqual(profile);
|
||||
|
||||
const patchProfile = {
|
||||
familyName: 'another name',
|
||||
website: 'https://logto.io/',
|
||||
};
|
||||
const updatedProfile2 = await updateUserProfile(user.id, patchProfile);
|
||||
expect(updatedProfile2).toStrictEqual({ ...profile, ...patchProfile });
|
||||
});
|
||||
|
||||
it('should respond 422 when no update data provided', async () => {
|
||||
|
@ -157,9 +187,10 @@ describe('admin console user management', () => {
|
|||
});
|
||||
|
||||
it('should update user password successfully', async () => {
|
||||
const user = await createUserByAdmin();
|
||||
const userEntity = await updateUserPassword(user.id, 'new_password');
|
||||
expect(userEntity).toMatchObject(user);
|
||||
const { updatedAt, ...rest } = await createUserByAdmin();
|
||||
const userEntity = await updateUserPassword(rest.id, 'new_password');
|
||||
expect(userEntity).toMatchObject(rest);
|
||||
expect(userEntity.updatedAt).toBeGreaterThan(updatedAt);
|
||||
});
|
||||
|
||||
it('should link social identity successfully', async () => {
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
[UserScope.Email]: 'Your email address',
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
custom_data: 'Your custom data',
|
||||
email: 'Your email address',
|
||||
phone: 'Your phone number',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
roles: 'Your roles',
|
||||
identities: 'Your linked social identities',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
|
||||
const user_scopes = {
|
||||
descriptions: {
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.CustomData]: 'Your custom data',
|
||||
custom_data: 'Your custom data',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Email]: 'Your email address',
|
||||
email: 'Your email address',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Phone]: 'Your phone number',
|
||||
phone: 'Your phone number',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Profile]: 'Your name, username and avatar',
|
||||
profile: 'Your name, username, avatar, and other profile info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Roles]: 'Your roles',
|
||||
roles: 'Your roles',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Identities]: 'Your linked social identities',
|
||||
identities: 'Your linked social identities',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.Organizations]: 'Your organizations info',
|
||||
'urn:logto:scope:organizations': 'Your organizations info',
|
||||
/** UNTRANSLATED */
|
||||
[UserScope.OrganizationRoles]: 'Your organization roles',
|
||||
'urn:logto:scope:organization_roles': 'Your organization roles',
|
||||
/** UNTRANSLATED */
|
||||
address: 'Your address',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { sql } from '@silverhand/slonik';
|
||||
|
||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||
|
||||
const alteration: AlterationScript = {
|
||||
up: async (pool) => {
|
||||
await pool.query(sql`
|
||||
alter table users
|
||||
add column profile jsonb not null default '{}'::jsonb,
|
||||
add column updated_at timestamptz not null default (now());
|
||||
`);
|
||||
await pool.query(sql`
|
||||
create function set_updated_at() returns trigger as
|
||||
$$ begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end; $$ language plpgsql;
|
||||
|
||||
create trigger set_updated_at
|
||||
before update on users
|
||||
for each row
|
||||
execute procedure set_updated_at();
|
||||
`);
|
||||
},
|
||||
down: async (pool) => {
|
||||
await pool.query(sql`
|
||||
alter table users
|
||||
drop column profile,
|
||||
drop column updated_at;
|
||||
`);
|
||||
await pool.query(sql`
|
||||
drop trigger set_updated_at on users;
|
||||
drop function set_updated_at();
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -2,6 +2,56 @@ import { z } from 'zod';
|
|||
|
||||
import { MfaFactor } from './sign-in-experience.js';
|
||||
|
||||
export type UserProfile = Partial<{
|
||||
familyName: string;
|
||||
givenName: string;
|
||||
middleName: string;
|
||||
nickname: string;
|
||||
preferredUsername: string;
|
||||
profile: string;
|
||||
website: string;
|
||||
gender: string;
|
||||
birthdate: string;
|
||||
zoneinfo: string;
|
||||
locale: string;
|
||||
address: Partial<{
|
||||
formatted: string;
|
||||
streetAddress: string;
|
||||
locality: string;
|
||||
region: string;
|
||||
postalCode: string;
|
||||
country: string;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export const userProfileGuard = (
|
||||
z.object({
|
||||
familyName: z.string(),
|
||||
givenName: z.string(),
|
||||
middleName: z.string(),
|
||||
nickname: z.string(),
|
||||
preferredUsername: z.string(),
|
||||
profile: z.string(),
|
||||
website: z.string(),
|
||||
gender: z.string(),
|
||||
birthdate: z.string(),
|
||||
zoneinfo: z.string(),
|
||||
locale: z.string(),
|
||||
address: z
|
||||
.object({
|
||||
formatted: z.string(),
|
||||
streetAddress: z.string(),
|
||||
locality: z.string(),
|
||||
region: z.string(),
|
||||
postalCode: z.string(),
|
||||
country: z.string(),
|
||||
})
|
||||
.partial(),
|
||||
}) satisfies z.ZodType<Required<UserProfile>>
|
||||
).partial();
|
||||
|
||||
export const userProfileKeys = Object.freeze(userProfileGuard.keyof().options);
|
||||
|
||||
export const roleNamesGuard = z.string().array();
|
||||
|
||||
export const identityGuard = z.object({
|
||||
|
|
|
@ -8,13 +8,10 @@ import {
|
|||
type OrganizationInvitation,
|
||||
OrganizationInvitations,
|
||||
} from '../db-entries/index.js';
|
||||
import { type ToZodObject } from '../utils/zod.js';
|
||||
|
||||
import { type UserInfo, type FeaturedUser, userInfoGuard } from './user.js';
|
||||
|
||||
type ToZodObject<T> = z.ZodObject<{
|
||||
[K in keyof T]: z.ZodType<T[K]>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* The simplified organization scope entity that is returned for some endpoints.
|
||||
*/
|
||||
|
|
|
@ -14,9 +14,11 @@ export const userInfoSelectFields = Object.freeze([
|
|||
'identities',
|
||||
'lastSignInAt',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'profile',
|
||||
'applicationId',
|
||||
'isSuspended',
|
||||
] as const);
|
||||
] satisfies Array<keyof User>);
|
||||
|
||||
export const userInfoGuard = Users.guard.pick(
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
|
5
packages/schemas/src/utils/zod.ts
Normal file
5
packages/schemas/src/utils/zod.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { type z } from 'zod';
|
||||
|
||||
export type ToZodObject<T> = z.ZodObject<{
|
||||
[K in keyof T]: z.ZodType<T[K]>;
|
||||
}>;
|
|
@ -1,5 +1,6 @@
|
|||
/* init_order = 0.5 */
|
||||
|
||||
/** A function to set the tenant_id column based on the current user. */
|
||||
create function set_tenant_id() returns trigger as
|
||||
$$ begin
|
||||
if new.tenant_id is not null then
|
||||
|
@ -13,4 +14,11 @@ $$ begin
|
|||
return new;
|
||||
end; $$ language plpgsql;
|
||||
|
||||
/** A function to set the `updated_at` column to the current time. */
|
||||
create function set_updated_at() returns trigger as
|
||||
$$ begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end; $$ language plpgsql;
|
||||
|
||||
/* no_after_each */
|
||||
|
|
|
@ -12,7 +12,10 @@ create table users (
|
|||
password_encrypted varchar(128),
|
||||
password_encryption_method users_password_encryption_method,
|
||||
name varchar(128),
|
||||
/** The URL that points to the user's profile picture. Mapped to OpenID Connect's `picture` claim. */
|
||||
avatar varchar(2048),
|
||||
/** Additional OpenID Connect standard claims that are not included in user's properties. */
|
||||
profile jsonb /* @use UserProfile */ not null default '{}'::jsonb,
|
||||
application_id varchar(21),
|
||||
identities jsonb /* @use Identities */ not null default '{}'::jsonb,
|
||||
custom_data jsonb /* @use JsonObject */ not null default '{}'::jsonb,
|
||||
|
@ -21,6 +24,7 @@ create table users (
|
|||
is_suspended boolean not null default false,
|
||||
last_sign_in_at timestamptz,
|
||||
created_at timestamptz not null default (now()),
|
||||
updated_at timestamptz not null default (now()),
|
||||
primary key (id),
|
||||
constraint users__username
|
||||
unique (tenant_id, username),
|
||||
|
@ -35,3 +39,8 @@ create index users__id
|
|||
|
||||
create index users__name
|
||||
on users (tenant_id, name);
|
||||
|
||||
create trigger set_updated_at
|
||||
before update on users
|
||||
for each row
|
||||
execute procedure set_updated_at();
|
||||
|
|
|
@ -15,19 +15,35 @@ export enum ReservedResource {
|
|||
}
|
||||
|
||||
export type UserClaim =
|
||||
// OIDC standard claims
|
||||
| 'name'
|
||||
| 'given_name'
|
||||
| 'family_name'
|
||||
| 'middle_name'
|
||||
| 'nickname'
|
||||
| 'preferred_username'
|
||||
| 'profile'
|
||||
| 'picture'
|
||||
| 'username'
|
||||
| 'website'
|
||||
| 'email'
|
||||
| 'email_verified'
|
||||
| 'gender'
|
||||
| 'birthdate'
|
||||
| 'zoneinfo'
|
||||
| 'locale'
|
||||
| 'phone_number'
|
||||
| 'phone_number_verified'
|
||||
| 'address'
|
||||
| 'updated_at'
|
||||
// Custom claims
|
||||
| 'username'
|
||||
| 'roles'
|
||||
| 'organizations'
|
||||
| 'organization_data'
|
||||
| 'organization_roles'
|
||||
| 'custom_data'
|
||||
| 'identities';
|
||||
| 'identities'
|
||||
| 'created_at';
|
||||
|
||||
/**
|
||||
* Scopes for ID Token and Userinfo Endpoint.
|
||||
|
@ -51,6 +67,12 @@ export enum UserScope {
|
|||
* See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
|
||||
*/
|
||||
Phone = 'phone',
|
||||
/**
|
||||
* Scope for user address.
|
||||
*
|
||||
* See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
|
||||
*/
|
||||
Address = 'address',
|
||||
/**
|
||||
* Scope for user's custom data.
|
||||
*
|
||||
|
@ -85,11 +107,33 @@ export enum UserScope {
|
|||
|
||||
/**
|
||||
* Mapped claims that ID Token includes.
|
||||
*
|
||||
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0} for standard scope - claim mapping.
|
||||
*/
|
||||
export const idTokenClaims: Readonly<Record<UserScope, UserClaim[]>> = Object.freeze({
|
||||
[UserScope.Profile]: ['name', 'picture', 'username'],
|
||||
[UserScope.Profile]: [
|
||||
// Standard claims
|
||||
'name',
|
||||
'family_name',
|
||||
'given_name',
|
||||
'middle_name',
|
||||
'nickname',
|
||||
'preferred_username',
|
||||
'profile',
|
||||
'picture',
|
||||
'website',
|
||||
'gender',
|
||||
'birthdate',
|
||||
'zoneinfo',
|
||||
'locale',
|
||||
'updated_at',
|
||||
// Custom claims
|
||||
'username',
|
||||
'created_at',
|
||||
],
|
||||
[UserScope.Email]: ['email', 'email_verified'],
|
||||
[UserScope.Phone]: ['phone_number', 'phone_number_verified'],
|
||||
[UserScope.Address]: ['address'],
|
||||
[UserScope.Roles]: ['roles'],
|
||||
[UserScope.Organizations]: ['organizations'],
|
||||
[UserScope.OrganizationRoles]: ['organization_roles'],
|
||||
|
@ -104,6 +148,7 @@ export const userinfoClaims: Readonly<Record<UserScope, UserClaim[]>> = Object.f
|
|||
[UserScope.Profile]: [],
|
||||
[UserScope.Email]: [],
|
||||
[UserScope.Phone]: [],
|
||||
[UserScope.Address]: [],
|
||||
[UserScope.Roles]: [],
|
||||
[UserScope.Organizations]: ['organization_data'],
|
||||
[UserScope.OrganizationRoles]: [],
|
||||
|
|
Loading…
Reference in a new issue