Template
Compare commits
3
Commits
7d1884be41
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a74017f0c8 | ||
|
|
d7d18e111d | ||
|
|
2d4f5b23b5 |
No files matched your search
@@ -32,12 +32,23 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
# Plain git instead of actions/checkout: rootless podman on cirunner-vps
|
||||||
|
# rejects symlinks inside cached actions ("path escapes from parent").
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ gitea.token }}
|
||||||
|
run: |
|
||||||
|
git init -q .
|
||||||
|
git remote add origin "${{ gitea.server_url }}/${{ gitea.repository }}.git"
|
||||||
|
auth="$(printf 'x-access-token:%s' "$GITEA_TOKEN" | base64 -w0)"
|
||||||
|
git -c http.extraHeader="Authorization: basic $auth" \
|
||||||
|
fetch -q --depth 1 origin "${{ gitea.sha }}"
|
||||||
|
git checkout -q FETCH_HEAD
|
||||||
|
|
||||||
- name: Set up Bun
|
- name: Set up Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
# Install script instead of oven-sh/setup-bun (same podman symlink issue).
|
||||||
with:
|
run: |
|
||||||
bun-version: 1.3.14
|
curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.14" >/dev/null
|
||||||
|
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: ./scripts/ci-install
|
run: ./scripts/ci-install
|
||||||
@@ -61,12 +72,23 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
# Plain git instead of actions/checkout: rootless podman on cirunner-vps
|
||||||
|
# rejects symlinks inside cached actions ("path escapes from parent").
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ gitea.token }}
|
||||||
|
run: |
|
||||||
|
git init -q .
|
||||||
|
git remote add origin "${{ gitea.server_url }}/${{ gitea.repository }}.git"
|
||||||
|
auth="$(printf 'x-access-token:%s' "$GITEA_TOKEN" | base64 -w0)"
|
||||||
|
git -c http.extraHeader="Authorization: basic $auth" \
|
||||||
|
fetch -q --depth 1 origin "${{ gitea.sha }}"
|
||||||
|
git checkout -q FETCH_HEAD
|
||||||
|
|
||||||
- name: Set up Bun
|
- name: Set up Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
# Install script instead of oven-sh/setup-bun (same podman symlink issue).
|
||||||
with:
|
run: |
|
||||||
bun-version: 1.3.14
|
curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.14" >/dev/null
|
||||||
|
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: ./scripts/ci-install
|
run: ./scripts/ci-install
|
||||||
@@ -88,7 +110,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
# Plain git instead of actions/checkout: rootless podman on cirunner-vps
|
||||||
|
# rejects symlinks inside cached actions ("path escapes from parent").
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ gitea.token }}
|
||||||
|
run: |
|
||||||
|
git init -q .
|
||||||
|
git remote add origin "${{ gitea.server_url }}/${{ gitea.repository }}.git"
|
||||||
|
auth="$(printf 'x-access-token:%s' "$GITEA_TOKEN" | base64 -w0)"
|
||||||
|
git -c http.extraHeader="Authorization: basic $auth" \
|
||||||
|
fetch -q --depth 1 origin "${{ gitea.sha }}"
|
||||||
|
git checkout -q FETCH_HEAD
|
||||||
|
|
||||||
- name: Log in to container registry
|
- name: Log in to container registry
|
||||||
run: |
|
run: |
|
||||||
@@ -119,9 +151,14 @@ jobs:
|
|||||||
# failure only warns: the image is pushed and the midnight
|
# failure only warns: the image is pushed and the midnight
|
||||||
# podman-update.timer deploys it anyway.
|
# podman-update.timer deploys it anyway.
|
||||||
- name: Trigger immediate deploy
|
- name: Trigger immediate deploy
|
||||||
|
# The key comes in through env, not ${{ secrets.* }} in the script:
|
||||||
|
# the runner prints the script, and Gitea does not mask a multi-line
|
||||||
|
# secret once it is split across script lines.
|
||||||
|
env:
|
||||||
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
run: |
|
run: |
|
||||||
install -m600 /dev/null "$HOME/.deploy_key"
|
install -m600 /dev/null "$HOME/.deploy_key"
|
||||||
printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY }}" > "$HOME/.deploy_key"
|
printf '%s\n' "$DEPLOY_SSH_KEY" > "$HOME/.deploy_key"
|
||||||
ssh -i "$HOME/.deploy_key" \
|
ssh -i "$HOME/.deploy_key" \
|
||||||
-o BatchMode=yes \
|
-o BatchMode=yes \
|
||||||
-o StrictHostKeyChecking=accept-new \
|
-o StrictHostKeyChecking=accept-new \
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { Preloaded } from 'convex/react';
|
import type { Preloaded } from 'convex/react';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useMutation, usePreloadedQuery, useQuery } from 'convex/react';
|
import { useMutation, usePreloadedQuery } from 'convex/react';
|
||||||
import { Loader2, Pencil, Upload, XIcon } from 'lucide-react';
|
import { Loader2, Pencil, Upload, XIcon } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@@ -48,11 +48,6 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
|||||||
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
|
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
|
||||||
const updateUser = useMutation(api.auth.updateUser);
|
const updateUser = useMutation(api.auth.updateUser);
|
||||||
|
|
||||||
const currentImageUrl = useQuery(
|
|
||||||
api.files.getImageUrl,
|
|
||||||
user?.image ? { storageId: user.image as Id<'_storage'> } : 'skip',
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0] ?? null;
|
const file = event.target.files?.[0] ?? null;
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@@ -117,7 +112,7 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
|||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
>
|
>
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
src={currentImageUrl ?? undefined}
|
src={user?.imageUrl}
|
||||||
fullName={user?.name}
|
fullName={user?.name}
|
||||||
className='h-42 w-42 text-6xl font-semibold'
|
className='h-42 w-42 text-6xl font-semibold'
|
||||||
userIconProps={{ size: 100 }}
|
userIconProps={{ size: 100 }}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { resetAnalyticsIdentity, useAnalyticsIdentity } from '@/lib/analytics';
|
|||||||
import { useAuthActions } from '@convex-dev/auth/react';
|
import { useAuthActions } from '@convex-dev/auth/react';
|
||||||
import { useConvexAuth, useQuery } from 'convex/react';
|
import { useConvexAuth, useQuery } from 'convex/react';
|
||||||
|
|
||||||
import type { Id } from '@gib/backend/convex/_generated/dataModel.js';
|
|
||||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||||
import {
|
import {
|
||||||
BasedAvatar,
|
BasedAvatar,
|
||||||
@@ -25,10 +24,6 @@ export const AvatarDropdown = () => {
|
|||||||
const { signOut } = useAuthActions();
|
const { signOut } = useAuthActions();
|
||||||
const user = useQuery(api.auth.getUser, {});
|
const user = useQuery(api.auth.getUser, {});
|
||||||
useAnalyticsIdentity(user?._id);
|
useAnalyticsIdentity(user?._id);
|
||||||
const currentImageUrl = useQuery(
|
|
||||||
api.files.getImageUrl,
|
|
||||||
user?.image ? { storageId: user.image as Id<'_storage'> } : 'skip',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +48,7 @@ export const AvatarDropdown = () => {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
src={currentImageUrl}
|
src={user?.imageUrl}
|
||||||
fullName={user?.name}
|
fullName={user?.name}
|
||||||
className='h-9 w-9'
|
className='h-9 w-9'
|
||||||
fallbackProps={{ className: 'text-sm font-semibold' }}
|
fallbackProps={{ className: 'text-sm font-semibold' }}
|
||||||
|
|||||||
@@ -45,12 +45,24 @@ export const getUserProvider = query({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `users.image` holds either a Convex storage ID (avatar uploaded in-app) or a
|
||||||
|
* plain URL (the `picture` claim Authentik writes on every OAuth sign-in).
|
||||||
|
* Resolves either form to a URL the client can render directly.
|
||||||
|
*/
|
||||||
|
const resolveImageUrl = async (ctx: QueryCtx, image: string | undefined) => {
|
||||||
|
if (!image) return null;
|
||||||
|
const storageId = ctx.db.system.normalizeId('_storage', image);
|
||||||
|
return storageId ? await ctx.storage.getUrl(storageId) : image;
|
||||||
|
};
|
||||||
|
|
||||||
export const getUser = query({
|
export const getUser = query({
|
||||||
args: { userId: v.optional(v.id('users')) },
|
args: { userId: v.optional(v.id('users')) },
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const userId = args.userId ?? (await getAuthUserId(ctx));
|
const userId = args.userId ?? (await getAuthUserId(ctx));
|
||||||
if (!userId) return null;
|
if (!userId) return null;
|
||||||
return getUserById(ctx, userId);
|
const user = await getUserById(ctx, userId);
|
||||||
|
return { ...user, imageUrl: await resolveImageUrl(ctx, user.image) };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,7 +85,10 @@ export const updateUser = mutation({
|
|||||||
if (args.name !== undefined) patch.name = args.name;
|
if (args.name !== undefined) patch.name = args.name;
|
||||||
if (args.email !== undefined) patch.email = args.email;
|
if (args.email !== undefined) patch.email = args.email;
|
||||||
if (args.image !== undefined) {
|
if (args.image !== undefined) {
|
||||||
const oldImage = user.image as Id<'_storage'> | undefined;
|
// Only delete the previous avatar when it was an uploaded file, not an IdP URL.
|
||||||
|
const oldImage = user.image
|
||||||
|
? ctx.db.system.normalizeId('_storage', user.image)
|
||||||
|
: null;
|
||||||
patch.image = args.image;
|
patch.image = args.image;
|
||||||
if (oldImage && oldImage !== args.image) {
|
if (oldImage && oldImage !== args.image) {
|
||||||
await ctx.storage.delete(oldImage);
|
await ctx.storage.delete(oldImage);
|
||||||
|
|||||||
Reference in new issue
Block a user