Compare commits

..

4 Commits

Author SHA1 Message Date
Henry
1b4b0280e3 chore: add translations 2025-07-09 21:56:05 +01:00
Henry
cc4a09914c feat: add loading state to update board slug button 2025-07-09 21:24:53 +01:00
Henry
09e2df708a feat: add edit workspace url button 2025-07-09 14:22:57 +01:00
Henry
a81594124e feat: add update board slug button 2025-07-09 13:37:10 +01:00
8 changed files with 27 additions and 84 deletions

4
.vscode/launch.json vendored
View File

@@ -6,7 +6,7 @@
"type": "node-terminal", "type": "node-terminal",
"request": "launch", "request": "launch",
"command": "pnpm dev", "command": "pnpm dev",
"cwd": "${workspaceFolder}/apps/web", "cwd": "${workspaceFolder}/apps/nextjs",
"skipFiles": ["<node_internals>/**"], "skipFiles": ["<node_internals>/**"],
"sourceMaps": true, "sourceMaps": true,
"sourceMapPathOverrides": { "sourceMapPathOverrides": {
@@ -14,4 +14,4 @@
} }
} }
] ]
} }

View File

@@ -5,23 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased](https://github.com/kanbn/kan/compare/v0.2.4...HEAD) ## [Unreleased](https://github.com/kanbn/kan/compare/v0.2.3...HEAD)
## [0.2.4](https://github.com/kanbn/kan/compare/v0.2.3...v0.2.4) - 2025-01-14
### Added
- Button to view and update board public URL directly from board page
- Redirect from root to login page for self-hosted instances
### Changed
- Updated all label router endpoints to return consistent structure
- Added verbose error logging to magic link invitation process
### Fixed
- Updated tRPC dependencies and added OpenAPI meta to providers endpoint
## [0.2.3](https://github.com/kanbn/kan/compare/v0.2.2...v0.2.3) - 2025-07-07 ## [0.2.3](https://github.com/kanbn/kan/compare/v0.2.2...v0.2.3) - 2025-07-07

View File

@@ -259,7 +259,6 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
} }
return ( return (
<Button <Button
key={key}
onClick={() => handleLoginWithProvider(key as SocialProvider)} onClick={() => handleLoginWithProvider(key as SocialProvider)}
isLoading={isLoginWithProviderPending === key} isLoading={isLoginWithProviderPending === key}
iconLeft={<provider.icon />} iconLeft={<provider.icon />}

View File

@@ -1,18 +0,0 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { env } from "next-runtime-env";
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === "/") {
if (env("NEXT_PUBLIC_KAN_ENV") !== "cloud") {
const loginUrl = new URL("/login", request.url);
return NextResponse.redirect(loginUrl);
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/"],
};

View File

@@ -3,8 +3,6 @@ import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env"; import { env } from "next-runtime-env";
import { useState } from "react"; import { useState } from "react";
import { generateUID } from "@kan/shared/utils";
import { usePopup } from "~/providers/popup"; import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { getAvatarUrl } from "~/utils/helpers"; import { getAvatarUrl } from "~/utils/helpers";
@@ -60,7 +58,7 @@ export default function Avatar({
} }
const fileExt = file.name.split(".").pop(); const fileExt = file.name.split(".").pop();
const fileName = `${userId}/avatar-${generateUID()}.${fileExt}`; const fileName = `${userId}/avatar.${fileExt}`;
setUploading(true); setUploading(true);

View File

@@ -1,6 +1,7 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { authClient } from "@kan/auth/client";
import * as memberRepo from "@kan/db/repository/member.repo"; import * as memberRepo from "@kan/db/repository/member.repo";
import * as userRepo from "@kan/db/repository/user.repo"; import * as userRepo from "@kan/db/repository/user.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo";
@@ -77,22 +78,16 @@ export const memberRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
}); });
const { status } = await ctx.auth.api.signInMagicLink({ const { error } = await authClient.signIn.magicLink({
email: input.email, email: input.email,
callbackURL: `/boards?type=invite&memberPublicId=${invite.publicId}`, callbackURL: `/boards?type=invite&memberPublicId=${invite.publicId}`,
}); });
if (!status) { if (error)
console.error("Failed to send magic link invitation:", {
email: input.email,
callbackURL: `/boards?type=invite&memberPublicId=${invite.publicId}`,
});
throw new TRPCError({ throw new TRPCError({
message: `Failed to send magic link invitation to user with email ${input.email}.`, message: `Failed to send magic link to user with email ${input.email}`,
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
}); });
}
return invite; return invite;
}), }),

View File

@@ -20,73 +20,58 @@ export interface User {
stripeCustomerId?: string | null | undefined; stripeCustomerId?: string | null | undefined;
} }
const createAuthWithHeaders = (
auth: ReturnType<typeof initAuth>,
headers: Headers,
) => {
return {
api: {
getSession: () => auth.api.getSession({ headers }),
signInMagicLink: (input: { email: string; callbackURL: string }) =>
auth.api.signInMagicLink({
headers,
body: { email: input.email, callbackURL: input.callbackURL },
}),
},
};
};
interface CreateContextOptions { interface CreateContextOptions {
user: User | null | undefined; user: User | null | undefined;
db: dbClient; db: dbClient;
auth: ReturnType<typeof createAuthWithHeaders>;
} }
export const createInnerTRPCContext = (opts: CreateContextOptions) => { export const createInnerTRPCContext = (opts: CreateContextOptions) => {
return { return {
user: opts.user, user: opts.user,
db: opts.db, db: opts.db,
auth: opts.auth,
}; };
}; };
export const createTRPCContext = async ({ req }: CreateNextContextOptions) => { export const createTRPCContext = async ({ req }: CreateNextContextOptions) => {
const db = createDrizzleClient(); const db = createDrizzleClient();
const baseAuth = initAuth(db); const auth = initAuth(db);
const headers = new Headers(req.headers as Record<string, string>);
const auth = createAuthWithHeaders(baseAuth, headers);
const session = await auth.api.getSession(); const session = await auth.api.getSession({
// @ts-expect-error
headers: new Headers(req.headers),
});
return createInnerTRPCContext({ db, user: session?.user, auth }); return createInnerTRPCContext({ db, user: session?.user });
}; };
export const createNextApiContext = async (req: NextApiRequest) => { export const createNextApiContext = async (req: NextApiRequest) => {
const db = createDrizzleClient(); const db = createDrizzleClient();
const baseAuth = initAuth(db); const auth = initAuth(db);
const headers = new Headers(req.headers as Record<string, string>);
const auth = createAuthWithHeaders(baseAuth, headers);
const session = await auth.api.getSession(); const session = await auth.api.getSession({
// @ts-expect-error
headers: new Headers(req.headers),
});
return createInnerTRPCContext({ db, user: session?.user, auth }); return createInnerTRPCContext({ db, user: session?.user });
}; };
export const createRESTContext = async ({ req }: CreateNextContextOptions) => { export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
const db = createDrizzleClient(); const db = createDrizzleClient();
const baseAuth = initAuth(db); const auth = initAuth(db);
const headers = new Headers(req.headers as Record<string, string>);
const auth = createAuthWithHeaders(baseAuth, headers);
let session; let session;
try { try {
session = await auth.api.getSession(); session = await auth.api.getSession({
// @ts-expect-error
headers: new Headers(req.headers),
});
} catch (error) { } catch (error) {
console.error("Error getting session, ", error); console.error("Error getting session, ", error);
throw error; throw error;
} }
return createInnerTRPCContext({ db, user: session?.user, auth }); return createInnerTRPCContext({ db, user: session?.user });
}; };
const t = initTRPC const t = initTRPC

View File

@@ -84,7 +84,7 @@ export const socialProvidersPlugin = () => ({
method: "GET", method: "GET",
}, },
async (ctx) => async (ctx) =>
ctx.json(ctx.context.socialProviders.map((p) => p.id.toLowerCase())), ctx.json(ctx.context.socialProviders.map((p) => p.name.toLowerCase())),
), ),
}, },
}); });