Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
.*
node_modules
dist
contrib
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20.19.4
38 changes: 5 additions & 33 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,50 +1,22 @@
# Multi-stage Dockerfile for organice
# Supports both development and production builds

FROM node:20.17.0-bookworm AS base

WORKDIR /opt/organice

# Copy package files
COPY package.json yarn.lock /opt/organice/

# Development stage
FROM base AS development
FROM node:20.17.0-bookworm-slim AS build

# Copy source code
COPY . /opt/organice

# Generate environment variables
RUN bin/transient_env_vars.sh bait >> .env

# Create non-root user
RUN groupadd organice \
&& useradd -g organice organice \
&& chown -R organice: /opt/organice

USER organice

# Install dependencies
RUN yarn install

ENV NODE_ENV=development
EXPOSE 3000
CMD ["/bin/bash"]

# Build stage
FROM base AS build

# Copy source code
COPY . /opt/organice
WORKDIR /opt/organice

# Install dependencies, including devDependencies like Parcel
RUN yarn install --frozen-lockfile
# But remove the cache to save 1+ gb of container size.
RUN yarn install --frozen-lockfile && yarn cache clean

# Generate environment variables
RUN bin/transient_env_vars.sh bait >> .env

# Build the application
RUN yarn build
RUN yarn build && rm -rf .parcel-cache

# Production stage
FROM node:20.17.0-bookworm-slim AS production
Expand Down
39 changes: 39 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import createWebDAVSyncBackendClient from './sync_backend_clients/webdav_sync_ba
import createGitLabSyncBackendClient, {
createGitlabOAuth,
} from './sync_backend_clients/gitlab_sync_backend_client';
import createGiteaSyncBackendClient, {
createGiteaOAuth,
} from './sync_backend_clients/gitea_sync_backend_client';

import './base.css';

Expand Down Expand Up @@ -67,6 +70,30 @@ const handleGitLabAuthResponse = async (oauthClient) => {
}
};

const handleGiteaAuthResponse = async (oauthClient) => {
let success = false;
try {
success = await oauthClient.isReturningFromAuthServer();
await oauthClient.getAccessToken();
} catch {
success = false;
}
if (!success) {
// Edge case: somehow OAuth success redirect occurred but there isn't a code in
// the current location's search params. This /shouldn't/ happen in practice.
alert('Unexpected sign in error, please try again');
return;
}

const syncClient = createGiteaSyncBackendClient(oauthClient);
const isAccessible = await syncClient.isProjectAccessible();
if (!isAccessible) {
alert('Failed to access Gitea repository - is the URL correct?');
} else {
window.location.search = '';
}
};

export default class App extends PureComponent {
constructor(props) {
super(props);
Expand Down Expand Up @@ -99,6 +126,18 @@ export default class App extends PureComponent {
handleGitLabAuthResponse(gitlabOAuth);
}
break;
case 'Gitea':
const giteaOAuth = createGiteaOAuth();
if (giteaOAuth.isAuthorized()) {
client = createGiteaSyncBackendClient(giteaOAuth);
initialState.syncBackend = Map({
isAuthenticated: true,
client,
});
} else {
handleGiteaAuthResponse(giteaOAuth);
}
break;
case 'WebDAV':
client = createWebDAVSyncBackendClient(
getPersistedField('webdavEndpoint'),
Expand Down
1 change: 1 addition & 0 deletions src/actions/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export const setShouldStoreSettingsInSyncBackend = (newShouldStoreSettingsInSync
switch (client.type) {
case 'Dropbox':
case 'GitLab':
case 'Gitea':
case 'WebDAV':
client
.deleteFile('/.organice-config.json')
Expand Down
11 changes: 10 additions & 1 deletion src/actions/sync_backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { setLoadingMessage, hideLoadingMessage, clearModalStack, setIsLoading }
import { parseFile, setDirty, setLastSyncAt, setOrgFileErrorMessage } from './org';
import { localStorageAvailable, persistField } from '../util/settings_persister';
import { createGitlabOAuth } from '../sync_backend_clients/gitlab_sync_backend_client';
import { createGiteaOAuth } from '../sync_backend_clients/gitea_sync_backend_client';

import { addSeconds } from 'date-fns';

Expand All @@ -30,6 +31,13 @@ export const signOut = () => (dispatch, getState) => {
persistField('gitLabProject', null);
createGitlabOAuth().reset();
break;
case 'Gitea':
persistField('giteaURL', null);
persistField('giteaProject', null);
persistField('giteaClientId', null);
persistField('giteaClientSecret', null);
createGiteaOAuth().reset();
break;
default:
}

Expand Down Expand Up @@ -113,7 +121,8 @@ export const pushBackup = (pathOrFileId, contents) => {
client.createFile(`${pathOrFileId}.organice-bak`, contents);
break;
case 'GitLab':
// No-op for GitLab, because the beauty of version control makes backup files redundant.
case 'Gitea':
// No-op for GitLab/Gitea, because the beauty of version control makes backup files redundant.
break;
default:
}
Expand Down
1 change: 1 addition & 0 deletions src/components/FileBrowser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const FileBrowser = ({
switch (syncBackendType) {
case 'Dropbox':
case 'GitLab':
case 'Gitea':
case 'WebDAV':
const pathParts = path.split('/');
return pathParts.slice(0, pathParts.length - 1).join('/');
Expand Down
66 changes: 66 additions & 0 deletions src/components/SyncServiceSignIn/gitea.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 122 additions & 1 deletion src/components/SyncServiceSignIn/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ import './stylesheet.css';

import DropboxLogo from 'url:./dropbox.svg';
import GitLabLogo from 'url:./gitlab.svg';
import GiteaLogo from 'url:./gitea.svg';

import { persistField } from '../../util/settings_persister';
import {
createGitlabOAuth,
gitLabProjectIdFromURL,
} from '../../sync_backend_clients/gitlab_sync_backend_client';
import {
createGiteaOAuth,
giteaProjectFromURL,
} from '../../sync_backend_clients/gitea_sync_backend_client';

import { DropboxAuth } from 'dropbox';
import _ from 'lodash';
Expand Down Expand Up @@ -149,6 +154,118 @@ function GitLab() {
);
}

function Gitea() {
const [isVisible, setIsVisible] = useState(false);
const toggleVisible = () => setIsVisible(!isVisible);

const defaultURL = 'https://gitea.example.com';
const defaultProject = 'https://gitea.example.com/owner/repo';
const [giteaURL, setGiteaURL] = useState(defaultURL);
const [project, setProject] = useState(defaultProject);
const [clientId, setClientId] = useState('');
const [clientSecret, setClientSecret] = useState('');
const [branch, setBranch] = useState('');

const handleSubmit = (evt) => {
evt.preventDefault();
const projectInfo = giteaProjectFromURL(project);
if (!giteaURL) {
alert('Please enter your Gitea instance URL');
return;
}
if (!clientId || !clientSecret) {
alert('Please enter your OAuth Client ID and Secret');
return;
}
if (projectInfo) {
persistField('authenticatedSyncService', 'Gitea');
persistField('giteaURL', giteaURL);
persistField('giteaProject', JSON.stringify(projectInfo));
persistField('giteaClientId', clientId);
persistField('giteaClientSecret', clientSecret);
// Only persist branch if user specified one (empty string means use default)
if (branch) {
persistField('giteaBranch', branch);
} else {
persistField('giteaBranch', null);
}
try {
createGiteaOAuth().fetchAuthorizationCode();
} catch (e) {
alert('Error initializing OAuth: ' + e.message);
}
} else {
alert('Project does not appear to be a valid Gitea repository URL (should be owner/repo)');
}
};

return (
<>
<a href="#gitea" onClick={toggleVisible}>
<img src={GiteaLogo} alt="Gitea logo" />
</a>
{isVisible && (
<form onSubmit={handleSubmit}>
<p>
<label htmlFor="input-gitea-url">Gitea Instance URL:</label>
<input
id="input-gitea-url"
type="url"
className="textfield"
placeholder={defaultURL}
value={giteaURL}
onChange={(e) => setGiteaURL(e.target.value)}
/>
</p>
<p>
<label htmlFor="input-gitea-project">Repository:</label>
<input
id="input-gitea-project"
type="url"
className="textfield"
placeholder={defaultProject}
value={project}
onChange={(e) => setProject(e.target.value)}
/>
</p>
<p>
<label htmlFor="input-gitea-client-id">OAuth Client ID:</label>
<input
id="input-gitea-client-id"
type="text"
className="textfield"
value={clientId}
onChange={(e) => setClientId(e.target.value)}
/>
</p>
<p>
<label htmlFor="input-gitea-client-secret">OAuth Client Secret:</label>
<input
id="input-gitea-client-secret"
type="password"
className="textfield"
value={clientSecret}
onChange={(e) => setClientSecret(e.target.value)}
/>
</p>
<p>
<label htmlFor="input-gitea-branch">Branch (optional):</label>
<input
id="input-gitea-branch"
type="text"
className="textfield"
placeholder="Leave empty for default branch"
value={branch}
onChange={(e) => setBranch(e.target.value)}
/>
</p>
<input type="submit" value="Sign-in" />
</form>
)}
</>
);
}

export default class SyncServiceSignIn extends PureComponent {
constructor(props) {
super(props);
Expand Down Expand Up @@ -178,7 +295,7 @@ export default class SyncServiceSignIn extends PureComponent {
return (
<div className="sync-service-sign-in-container">
<p className="sync-service-sign-in__help-text">
organice syncs your files with Dropbox, GitLab, and WebDAV.
organice syncs your files with Dropbox, GitLab, Gitea, and WebDAV.
</p>
<p className="sync-service-sign-in__help-text">Click to sign in with:</p>

Expand All @@ -192,6 +309,10 @@ export default class SyncServiceSignIn extends PureComponent {
<GitLab />
</div>

<div className="sync-service-container">
<Gitea />
</div>

<div className="sync-service-container">
<WebDAVForm />
</div>
Expand Down
Loading