Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(password-protected-folders): add password protected folders app #12137

Merged
merged 1 commit into from
Jan 30, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Add password protected folders app

We've added a new application called "Password protected folders". This application allows users to create new folders that are accessible only by entering a password.

https://github.com/owncloud/web/pull/12137
https://github.com/owncloud/web/issues/12039
3 changes: 2 additions & 1 deletion dev/docker/ocis.web.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"epub-reader",
"app-store",
"activities",
"preview"
"preview",
"password-protected-folders"
]
}
20 changes: 20 additions & 0 deletions packages/web-app-password-protected-folders/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# web-app-password-protected-folders

This web extension enhances the oCIS platform by allowing users to create password-protected folders. It provides an additional layer of security for sensitive or confidential information stored within oCIS.

## Features

- 🔒 Create password-protected folders within oCIS
- 🔑 Set unique passwords for each protected folder
- 🎨 Seamless integration with the oCIS user interface

## Usage

Once the extension is installed, users can create password-protected folders by following these steps:

1. Log in to your oCIS instance.
2. Navigate to the folder where you want to create a password-protected subfolder.
3. Click the "New" button and select "Password Protected Folder."
4. Enter a name for the folder and set a unique password.
5. Click "Create" to create the password-protected folder.
6. To access the protected folder, users will be prompted to enter the corresponding password.
10 changes: 10 additions & 0 deletions packages/web-app-password-protected-folders/l10n/.tx/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com

[o:owncloud-org:p:owncloud-web:r:password-protected-folders]
file_filter = locale/<lang>/app.po
minimum_perc = 0
resource_name = web-password-protected-folders
source_file = template.pot
source_lang = en
type = PO
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
24 changes: 24 additions & 0 deletions packages/web-app-password-protected-folders/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "web-app-password-protected-folders",
"version": "0.4.0",
"private": true,
"description": "ownCloud Web password protected folders",
"license": "AGPL-3.0",
"type": "module",
"scripts": {
"build": "pnpm vite build",
"build:w": "pnpm vite build --watch --mode development",
"check:types": "vue-tsc --noEmit",
"test:unit": "NODE_OPTIONS=--unhandled-rejections=throw vitest"
},
"devDependencies": {
"@ownclouders/web-client": "workspace:*",
"@ownclouders/web-pkg": "workspace:*",
"@ownclouders/web-test-helpers": "workspace:*",
"@vue/test-utils": "^2.4.6",
"uuid": "^11.0.0",
"vitest-mock-extended": "2.0.2",
"vue": "^3.4.21",
"vue3-gettext": "^2.4.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"entrypoint": "password-protected-folders.js"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<template>
<form autocomplete="off" @submit.prevent="emit('confirm')">
<oc-text-input
id="input-folder-name"
v-model="formData.folderName"
:label="$gettext('Folder name')"
/>
<oc-text-input
id="input-folder-password"
v-model="formData.password"
type="password"
:label="$gettext('Password')"
class="oc-mt-s"
/>
<input type="submit" class="oc-hidden" />
</form>
</template>

<script lang="ts" setup>
import { useMessages, useResourcesStore, useSpacesStore } from '@ownclouders/web-pkg'
import { computed, reactive, unref, watch } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useCreateFileHandler } from '../composables/useCreateFileHandler'

const emit = defineEmits<{
confirm: []
'update:confirmDisabled': [isDisabled: boolean]
}>()

const { $gettext } = useGettext()
const { showErrorMessage } = useMessages()
const { createFileHandler } = useCreateFileHandler()
const { currentFolder } = useResourcesStore()
const { currentSpace } = useSpacesStore()

const formData = reactive({
folderName: '',
password: ''
})

const isFormValid = computed(() => formData.folderName !== '' && formData.password !== '')

const onConfirm = async () => {
if (!unref(isFormValid)) {
return Promise.reject()
}

try {
await createFileHandler({
fileName: formData.folderName,
currentFolder: unref(currentFolder),
space: unref(currentSpace),
password: formData.password
})
} catch (error) {
console.error(error)
showErrorMessage({ title: $gettext('Failed to create folder'), errors: [error] })
}
}

watch(
isFormValid,
(isValid) => {
emit('update:confirmDisabled', !isValid)
},
{ immediate: true }
)

defineExpose({ onConfirm })
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Resource, SpaceResource, urlJoin } from '@ownclouders/web-client'
import { SharingLinkType } from '@ownclouders/web-client/graph/generated'
import { useClientService, useResourcesStore, useSharesStore } from '@ownclouders/web-pkg'
import { unref } from 'vue'

export const useCreateFileHandler = () => {
const clientService = useClientService()
const { upsertResource } = useResourcesStore()
const { addLink } = useSharesStore()

const createFileHandler = async ({
fileName,
space,
currentFolder,
password
}: {
fileName: string
space: SpaceResource
currentFolder: Resource
password: string
}) => {
if (fileName === '') {
return
}

const folderPath = '/.' + fileName

const folder = await clientService.webdav.createFolder(unref(space), { path: folderPath })
upsertResource(folder)

await addLink({
clientService,
space,
resource: folder,
options: { password, type: SharingLinkType.Edit }
})

const path = urlJoin(currentFolder.path, fileName + '.psec')

const file = await clientService.webdav.putFileContents(unref(space), { path })
upsertResource(file)
}

return { createFileHandler }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useModals } from '@ownclouders/web-pkg'
import { useGettext } from 'vue3-gettext'
import CreateFolderModal from '../components/CreateFolderModal.vue'

export const useCustomHandler = () => {
const { dispatchModal } = useModals()
const { $gettext } = useGettext()

const customHandler = () => {
dispatchModal({
title: $gettext('Create a new password protected folder'),
customComponent: CreateFolderModal,
confirmText: $gettext('Create')
})
}

return { customHandler }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ActionExtension } from '@ownclouders/web-pkg'
import { computed, unref } from 'vue'
import { useOpenFolderAction } from './useOpenFolderAction'

export const useExtensions = () => {
const action = useOpenFolderAction()

const actionExtension = computed<ActionExtension>(() => ({
id: 'com.github.owncloud.web-extensions.password-protected-folders',
type: 'action',
extensionPointIds: ['global.files.context-actions', 'global.files.default-actions'],
action: unref(action)
}))

return computed(() => [unref(actionExtension)])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { FileAction } from '@ownclouders/web-pkg'
import { computed } from 'vue'
import { useGettext } from 'vue3-gettext'

export const useOpenFolderAction = () => {
const { $gettext } = useGettext()

const action = computed<FileAction>(() => ({
name: 'open-password-protected-folder',
icon: 'external-link',
handler: () => {
// TODO: add handler
console.warn('NOT IMPLEMENTED')
},
label: () => $gettext('Open folder'),
isDisabled: () => false,
isVisible: ({ resources }) => {
if (resources.length !== 1) {
return false
}

return resources[0].extension === 'psec'
},
componentType: 'button',
class: 'oc-files-actions-open-password-protected-folder'
}))

return action
}
29 changes: 29 additions & 0 deletions packages/web-app-password-protected-folders/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useGettext } from 'vue3-gettext'
import translations from '../l10n/translations.json'
import { defineWebApplication } from '@ownclouders/web-pkg'
import { useExtensions } from './composables/useExtensions'
import { useCustomHandler } from './composables/useCustomHandler'

export default defineWebApplication({
setup() {
const { $gettext } = useGettext()
const extensions = useExtensions()
const { customHandler } = useCustomHandler()

return {
appInfo: {
name: $gettext('Password Protected Folders'),
id: 'password-protected-folders',
extensions: [
{
newFileMenu: { menuTitle: () => $gettext('Password Protected Folder') },
extension: 'psec',
customHandler
}
]
},
translations,
extensions
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { defaultComponentMocks, defaultPlugins, shallowMount } from '@ownclouders/web-test-helpers'
import CreateFolderModal from '../../../src/components/CreateFolderModal.vue'
import { useCreateFileHandler } from '../../../src/composables/useCreateFileHandler'
import { mock } from 'vitest-mock-extended'
import { Resource, SpaceResource } from '@ownclouders/web-client'
import { VueWrapper } from '@vue/test-utils'

vi.mock('../../../src/composables/useCreateFileHandler', () => ({
useCreateFileHandler: vi.fn().mockReturnValue({ createFileHandler: vi.fn() })
}))

const currentFolder = mock<Resource>()
const currentSpace = mock<SpaceResource>()

const SELECTORS = Object.freeze({
inputFolderName: '#input-folder-name',
inputFolderPassword: '#input-folder-password'
})

describe('CreateFolderModal', () => {
it('should call "createFileHandler" when form is valid', () => {
const { wrapper } = getWrapper()

const folderNameInput = wrapper.findComponent(SELECTORS.inputFolderName) as VueWrapper
const passwordInput = wrapper.findComponent(SELECTORS.inputFolderPassword) as VueWrapper

folderNameInput.vm.$emit('update:modelValue', 'name')
passwordInput.vm.$emit('update:modelValue', 'password')

wrapper.vm.onConfirm()

expect(useCreateFileHandler().createFileHandler).toHaveBeenCalledWith({
fileName: 'name',
password: 'password',
space: currentSpace,
currentFolder
})
})

it('should not call "createFileHandler" when form is invalid', () => {
const { wrapper } = getWrapper()

const folderNameInput = wrapper.findComponent(SELECTORS.inputFolderName) as VueWrapper
folderNameInput.vm.$emit('update:modelValue', 'name')

expect(wrapper.vm.onConfirm()).rejects.toThrow()
expect(useCreateFileHandler().createFileHandler).not.toHaveBeenCalled()
})
})

function getWrapper() {
const mocks = defaultComponentMocks()

return {
wrapper: shallowMount(CreateFolderModal, {
global: {
plugins: defaultPlugins({
piniaOptions: {
resourcesStore: { currentFolder },
spacesState: { currentSpace }
}
}),
mocks,
provide: mocks
}
}),
mocks
}
}
Loading