mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
api: register with username / password
This commit is contained in:
parent
b2c59b4e8e
commit
916130b5fc
6 changed files with 103 additions and 11 deletions
|
@ -13,7 +13,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@logto/essentials": "^1.0.5",
|
"@logto/essentials": "^1.0.5",
|
||||||
"@logto/schemas": "^1.0.3",
|
"@logto/schemas": "^1.0.5",
|
||||||
"dayjs": "^1.10.5",
|
"dayjs": "^1.10.5",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.2",
|
||||||
|
@ -24,6 +24,7 @@
|
||||||
"koa-proxies": "^0.12.1",
|
"koa-proxies": "^0.12.1",
|
||||||
"koa-router": "^10.0.0",
|
"koa-router": "^10.0.0",
|
||||||
"module-alias": "^2.2.2",
|
"module-alias": "^2.2.2",
|
||||||
|
"nanoid": "^3.1.23",
|
||||||
"oidc-provider": "^7.4.1",
|
"oidc-provider": "^7.4.1",
|
||||||
"slonik": "^23.8.1",
|
"slonik": "^23.8.1",
|
||||||
"slonik-interceptor-preset": "^1.2.10",
|
"slonik-interceptor-preset": "^1.2.10",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { IdentifierSqlTokenType, sql } from 'slonik';
|
import { IdentifierSqlTokenType, sql, ValueExpressionType } from 'slonik';
|
||||||
|
|
||||||
type Table = { table: string; fields: Record<string, string> };
|
type Table = { table: string; fields: Record<string, string> };
|
||||||
type FieldIdentifiers<Key extends string | number | symbol> = {
|
type FieldIdentifiers<Key extends string | number | symbol> = {
|
||||||
|
@ -19,3 +19,15 @@ export const convertToIdentifiers = <T extends Table>(
|
||||||
{} as FieldIdentifiers<keyof T['fields']>
|
{} as FieldIdentifiers<keyof T['fields']>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const insertInto = <T extends string>(
|
||||||
|
table: IdentifierSqlTokenType,
|
||||||
|
fields: FieldIdentifiers<T>,
|
||||||
|
fieldKeys: readonly T[],
|
||||||
|
value: { [key in T]?: ValueExpressionType }
|
||||||
|
) => sql`
|
||||||
|
insert into ${table} (${sql.join(Object.values(fields), sql`, `)})
|
||||||
|
values (${sql.join(
|
||||||
|
fieldKeys.map((key) => value[key] ?? null),
|
||||||
|
sql`, `
|
||||||
|
)})`;
|
||||||
|
|
|
@ -3,11 +3,13 @@ import Router from 'koa-router';
|
||||||
import { Provider } from 'oidc-provider';
|
import { Provider } from 'oidc-provider';
|
||||||
import createSignInRoutes from '@/routes/sign-in';
|
import createSignInRoutes from '@/routes/sign-in';
|
||||||
import createUIProxy from '@/proxies/ui';
|
import createUIProxy from '@/proxies/ui';
|
||||||
|
import createRegisterRoutes from '@/routes/register';
|
||||||
|
|
||||||
const createRouter = (provider: Provider): Router => {
|
const createRouter = (provider: Provider): Router => {
|
||||||
const router = new Router();
|
const router = new Router({ prefix: '/api' });
|
||||||
|
|
||||||
router.use('/api', createSignInRoutes(provider));
|
router.use(createSignInRoutes(provider));
|
||||||
|
router.use(createRegisterRoutes());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { UserDBEntry, Users } from '@logto/schemas';
|
import { UserDBEntry, Users } from '@logto/schemas';
|
||||||
import { sql } from 'slonik';
|
import { sql } from 'slonik';
|
||||||
import pool from '@/database/pool';
|
import pool from '@/database/pool';
|
||||||
import { convertToIdentifiers } from '@/database/utils';
|
import { convertToIdentifiers, insertInto } from '@/database/utils';
|
||||||
|
|
||||||
const { table, fields } = convertToIdentifiers(Users);
|
const { table, fields } = convertToIdentifiers(Users);
|
||||||
|
|
||||||
|
@ -9,5 +9,22 @@ export const findUserById = async (id: string) =>
|
||||||
pool.one<UserDBEntry>(sql`
|
pool.one<UserDBEntry>(sql`
|
||||||
select ${sql.join(Object.values(fields), sql`,`)}
|
select ${sql.join(Object.values(fields), sql`,`)}
|
||||||
from ${table}
|
from ${table}
|
||||||
where id=${id}
|
where ${fields.id}=${id}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
export const hasUser = async (username: string) =>
|
||||||
|
pool.exists(sql`
|
||||||
|
select ${fields.id}
|
||||||
|
from ${table}
|
||||||
|
where ${fields.username}=${username}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const hasUserWithId = async (id: string) =>
|
||||||
|
pool.exists(sql`
|
||||||
|
select ${fields.id}
|
||||||
|
from ${table}
|
||||||
|
where ${fields.id}=${id}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const insertUser = async (user: UserDBEntry) =>
|
||||||
|
pool.query(insertInto(table, fields, Users.fieldKeys, user));
|
||||||
|
|
60
packages/core/src/routes/register.ts
Normal file
60
packages/core/src/routes/register.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import Router from 'koa-router';
|
||||||
|
import koaBody from 'koa-body';
|
||||||
|
import { object, string } from 'zod';
|
||||||
|
import { encryptPassword } from '@/utils/password';
|
||||||
|
import { hasUser, hasUserWithId, insertUser } from '@/queries/user';
|
||||||
|
import { customAlphabet, nanoid } from 'nanoid';
|
||||||
|
import { PasswordEncryptionMethod } from '@logto/schemas';
|
||||||
|
|
||||||
|
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||||
|
const userId = customAlphabet(alphabet, 12);
|
||||||
|
|
||||||
|
const generateUserId = async (maxRetries = 500) => {
|
||||||
|
for (let i = 0; i < maxRetries; ++i) {
|
||||||
|
const id = userId();
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
if (!(await hasUserWithId(id))) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Cannot generate user ID in reasonable retries');
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function createRegisterRoutes() {
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
router.post('/register', koaBody(), async (ctx) => {
|
||||||
|
const RegisterBody = object({
|
||||||
|
username: string().min(3),
|
||||||
|
password: string().min(6),
|
||||||
|
});
|
||||||
|
const { username, password } = RegisterBody.parse(ctx.request.body);
|
||||||
|
|
||||||
|
if (await hasUser(username)) {
|
||||||
|
throw new Error('Username already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = await generateUserId();
|
||||||
|
const passwordEncryptionSalt = nanoid();
|
||||||
|
const passwordEncryptionMethod = PasswordEncryptionMethod.SaltAndPepper;
|
||||||
|
const passwordEncrypted = encryptPassword(
|
||||||
|
id,
|
||||||
|
password,
|
||||||
|
passwordEncryptionSalt,
|
||||||
|
passwordEncryptionMethod
|
||||||
|
);
|
||||||
|
|
||||||
|
await insertUser({
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
passwordEncrypted,
|
||||||
|
passwordEncryptionMethod,
|
||||||
|
passwordEncryptionSalt,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = { id };
|
||||||
|
});
|
||||||
|
|
||||||
|
return router.routes();
|
||||||
|
}
|
|
@ -396,10 +396,10 @@
|
||||||
lodash.orderby "^4.6.0"
|
lodash.orderby "^4.6.0"
|
||||||
lodash.pick "^4.4.0"
|
lodash.pick "^4.4.0"
|
||||||
|
|
||||||
"@logto/schemas@^1.0.3":
|
"@logto/schemas@^1.0.5":
|
||||||
version "1.0.3"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@logto/schemas/-/schemas-1.0.3.tgz#6a596bb9d1b99460857eaec5d643040ae384e037"
|
resolved "https://registry.yarnpkg.com/@logto/schemas/-/schemas-1.0.5.tgz#7784ca8f58569bfe1feafb4ad5ca6f098b1a557e"
|
||||||
integrity sha512-inCo/PQUQl9OZFkjehjcB/4PBzYBW9FVn/3nZ2CPr/+3dJKDF3B0qxu8gyQICGJuWYx1BZPygc/NvV0F1klR3g==
|
integrity sha512-JxA9uXNz2tt0NqAp8Gl0hp0L/dLhZakG5sm41JrJKfhtse2Yy2K1X7gBYkH1C9P8P+NcHhv/K4pEgY66yXMOGQ==
|
||||||
|
|
||||||
"@mrmlnc/readdir-enhanced@^2.2.1":
|
"@mrmlnc/readdir-enhanced@^2.2.1":
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
|
@ -3870,7 +3870,7 @@ multimap@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/multimap/-/multimap-1.1.0.tgz#5263febc085a1791c33b59bb3afc6a76a2a10ca8"
|
resolved "https://registry.yarnpkg.com/multimap/-/multimap-1.1.0.tgz#5263febc085a1791c33b59bb3afc6a76a2a10ca8"
|
||||||
integrity sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==
|
integrity sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==
|
||||||
|
|
||||||
nanoid@^3.1.15:
|
nanoid@^3.1.15, nanoid@^3.1.23:
|
||||||
version "3.1.23"
|
version "3.1.23"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
|
||||||
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
|
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
|
||||||
|
|
Loading…
Reference in a new issue