@haruhimemoe/next-kit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -1
- package/README.md +18 -4
- package/dist/auth-react/AccountMenu.d.ts +44 -0
- package/dist/auth-react/AccountMenu.js +36 -0
- package/dist/auth-react/DeleteAccountForm.d.ts +40 -0
- package/dist/auth-react/DeleteAccountForm.js +70 -0
- package/dist/auth-react/SignInWithOsu.d.ts +43 -0
- package/dist/auth-react/SignInWithOsu.js +58 -0
- package/dist/auth-react/SignOutButton.d.ts +35 -0
- package/dist/auth-react/SignOutButton.js +31 -0
- package/dist/auth-react/auth-components.d.ts +50 -0
- package/dist/auth-react/auth-components.js +36 -0
- package/dist/auth-react/avatar.d.ts +19 -0
- package/dist/auth-react/avatar.js +37 -0
- package/dist/auth-react/index.d.ts +8 -1
- package/dist/auth-react/index.js +8 -1
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `@haruhimemoe/next-kit/auth-react`: the account components, styled with `@haruhimemoe/ui` (a new optional peer, ^0.5.0). `SignInWithOsu`, `SignOutButton`, `AccountMenu` (ui's HeaderMenu with the app's links) and `DeleteAccountForm` (type the username, then one DELETE), with `createAuthComponents(authClient, kit)` to bind the app's client and account kit. Moved from packs, pools and bb, which each had a copy.
|
|
14
|
+
- `osuAvatarSrc` and `OSU_AVATAR_HOSTS`: an osu! avatar URL only from a.ppy.sh or osu.ppy.sh. Moved from pools and bb.
|
|
15
|
+
- `signInErrorMessage`: reads better-auth's flat or nested error message, as packs did.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `auth-react` now also loads `@haruhimemoe/ui` at runtime.
|
|
20
|
+
|
|
9
21
|
## [0.1.0] - 2026-09-28
|
|
10
22
|
|
|
11
23
|
### Added
|
|
@@ -17,5 +29,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
17
29
|
- `@haruhimemoe/next-kit/auth-react`: `createSignedInMarker`, `createAccountStore`, `useAccount`, `createAccount`, `RestoreSignedIn` and `osuSignIn`.
|
|
18
30
|
- `@haruhimemoe/next-kit/testing`: `startMemoryMongo`, `setupTestDb`, `setupMsw` and the fake osu! app env.
|
|
19
31
|
|
|
20
|
-
[unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.
|
|
32
|
+
[unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.2.0...HEAD
|
|
33
|
+
[0.2.0]: https://github.com/haruhimemoe/next-kit/compare/v0.1.0...v0.2.0
|
|
21
34
|
[0.1.0]: https://github.com/haruhimemoe/next-kit/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The Next.js server plumbing the haruhime.moe tools share. [packs.haruhime.moe](h
|
|
|
8
8
|
- **`/env`:** zod env parsing that runs on first use, with SKIP_ENV_VALIDATION for CI and a guard that refuses placeholder secrets on a production server.
|
|
9
9
|
- **`/mongo`:** one MongoClient per process with Mongoose on the same client, index builds that never take the site down, and frozen collection names.
|
|
10
10
|
- **`/auth`:** better-auth with osu! as the only way in, the indexes its collections need, and reading the caller.
|
|
11
|
-
- **`/auth-react`:** the browser half: the signed-in marker cookie, the account store and `useAccount`, and
|
|
11
|
+
- **`/auth-react`:** the browser half: the signed-in marker cookie, the account store and `useAccount`, `RestoreSignedIn`, and the account components (Sign in with osu!, Sign out, the header's account menu, Delete my account) styled with `@haruhimemoe/ui`.
|
|
12
12
|
- **`/testing`:** Vitest helpers: one in-memory MongoDB per run, an msw server that refuses unhandled requests, and a fake env.
|
|
13
13
|
|
|
14
14
|
Every name, path, limit and message comes from the caller. There is no root entry point; import a subpath.
|
|
@@ -28,7 +28,7 @@ bun add @haruhimemoe/next-kit zod
|
|
|
28
28
|
| `env` | nothing else |
|
|
29
29
|
| `mongo` | `mongodb` ^7.6.0, `mongoose` ^9.10.2 |
|
|
30
30
|
| `auth` | `better-auth` ^1.7.5, `mongodb`, `@haruhimemoe/osu` 0.2 or 0.3 |
|
|
31
|
-
| `auth-react` | `react` ^19.3.0, `next` ^16.3.6 |
|
|
31
|
+
| `auth-react` | `react` ^19.3.0, `next` ^16.3.6, `@haruhimemoe/ui` ^0.5.0 (with its theme set up) |
|
|
32
32
|
| `testing` | `vitest` ^5.0.1, `msw` ^2.15.0, `mongodb-memory-server` ^11.3.0 |
|
|
33
33
|
|
|
34
34
|
## Use
|
|
@@ -103,7 +103,15 @@ export const getAuth = () => {
|
|
|
103
103
|
import { createAccount, createSignedInMarker } from "@haruhimemoe/next-kit/auth-react";
|
|
104
104
|
|
|
105
105
|
export const marker = createSignedInMarker("pools-signed-in");
|
|
106
|
-
|
|
106
|
+
const kit = createAccount(authClient, marker);
|
|
107
|
+
export const { store, useAccount, markSignedOut, RestoreSignedIn } = kit;
|
|
108
|
+
export const { SignInWithOsu, SignOutButton, AccountMenu, DeleteAccountForm } =
|
|
109
|
+
createAuthComponents(authClient, kit);
|
|
110
|
+
|
|
111
|
+
// Anywhere, server pages included: only plain data is left to pass.
|
|
112
|
+
<SignInWithOsu next={next} />
|
|
113
|
+
<AccountMenu items={[{ href: "/new", label: "Make a pool" }, { href: "/account", label: "Account" }]} />
|
|
114
|
+
<DeleteAccountForm username={user.username} appName="pools" deletes="This deletes your account and every pool you own. It can't be undone." />
|
|
107
115
|
```
|
|
108
116
|
|
|
109
117
|
Build the auth instance once (memoize `getAuth`), and use the same cookie name on both sides.
|
|
@@ -169,6 +177,12 @@ Build the auth instance once (memoize `getAuth`), and use the same cookie name o
|
|
|
169
177
|
| `RestoreSignedIn({ store, hasMarker, next?, pending? })` | Asks for the session once when the marker is missing; with `next`, goes there after. |
|
|
170
178
|
| `osuSignIn(next, signInPath?)` | The body for `authClient.signIn.social`: an error comes back to `/signin?next=<next>`. |
|
|
171
179
|
| `safeNextPath`, `signInHref`, `OSU_PROVIDER_ID` | The same as in `server` and `auth`, safe in the browser. |
|
|
180
|
+
| `createAuthComponents(authClient, kit)` | Since 0.2.0. The four components below with the client's `signIn.social` and `signOut` and the kit's `useAccount` and `markSignedOut` bound (`AuthComponents`; props `BoundSignInProps`, `BoundSignOutProps`, `BoundAccountMenuProps`, `BoundDeleteAccountProps`: each component's own minus what's bound). Export them from a `"use client"` module. |
|
|
181
|
+
| `SignInWithOsu({ next, signIn, signInPath?, label?, pendingLabel?, failedMessage? })` | Since 0.2.0. The large "Sign in with osu!" button. Calls `signIn(osuSignIn(next))`; an error (better-auth's `{ message }` or `{ error: { message } }`, or a throw) shows in a `role="alert"` line and the button comes back. `signInErrorMessage(error, fallback?)` reads it. |
|
|
182
|
+
| `SignOutButton({ signOut, onSignedOut, redirectTo?, variant?, className?, label?, pendingLabel? })` | Since 0.2.0. Signs out, runs `onSignedOut` only once the session is gone, then `router.replace(redirectTo ?? "/")` and `router.refresh()`. |
|
|
183
|
+
| `AccountMenu({ account, items, signOut, onSignedOut, avatarSrc?, signInPath?, signInLabel?, signOutLabel?, signOutRedirect? })` | Since 0.2.0. A sized blank while loading, a "Sign in" link back to this page when signed out, else ui's `HeaderMenu` with the avatar and name, `items` and Sign out. The avatar is a plain `<img>` (your CSP's img-src must allow a.ppy.sh and osu.ppy.sh); `avatarSrc` defaults to `osuAvatarSrc`. |
|
|
184
|
+
| `DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint?, homeHref?, homeLabel?, fetcher? })` | Since 0.2.0. ui's `TypeToConfirm` on the username, then `DELETE endpoint` (default `/api/account`) with `{ username }`. 204: `onDeleted`, "Your account is deleted." and home. Another 2xx: `onDeleted` and the answer's `notice` after that line, staying. A refusal shows `error.message` (else "Deleting failed (status)."); no answer, "Couldn't reach <appName>. Your account is still there." `deletes` is inline content in a `<p>`. |
|
|
185
|
+
| `osuAvatarSrc(url)`, `OSU_AVATAR_HOSTS` | Since 0.2.0. An osu! avatar URL on https when it's on a.ppy.sh or osu.ppy.sh (a bare path is osu.ppy.sh's), else null. |
|
|
172
186
|
|
|
173
187
|
### testing
|
|
174
188
|
|
|
@@ -197,7 +211,7 @@ For pools.haruhime.moe, `createMongo` runs `onConnect` (the privilege check, ind
|
|
|
197
211
|
|
|
198
212
|
## Compatibility
|
|
199
213
|
|
|
200
|
-
ES modules for Node 22.12+ on the server. `auth-react` also runs in browsers; its hook and component files keep `"use client"`, and it loads only `react
|
|
214
|
+
ES modules for Node 22.12+ on the server. `auth-react` also runs in browsers; its hook and component files keep `"use client"`, and it loads only `react`, `next/navigation.js` and `@haruhimemoe/ui`. `server` loads `node:crypto` for machine auth.
|
|
201
215
|
|
|
202
216
|
## License
|
|
203
217
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/AccountMenu.tsx
|
|
3
|
+
* @desc The header's account area. Client-side, so static pages stay static and read no cookies:
|
|
4
|
+
* the account comes from the app's useAccount, which asks only when the signed-in marker is
|
|
5
|
+
* there. Nothing to press while loading; "Sign in" (back to this page) when signed out;
|
|
6
|
+
* signed in, ui's HeaderMenu: the avatar (a plain img, so the app's CSP img-src must allow
|
|
7
|
+
* osu!'s avatar hosts) and name, the app's links, and Sign out. Escape,
|
|
8
|
+
* a click outside or on a link, or focus leaving it closes it. Moved from pools and bb
|
|
9
|
+
* (src/components/layout/AccountMenu.tsx), which differed only in the links.
|
|
10
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
11
|
+
* @created Mon Sep 28, 2026
|
|
12
|
+
* @modified Mon Sep 28, 2026
|
|
13
|
+
*/
|
|
14
|
+
import { type HeaderMenuItem } from "@haruhimemoe/ui";
|
|
15
|
+
import type { ReactNode } from "react";
|
|
16
|
+
import type { Account } from "./account-store.js";
|
|
17
|
+
/** AccountMenu's props. */
|
|
18
|
+
export type AccountMenuProps = {
|
|
19
|
+
/** The account (the app's useAccount()); createAuthComponents binds it. */
|
|
20
|
+
account: Account;
|
|
21
|
+
/** The menu's links, top to bottom; Sign out comes after them. */
|
|
22
|
+
items: readonly HeaderMenuItem[];
|
|
23
|
+
/** Ends the session; createAuthComponents binds it. */
|
|
24
|
+
signOut: () => Promise<unknown>;
|
|
25
|
+
/** Runs once the session is gone (markSignedOut); createAuthComponents binds it. */
|
|
26
|
+
onSignedOut: () => void;
|
|
27
|
+
/** Where the avatar is shown from, or null for none (default osuAvatarSrc). */
|
|
28
|
+
avatarSrc?: ((url: string | null) => string | null) | undefined;
|
|
29
|
+
/** The sign-in page (default /signin). */
|
|
30
|
+
signInPath?: string | undefined;
|
|
31
|
+
/** The sign-in link's text (default "Sign in"). */
|
|
32
|
+
signInLabel?: ReactNode;
|
|
33
|
+
/** The sign-out button's text (default "Sign out"). */
|
|
34
|
+
signOutLabel?: ReactNode;
|
|
35
|
+
/** Where sign-out goes (default "/"). */
|
|
36
|
+
signOutRedirect?: string | undefined;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* @function AccountMenu
|
|
40
|
+
* @param props {AccountMenuProps} the account, the links, the sign-out wiring and the words
|
|
41
|
+
* @returns {ReactNode} a sized blank while loading, the sign-in link when signed out, else the
|
|
42
|
+
* avatar menu with the links and Sign out
|
|
43
|
+
*/
|
|
44
|
+
export declare function AccountMenu({ account, items, signOut, onSignedOut, avatarSrc, signInPath, signInLabel, signOutLabel, signOutRedirect, }: AccountMenuProps): ReactNode;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/AccountMenu.tsx
|
|
3
|
+
* @desc The header's account area. Client-side, so static pages stay static and read no cookies:
|
|
4
|
+
* the account comes from the app's useAccount, which asks only when the signed-in marker is
|
|
5
|
+
* there. Nothing to press while loading; "Sign in" (back to this page) when signed out;
|
|
6
|
+
* signed in, ui's HeaderMenu: the avatar (a plain img, so the app's CSP img-src must allow
|
|
7
|
+
* osu!'s avatar hosts) and name, the app's links, and Sign out. Escape,
|
|
8
|
+
* a click outside or on a link, or focus leaving it closes it. Moved from pools and bb
|
|
9
|
+
* (src/components/layout/AccountMenu.tsx), which differed only in the links.
|
|
10
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
11
|
+
* @created Mon Sep 28, 2026
|
|
12
|
+
* @modified Mon Sep 28, 2026
|
|
13
|
+
*/
|
|
14
|
+
"use client";
|
|
15
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
16
|
+
import { HeaderMenu } from "@haruhimemoe/ui";
|
|
17
|
+
import { usePathname } from "next/navigation.js";
|
|
18
|
+
import { DEFAULT_SIGN_IN_PATH, signInHref } from "../server/safe-next.js";
|
|
19
|
+
import { osuAvatarSrc } from "./avatar.js";
|
|
20
|
+
import { SignOutButton } from "./SignOutButton.js";
|
|
21
|
+
/**
|
|
22
|
+
* @function AccountMenu
|
|
23
|
+
* @param props {AccountMenuProps} the account, the links, the sign-out wiring and the words
|
|
24
|
+
* @returns {ReactNode} a sized blank while loading, the sign-in link when signed out, else the
|
|
25
|
+
* avatar menu with the links and Sign out
|
|
26
|
+
*/
|
|
27
|
+
export function AccountMenu({ account, items, signOut, onSignedOut, avatarSrc = osuAvatarSrc, signInPath = DEFAULT_SIGN_IN_PATH, signInLabel = "Sign in", signOutLabel, signOutRedirect, }) {
|
|
28
|
+
const pathname = usePathname();
|
|
29
|
+
if (account.status === "loading")
|
|
30
|
+
return _jsx("span", { "aria-hidden": "true", className: "block h-7 w-16" });
|
|
31
|
+
if (account.status === "signed-out") {
|
|
32
|
+
return (_jsx("a", { href: signInHref(pathname || "/", signInPath), className: "font-bold text-c3 text-sm transition-colors hover:text-c1", children: signInLabel }));
|
|
33
|
+
}
|
|
34
|
+
const avatar = avatarSrc(account.user.avatarUrl);
|
|
35
|
+
return (_jsx(HeaderMenu, { label: _jsxs(_Fragment, { children: [avatar ? (_jsx("img", { src: avatar, alt: "", width: 28, height: 28, className: "rounded-full" })) : null, _jsx("span", { children: account.user.username })] }), items: items, children: _jsx(SignOutButton, { signOut: signOut, onSignedOut: onSignedOut, redirectTo: signOutRedirect, label: signOutLabel, variant: "ghost", className: "h-auto justify-start rounded px-3 py-2" }) }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/DeleteAccountForm.tsx
|
|
3
|
+
* @desc "Delete my account". The confirmation is built into the page (ui's TypeToConfirm, no
|
|
4
|
+
* confirm() dialog): the button stays off until the osu! username is typed exactly, then
|
|
5
|
+
* one DELETE to the app's endpoint carries it. 204 means gone: signed out, the page says
|
|
6
|
+
* so and goes home. Another 2xx is gone too but stays, to show the answer's `notice` (work
|
|
7
|
+
* still waiting elsewhere). A refusal says the server's message, and no answer says the app
|
|
8
|
+
* couldn't be reached; nothing was deleted. The form never comes back once it's gone.
|
|
9
|
+
* Moved from pools and bb (src/components/account/DeleteAccountForm.tsx).
|
|
10
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
11
|
+
* @created Mon Sep 28, 2026
|
|
12
|
+
* @modified Mon Sep 28, 2026
|
|
13
|
+
*/
|
|
14
|
+
import { type ReactNode } from "react";
|
|
15
|
+
/** DeleteAccountForm's props. */
|
|
16
|
+
export type DeleteAccountFormProps = {
|
|
17
|
+
/** The signed-in osu! username, typed to confirm and sent as `{ username }`. */
|
|
18
|
+
username: string;
|
|
19
|
+
/** The app's name, for "Couldn't reach <appName>. Your account is still there." */
|
|
20
|
+
appName: string;
|
|
21
|
+
/** What goes, above the field ("This deletes your account and every pool you own…"). */
|
|
22
|
+
deletes: ReactNode;
|
|
23
|
+
/** Runs once the account is gone (markSignedOut); createAuthComponents binds it. */
|
|
24
|
+
onDeleted: () => void;
|
|
25
|
+
/** The DELETE endpoint (default "/api/account"). */
|
|
26
|
+
endpoint?: string | undefined;
|
|
27
|
+
/** Where to go after (default "/"). */
|
|
28
|
+
homeHref?: string | undefined;
|
|
29
|
+
/** The link home's text once deleted (default "Go to the home page"). */
|
|
30
|
+
homeLabel?: ReactNode;
|
|
31
|
+
/** Test seam (default: fetch). */
|
|
32
|
+
fetcher?: typeof fetch | undefined;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* @function DeleteAccountForm
|
|
36
|
+
* @param props {DeleteAccountFormProps} the username, the app's name, what goes, the endpoint
|
|
37
|
+
* and what to do after
|
|
38
|
+
* @returns {ReactNode} the typed-name confirmation, or what happened
|
|
39
|
+
*/
|
|
40
|
+
export declare function DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint, homeHref, homeLabel, fetcher, }: DeleteAccountFormProps): ReactNode;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/DeleteAccountForm.tsx
|
|
3
|
+
* @desc "Delete my account". The confirmation is built into the page (ui's TypeToConfirm, no
|
|
4
|
+
* confirm() dialog): the button stays off until the osu! username is typed exactly, then
|
|
5
|
+
* one DELETE to the app's endpoint carries it. 204 means gone: signed out, the page says
|
|
6
|
+
* so and goes home. Another 2xx is gone too but stays, to show the answer's `notice` (work
|
|
7
|
+
* still waiting elsewhere). A refusal says the server's message, and no answer says the app
|
|
8
|
+
* couldn't be reached; nothing was deleted. The form never comes back once it's gone.
|
|
9
|
+
* Moved from pools and bb (src/components/account/DeleteAccountForm.tsx).
|
|
10
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
11
|
+
* @created Mon Sep 28, 2026
|
|
12
|
+
* @modified Mon Sep 28, 2026
|
|
13
|
+
*/
|
|
14
|
+
"use client";
|
|
15
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
16
|
+
import { buttonClasses, TypeToConfirm } from "@haruhimemoe/ui";
|
|
17
|
+
import { useRouter } from "next/navigation.js";
|
|
18
|
+
import { useId, useState } from "react";
|
|
19
|
+
const messageOf = async (response) => {
|
|
20
|
+
const fallback = `Deleting failed (${response.status}).`;
|
|
21
|
+
try {
|
|
22
|
+
const body = (await response.json());
|
|
23
|
+
return body.error?.message ?? fallback;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* @function DeleteAccountForm
|
|
31
|
+
* @param props {DeleteAccountFormProps} the username, the app's name, what goes, the endpoint
|
|
32
|
+
* and what to do after
|
|
33
|
+
* @returns {ReactNode} the typed-name confirmation, or what happened
|
|
34
|
+
*/
|
|
35
|
+
export function DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint = "/api/account", homeHref = "/", homeLabel = "Go to the home page", fetcher = fetch, }) {
|
|
36
|
+
const router = useRouter();
|
|
37
|
+
const id = useId();
|
|
38
|
+
const [error, setError] = useState(null);
|
|
39
|
+
// Once it's gone: what more to say. The form doesn't come back, so no second delete.
|
|
40
|
+
const [done, setDone] = useState(null);
|
|
41
|
+
const remove = async () => {
|
|
42
|
+
setError(null);
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetcher(endpoint, {
|
|
45
|
+
method: "DELETE",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify({ username }),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
setError(await messageOf(response));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
onDeleted();
|
|
54
|
+
if (response.status === 204) {
|
|
55
|
+
setDone("");
|
|
56
|
+
router.push(homeHref);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const body = (await response.json().catch(() => ({})));
|
|
60
|
+
setDone(typeof body.notice === "string" ? body.notice : "");
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
setError(`Couldn't reach ${appName}. Your account is still there.`);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
if (done !== null) {
|
|
67
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { role: "status", className: "text-c2 text-sm", children: `Your account is deleted. ${done}`.trim() }), _jsx("a", { href: homeHref, className: buttonClasses({ variant: "secondary", className: "self-start" }), children: homeLabel })] }));
|
|
68
|
+
}
|
|
69
|
+
return (_jsx(TypeToConfirm, { id: id, expected: username, submitLabel: "Delete my account", pendingLabel: "Deleting\u2026", error: error, onConfirm: remove, children: _jsx("p", { className: "text-c2 text-sm", children: deletes }) }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/SignInWithOsu.tsx
|
|
3
|
+
* @desc "Sign in with osu!": starts the OAuth redirect through the app's better-auth client, then
|
|
4
|
+
* lands on `next`, or back on /signin?next=<next>&error=<code> when osu! says no. A call
|
|
5
|
+
* that can't start says so under the button (role="alert") and lets the visitor try again.
|
|
6
|
+
* Styled with @haruhimemoe/ui. Moved from packs and pools
|
|
7
|
+
* (src/components/auth/SignInWithOsu.tsx), where only the error reading differed.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Mon Sep 28, 2026
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
11
|
+
*/
|
|
12
|
+
import { type ReactNode } from "react";
|
|
13
|
+
import { type OsuSignIn } from "./sign-in.js";
|
|
14
|
+
/** The better-auth call that starts sign-in (`(body) => authClient.signIn.social(body)`). */
|
|
15
|
+
export type StartSignIn = (body: OsuSignIn) => Promise<unknown>;
|
|
16
|
+
/** SignInWithOsu's props. */
|
|
17
|
+
export type SignInWithOsuProps = {
|
|
18
|
+
/** Where to land after signing in (already checked with safeNextPath). */
|
|
19
|
+
next: string;
|
|
20
|
+
/** Starts sign-in; createAuthComponents binds the app's client. */
|
|
21
|
+
signIn: StartSignIn;
|
|
22
|
+
/** The sign-in page errors come back to (default /signin). */
|
|
23
|
+
signInPath?: string | undefined;
|
|
24
|
+
/** The button's text (default "Sign in with osu!"). */
|
|
25
|
+
label?: ReactNode;
|
|
26
|
+
/** The button's text while osu! opens (default "Opening osu!…"). */
|
|
27
|
+
pendingLabel?: ReactNode;
|
|
28
|
+
/** Said when sign-in can't start and the error has no message. */
|
|
29
|
+
failedMessage?: string | undefined;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* @function signInErrorMessage
|
|
33
|
+
* @param error {unknown} better-auth's error (`{ message }`, or `{ error: { message } }`)
|
|
34
|
+
* @param fallback {string} said when it carries no message
|
|
35
|
+
* @returns {string} the message to show
|
|
36
|
+
*/
|
|
37
|
+
export declare const signInErrorMessage: (error: unknown, fallback?: string) => string;
|
|
38
|
+
/**
|
|
39
|
+
* @function SignInWithOsu
|
|
40
|
+
* @param props {SignInWithOsuProps} `next`, the sign-in call, and the words
|
|
41
|
+
* @returns {ReactNode} the large "Sign in with osu!" button, with an alert when it can't start
|
|
42
|
+
*/
|
|
43
|
+
export declare function SignInWithOsu({ next, signIn, signInPath, label, pendingLabel, failedMessage, }: SignInWithOsuProps): ReactNode;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/SignInWithOsu.tsx
|
|
3
|
+
* @desc "Sign in with osu!": starts the OAuth redirect through the app's better-auth client, then
|
|
4
|
+
* lands on `next`, or back on /signin?next=<next>&error=<code> when osu! says no. A call
|
|
5
|
+
* that can't start says so under the button (role="alert") and lets the visitor try again.
|
|
6
|
+
* Styled with @haruhimemoe/ui. Moved from packs and pools
|
|
7
|
+
* (src/components/auth/SignInWithOsu.tsx), where only the error reading differed.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Mon Sep 28, 2026
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
11
|
+
*/
|
|
12
|
+
"use client";
|
|
13
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
import { Button } from "@haruhimemoe/ui";
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { osuSignIn } from "./sign-in.js";
|
|
17
|
+
const FAILED = "Couldn't start osu! sign-in. Try again.";
|
|
18
|
+
/**
|
|
19
|
+
* @function signInErrorMessage
|
|
20
|
+
* @param error {unknown} better-auth's error (`{ message }`, or `{ error: { message } }`)
|
|
21
|
+
* @param fallback {string} said when it carries no message
|
|
22
|
+
* @returns {string} the message to show
|
|
23
|
+
*/
|
|
24
|
+
export const signInErrorMessage = (error, fallback = FAILED) => {
|
|
25
|
+
if (typeof error !== "object" || error === null)
|
|
26
|
+
return fallback;
|
|
27
|
+
const { message, error: nested } = error;
|
|
28
|
+
if (typeof message === "string" && message)
|
|
29
|
+
return message;
|
|
30
|
+
if (typeof nested?.message === "string" && nested.message)
|
|
31
|
+
return nested.message;
|
|
32
|
+
return fallback;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* @function SignInWithOsu
|
|
36
|
+
* @param props {SignInWithOsuProps} `next`, the sign-in call, and the words
|
|
37
|
+
* @returns {ReactNode} the large "Sign in with osu!" button, with an alert when it can't start
|
|
38
|
+
*/
|
|
39
|
+
export function SignInWithOsu({ next, signIn, signInPath, label = "Sign in with osu!", pendingLabel = "Opening osu!…", failedMessage = FAILED, }) {
|
|
40
|
+
const [pending, setPending] = useState(false);
|
|
41
|
+
const [error, setError] = useState(null);
|
|
42
|
+
const onClick = async () => {
|
|
43
|
+
setPending(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
try {
|
|
46
|
+
// better-auth adds &error=<code> to the error URL; /signin explains it and keeps `next`.
|
|
47
|
+
const result = (await signIn(osuSignIn(next, signInPath)));
|
|
48
|
+
if (!result?.error)
|
|
49
|
+
return;
|
|
50
|
+
setError(signInErrorMessage(result.error, failedMessage));
|
|
51
|
+
}
|
|
52
|
+
catch (cause) {
|
|
53
|
+
setError(signInErrorMessage(cause, failedMessage));
|
|
54
|
+
}
|
|
55
|
+
setPending(false);
|
|
56
|
+
};
|
|
57
|
+
return (_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(Button, { size: "lg", onClick: onClick, disabled: pending, children: pending ? pendingLabel : label }), error ? (_jsx("p", { role: "alert", className: "font-bold text-rose-300 text-sm", children: error })) : null] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/SignOutButton.tsx
|
|
3
|
+
* @desc Signs out, forgets the signed-in marker once the session is really gone (so the header
|
|
4
|
+
* flips without a reload), then goes home and refreshes server components. A failed
|
|
5
|
+
* sign-out still goes home, marker kept. Styled with @haruhimemoe/ui. Moved from packs and
|
|
6
|
+
* pools (src/components/auth/SignOutButton.tsx).
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
import { type ButtonVariant } from "@haruhimemoe/ui";
|
|
12
|
+
import { type ReactNode } from "react";
|
|
13
|
+
/** SignOutButton's props. */
|
|
14
|
+
export type SignOutButtonProps = {
|
|
15
|
+
/** Ends the session (`() => authClient.signOut()`); createAuthComponents binds it. */
|
|
16
|
+
signOut: () => Promise<unknown>;
|
|
17
|
+
/** Runs once the session is gone (the account kit's markSignedOut). */
|
|
18
|
+
onSignedOut: () => void;
|
|
19
|
+
/** Where to go after (default "/"). */
|
|
20
|
+
redirectTo?: string | undefined;
|
|
21
|
+
/** The button's look (default "secondary"). */
|
|
22
|
+
variant?: ButtonVariant | undefined;
|
|
23
|
+
/** Classes for the button. */
|
|
24
|
+
className?: string | undefined;
|
|
25
|
+
/** The button's text (default "Sign out"). */
|
|
26
|
+
label?: ReactNode;
|
|
27
|
+
/** The button's text while signing out (default "Signing out…"). */
|
|
28
|
+
pendingLabel?: ReactNode;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* @function SignOutButton
|
|
32
|
+
* @param props {SignOutButtonProps} the sign-out call, what to do after, where to go, the look
|
|
33
|
+
* @returns {ReactNode} a button that signs out, tells the header, and goes to `redirectTo`
|
|
34
|
+
*/
|
|
35
|
+
export declare function SignOutButton({ signOut, onSignedOut, redirectTo, variant, className, label, pendingLabel, }: SignOutButtonProps): ReactNode;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/SignOutButton.tsx
|
|
3
|
+
* @desc Signs out, forgets the signed-in marker once the session is really gone (so the header
|
|
4
|
+
* flips without a reload), then goes home and refreshes server components. A failed
|
|
5
|
+
* sign-out still goes home, marker kept. Styled with @haruhimemoe/ui. Moved from packs and
|
|
6
|
+
* pools (src/components/auth/SignOutButton.tsx).
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
"use client";
|
|
12
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
import { Button } from "@haruhimemoe/ui";
|
|
14
|
+
import { useRouter } from "next/navigation.js";
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
/**
|
|
17
|
+
* @function SignOutButton
|
|
18
|
+
* @param props {SignOutButtonProps} the sign-out call, what to do after, where to go, the look
|
|
19
|
+
* @returns {ReactNode} a button that signs out, tells the header, and goes to `redirectTo`
|
|
20
|
+
*/
|
|
21
|
+
export function SignOutButton({ signOut, onSignedOut, redirectTo = "/", variant = "secondary", className, label = "Sign out", pendingLabel = "Signing out…", }) {
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const [pending, setPending] = useState(false);
|
|
24
|
+
const onClick = async () => {
|
|
25
|
+
setPending(true);
|
|
26
|
+
await signOut().then(onSignedOut, () => undefined);
|
|
27
|
+
router.replace(redirectTo);
|
|
28
|
+
router.refresh();
|
|
29
|
+
};
|
|
30
|
+
return (_jsx(Button, { variant: variant, className: className, onClick: onClick, disabled: pending, children: pending ? pendingLabel : label }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/auth-components.tsx
|
|
3
|
+
* @desc createAuthComponents: SignInWithOsu, SignOutButton, AccountMenu and DeleteAccountForm
|
|
4
|
+
* with the app's better-auth client and account kit already bound, so what's left is plain
|
|
5
|
+
* data a server page can pass. Export them from the app's own "use client" module, next to
|
|
6
|
+
* createAccount's.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
import type { ReactNode } from "react";
|
|
12
|
+
import { type AccountMenuProps } from "./AccountMenu.js";
|
|
13
|
+
import type { Account } from "./account-store.js";
|
|
14
|
+
import { type DeleteAccountFormProps } from "./DeleteAccountForm.js";
|
|
15
|
+
import { type SignInWithOsuProps, type StartSignIn } from "./SignInWithOsu.js";
|
|
16
|
+
import { type SignOutButtonProps } from "./SignOutButton.js";
|
|
17
|
+
/** The part of a better-auth client the components call. */
|
|
18
|
+
export type AuthUiClient = {
|
|
19
|
+
signIn: {
|
|
20
|
+
social: StartSignIn;
|
|
21
|
+
};
|
|
22
|
+
signOut: () => Promise<unknown>;
|
|
23
|
+
};
|
|
24
|
+
/** The part of createAccount's kit the components use. */
|
|
25
|
+
export type AuthUiAccount = {
|
|
26
|
+
useAccount: () => Account;
|
|
27
|
+
markSignedOut: () => void;
|
|
28
|
+
};
|
|
29
|
+
/** The bound components' props: each component's own, minus what's bound. */
|
|
30
|
+
export type BoundSignInProps = Omit<SignInWithOsuProps, "signIn">;
|
|
31
|
+
/** SignOutButton's props minus the bound sign-out. */
|
|
32
|
+
export type BoundSignOutProps = Omit<SignOutButtonProps, "signOut" | "onSignedOut">;
|
|
33
|
+
/** AccountMenu's props minus the bound account and sign-out. */
|
|
34
|
+
export type BoundAccountMenuProps = Omit<AccountMenuProps, "account" | "signOut" | "onSignedOut">;
|
|
35
|
+
/** DeleteAccountForm's props minus the bound onDeleted. */
|
|
36
|
+
export type BoundDeleteAccountProps = Omit<DeleteAccountFormProps, "onDeleted">;
|
|
37
|
+
/** What createAuthComponents returns. */
|
|
38
|
+
export type AuthComponents = {
|
|
39
|
+
SignInWithOsu: (props: BoundSignInProps) => ReactNode;
|
|
40
|
+
SignOutButton: (props: BoundSignOutProps) => ReactNode;
|
|
41
|
+
AccountMenu: (props: BoundAccountMenuProps) => ReactNode;
|
|
42
|
+
DeleteAccountForm: (props: BoundDeleteAccountProps) => ReactNode;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* @function createAuthComponents
|
|
46
|
+
* @param client {AuthUiClient} the app's better-auth client (signIn.social and signOut)
|
|
47
|
+
* @param account {AuthUiAccount} createAccount's kit (useAccount and markSignedOut)
|
|
48
|
+
* @returns {AuthComponents} the four components with the client and account bound
|
|
49
|
+
*/
|
|
50
|
+
export declare const createAuthComponents: (client: AuthUiClient, account: AuthUiAccount) => AuthComponents;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/auth-components.tsx
|
|
3
|
+
* @desc createAuthComponents: SignInWithOsu, SignOutButton, AccountMenu and DeleteAccountForm
|
|
4
|
+
* with the app's better-auth client and account kit already bound, so what's left is plain
|
|
5
|
+
* data a server page can pass. Export them from the app's own "use client" module, next to
|
|
6
|
+
* createAccount's.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
"use client";
|
|
12
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
import { AccountMenu } from "./AccountMenu.js";
|
|
14
|
+
import { DeleteAccountForm } from "./DeleteAccountForm.js";
|
|
15
|
+
import { SignInWithOsu } from "./SignInWithOsu.js";
|
|
16
|
+
import { SignOutButton } from "./SignOutButton.js";
|
|
17
|
+
/**
|
|
18
|
+
* @function createAuthComponents
|
|
19
|
+
* @param client {AuthUiClient} the app's better-auth client (signIn.social and signOut)
|
|
20
|
+
* @param account {AuthUiAccount} createAccount's kit (useAccount and markSignedOut)
|
|
21
|
+
* @returns {AuthComponents} the four components with the client and account bound
|
|
22
|
+
*/
|
|
23
|
+
export const createAuthComponents = (client, account) => {
|
|
24
|
+
const signIn = (body) => client.signIn.social(body);
|
|
25
|
+
const signOut = () => client.signOut();
|
|
26
|
+
const onSignedOut = () => account.markSignedOut();
|
|
27
|
+
return {
|
|
28
|
+
SignInWithOsu: (props) => _jsx(SignInWithOsu, { ...props, signIn: signIn }),
|
|
29
|
+
SignOutButton: (props) => (_jsx(SignOutButton, { ...props, signOut: signOut, onSignedOut: onSignedOut })),
|
|
30
|
+
AccountMenu: function BoundAccountMenu(props) {
|
|
31
|
+
const current = account.useAccount();
|
|
32
|
+
return (_jsx(AccountMenu, { ...props, account: current, signOut: signOut, onSignedOut: onSignedOut }));
|
|
33
|
+
},
|
|
34
|
+
DeleteAccountForm: (props) => _jsx(DeleteAccountForm, { ...props, onDeleted: onSignedOut }),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/avatar.ts
|
|
3
|
+
* @desc Where an osu! avatar is shown from. osu!'s API sends a.ppy.sh URLs, and the guest avatar
|
|
4
|
+
* on osu.ppy.sh (which osu-web can send as a bare path). An app's CSP and next/image
|
|
5
|
+
* config allow those two hosts, so any other URL shows no avatar rather than a broken one.
|
|
6
|
+
* Pure and browser-safe. Moved from pools and bb (src/utils/avatar.ts, identical).
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
/** osu!'s avatar hosts. */
|
|
12
|
+
export declare const OSU_AVATAR_HOSTS: ReadonlySet<string>;
|
|
13
|
+
/**
|
|
14
|
+
* @function osuAvatarSrc
|
|
15
|
+
* @param url {string | null | undefined} the avatar URL osu! sent
|
|
16
|
+
* @returns {string | null} it on https when it's on a.ppy.sh or osu.ppy.sh (a bare path read as
|
|
17
|
+
* osu.ppy.sh's), else null
|
|
18
|
+
*/
|
|
19
|
+
export declare const osuAvatarSrc: (url: string | null | undefined) => string | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/auth-react/avatar.ts
|
|
3
|
+
* @desc Where an osu! avatar is shown from. osu!'s API sends a.ppy.sh URLs, and the guest avatar
|
|
4
|
+
* on osu.ppy.sh (which osu-web can send as a bare path). An app's CSP and next/image
|
|
5
|
+
* config allow those two hosts, so any other URL shows no avatar rather than a broken one.
|
|
6
|
+
* Pure and browser-safe. Moved from pools and bb (src/utils/avatar.ts, identical).
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
/** osu!'s avatar hosts. */
|
|
12
|
+
export const OSU_AVATAR_HOSTS = new Set(["a.ppy.sh", "osu.ppy.sh"]);
|
|
13
|
+
/** A bare path is osu-web's own. */
|
|
14
|
+
const OSU_WEB = "https://osu.ppy.sh";
|
|
15
|
+
/**
|
|
16
|
+
* @function osuAvatarSrc
|
|
17
|
+
* @param url {string | null | undefined} the avatar URL osu! sent
|
|
18
|
+
* @returns {string | null} it on https when it's on a.ppy.sh or osu.ppy.sh (a bare path read as
|
|
19
|
+
* osu.ppy.sh's), else null
|
|
20
|
+
*/
|
|
21
|
+
export const osuAvatarSrc = (url) => {
|
|
22
|
+
if (!url)
|
|
23
|
+
return null;
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = new URL(url, OSU_WEB);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
32
|
+
return null;
|
|
33
|
+
if (!OSU_AVATAR_HOSTS.has(parsed.hostname) || parsed.port !== "")
|
|
34
|
+
return null;
|
|
35
|
+
parsed.protocol = "https:";
|
|
36
|
+
return parsed.href;
|
|
37
|
+
};
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* @file src/auth-react/index.ts
|
|
3
3
|
* @desc @haruhimemoe/next-kit/auth-react: the browser side of osu! sign-in. The signed-in marker
|
|
4
4
|
* cookie, the account store and useAccount, RestoreSignedIn, what the sign-in button
|
|
5
|
-
* sends,
|
|
5
|
+
* sends, where to go after sign-in, and the account components (sign in, sign out, the
|
|
6
|
+
* header's account menu, delete my account), styled with @haruhimemoe/ui. No "use client" here: the hook and component
|
|
6
7
|
* files carry it, so a server page can still call safeNextPath from this entry point.
|
|
7
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
9
|
* @created Mon Sep 28, 2026
|
|
@@ -10,8 +11,14 @@
|
|
|
10
11
|
*/
|
|
11
12
|
export { OSU_PROVIDER_ID } from "../auth/osu-id.js";
|
|
12
13
|
export { DEFAULT_SIGN_IN_PATH, type SafeNextOptions, safeNextPath, signInHref, } from "../server/safe-next.js";
|
|
14
|
+
export { AccountMenu, type AccountMenuProps, } from "./AccountMenu.js";
|
|
13
15
|
export { type Account, type AccountDeps, type AccountStore, createAccountStore, LOADING, type SessionData, } from "./account-store.js";
|
|
16
|
+
export { type AuthComponents, type AuthUiAccount, type AuthUiClient, type BoundAccountMenuProps, type BoundDeleteAccountProps, type BoundSignInProps, type BoundSignOutProps, createAuthComponents, } from "./auth-components.js";
|
|
17
|
+
export { OSU_AVATAR_HOSTS, osuAvatarSrc } from "./avatar.js";
|
|
18
|
+
export { DeleteAccountForm, type DeleteAccountFormProps } from "./DeleteAccountForm.js";
|
|
14
19
|
export { createSignedInMarker, markerMaxAge, type SignedInMarker } from "./marker.js";
|
|
15
20
|
export { RestoreSignedIn, type RestoreSignedInProps } from "./RestoreSignedIn.js";
|
|
21
|
+
export { SignInWithOsu, type SignInWithOsuProps, type StartSignIn, signInErrorMessage, } from "./SignInWithOsu.js";
|
|
22
|
+
export { SignOutButton, type SignOutButtonProps } from "./SignOutButton.js";
|
|
16
23
|
export { type OsuSignIn, osuSignIn } from "./sign-in.js";
|
|
17
24
|
export { type AccountKit, type BoundRestoreProps, createAccount, type SessionClient, sessionFetcher, useAccount, } from "./use-account.js";
|
package/dist/auth-react/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* @file src/auth-react/index.ts
|
|
3
3
|
* @desc @haruhimemoe/next-kit/auth-react: the browser side of osu! sign-in. The signed-in marker
|
|
4
4
|
* cookie, the account store and useAccount, RestoreSignedIn, what the sign-in button
|
|
5
|
-
* sends,
|
|
5
|
+
* sends, where to go after sign-in, and the account components (sign in, sign out, the
|
|
6
|
+
* header's account menu, delete my account), styled with @haruhimemoe/ui. No "use client" here: the hook and component
|
|
6
7
|
* files carry it, so a server page can still call safeNextPath from this entry point.
|
|
7
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
9
|
* @created Mon Sep 28, 2026
|
|
@@ -10,8 +11,14 @@
|
|
|
10
11
|
*/
|
|
11
12
|
export { OSU_PROVIDER_ID } from "../auth/osu-id.js";
|
|
12
13
|
export { DEFAULT_SIGN_IN_PATH, safeNextPath, signInHref, } from "../server/safe-next.js";
|
|
14
|
+
export { AccountMenu, } from "./AccountMenu.js";
|
|
13
15
|
export { createAccountStore, LOADING, } from "./account-store.js";
|
|
16
|
+
export { createAuthComponents, } from "./auth-components.js";
|
|
17
|
+
export { OSU_AVATAR_HOSTS, osuAvatarSrc } from "./avatar.js";
|
|
18
|
+
export { DeleteAccountForm } from "./DeleteAccountForm.js";
|
|
14
19
|
export { createSignedInMarker, markerMaxAge } from "./marker.js";
|
|
15
20
|
export { RestoreSignedIn } from "./RestoreSignedIn.js";
|
|
21
|
+
export { SignInWithOsu, signInErrorMessage, } from "./SignInWithOsu.js";
|
|
22
|
+
export { SignOutButton } from "./SignOutButton.js";
|
|
16
23
|
export { osuSignIn } from "./sign-in.js";
|
|
17
24
|
export { createAccount, sessionFetcher, useAccount, } from "./use-account.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haruhimemoe/next-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The Next.js server plumbing packs.haruhime.moe and pools.haruhime.moe share: JSON route helpers, rate limits and budgets in MongoDB, bearer machine auth, zod env parsing, a connect-once MongoDB client with safe index builds, better-auth with osu! sign-in, and the signed-in marker and account store for the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
78
|
"@haruhimemoe/osu": "^0.2.0 || ^0.3.0",
|
|
79
|
+
"@haruhimemoe/ui": "^0.5.0",
|
|
79
80
|
"better-auth": "^1.7.5",
|
|
80
81
|
"mongodb": "^7.6.0",
|
|
81
82
|
"mongodb-memory-server": "^11.3.0",
|
|
@@ -90,6 +91,9 @@
|
|
|
90
91
|
"@haruhimemoe/osu": {
|
|
91
92
|
"optional": true
|
|
92
93
|
},
|
|
94
|
+
"@haruhimemoe/ui": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
93
97
|
"better-auth": {
|
|
94
98
|
"optional": true
|
|
95
99
|
},
|
|
@@ -118,6 +122,7 @@
|
|
|
118
122
|
"devDependencies": {
|
|
119
123
|
"@biomejs/biome": "2.5.14",
|
|
120
124
|
"@haruhimemoe/osu": "0.2.0",
|
|
125
|
+
"@haruhimemoe/ui": "0.5.0",
|
|
121
126
|
"@testing-library/dom": "10.4.2",
|
|
122
127
|
"@testing-library/react": "16.3.3",
|
|
123
128
|
"@types/node": "26.6.2",
|