Skip to content

Commit

Permalink
feat(password-protected-folders): add password protected folders app
Browse files Browse the repository at this point in the history
  • Loading branch information
LukasHirt committed Jan 28, 2025
1 parent 9e58069 commit 6505b98
Show file tree
Hide file tree
Showing 15 changed files with 407 additions and 1 deletion.
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

0 comments on commit 6505b98

Please sign in to comment.